myagent-ai 1.23.20 → 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/index.html +15 -2776
package/web/ui/index.html
CHANGED
|
@@ -354,2781 +354,20 @@ tr:hover{background:var(--surface2)}
|
|
|
354
354
|
</div>
|
|
355
355
|
<div id="modalContainer"></div>
|
|
356
356
|
|
|
357
|
-
<script>
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
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+')'}}
|
|
373
|
-
return data;
|
|
374
|
-
}catch(e){showToast('请求失败: '+e.message,'danger');return {error:e.message}}
|
|
375
|
-
}
|
|
376
|
-
function $(id){return document.getElementById(id)}
|
|
377
|
-
function escHtml(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''')}
|
|
378
|
-
/* [v1.20.2] 安全地将任意文本插入 JS 模板字符串中 textarea 元素:转义反引号、${ 和 </textarea */
|
|
379
|
-
function escTpl(s){return String(s||'').replace(/\\/g,'\\\\').replace(/`/g,'\\`').replace(/\$\{/g,'\\${').replace(/<\/textarea/gi,'<\\/textarea')}
|
|
380
|
-
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)}}
|
|
381
|
-
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)+'天前'}
|
|
382
|
-
function showToast(msg,type='info',duration=3000){
|
|
383
|
-
const colors={success:'var(--success)',danger:'var(--danger)',warn:'var(--warn)',info:'var(--primary)'};
|
|
384
|
-
const el=document.createElement('div');el.className='toast';el.style.background=colors[type]||colors.info;el.style.color='#fff';
|
|
385
|
-
el.style.whiteSpace='pre-line';el.style.maxWidth='420px';el.style.textAlign='left';
|
|
386
|
-
el.textContent=msg;document.body.appendChild(el);setTimeout(()=>el.remove(),duration);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// ── 版本号 & 更新检查 ──
|
|
390
|
-
async function loadVersion(){
|
|
391
|
-
try{
|
|
392
|
-
const r=await api('/api/status');
|
|
393
|
-
if(r.version){
|
|
394
|
-
$('sidebarVersion').textContent='v'+r.version;
|
|
395
|
-
}
|
|
396
|
-
}catch(e){}
|
|
397
|
-
}
|
|
398
|
-
async function checkUpdate(manual){
|
|
399
|
-
try{
|
|
400
|
-
const r=await api('/api/update/check',{method:'POST'});
|
|
401
|
-
if(r.error){if(manual) showToast('检查更新失败: '+r.error,'danger'); return}
|
|
402
|
-
if(r.has_update){
|
|
403
|
-
$('updateBadge').style.display='inline';
|
|
404
|
-
showToast('发现新版本 v'+r.latest_version,'warn');
|
|
405
|
-
}else{
|
|
406
|
-
$('updateBadge').style.display='none';
|
|
407
|
-
if(manual) showToast('已是最新版本 v'+r.current_version,'success');
|
|
408
|
-
}
|
|
409
|
-
}catch(e){if(manual) showToast('检查更新失败','danger')}
|
|
410
|
-
}
|
|
411
|
-
async function doUpdate(){
|
|
412
|
-
if(!confirm('确定要更新到最新版本吗?更新后将自动重启。'))return;
|
|
413
|
-
try{
|
|
414
|
-
const r=await api('/api/update/apply',{method:'POST',body:JSON.stringify({type:'full'})});
|
|
415
|
-
if(r.error){showToast('更新失败: '+r.error,'danger');return}
|
|
416
|
-
showToast('更新已开始,服务将自动重启...','success');
|
|
417
|
-
}catch(e){showToast('更新失败','danger')}
|
|
418
|
-
}
|
|
419
|
-
// 页面加载时获取版本号,30秒后自动检查更新
|
|
420
|
-
// ── Theme Management ──
|
|
421
|
-
function initTheme(){const s=localStorage.getItem('myagent-theme')||'claude';document.documentElement.setAttribute('data-theme',s);updateThemeIcon(s)}
|
|
422
|
-
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)}
|
|
423
|
-
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='切换到夜间模式'}}
|
|
424
|
-
// ── Sidebar Collapse ──
|
|
425
|
-
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()}
|
|
426
|
-
// Initialize
|
|
427
|
-
initTheme();
|
|
428
|
-
document.getElementById('themeToggle')?.addEventListener('click',toggleTheme);
|
|
429
|
-
if(localStorage.getItem('myagent-admin-sidebar-collapsed')==='true'){document.getElementById('adminSidebar')?.classList.add('collapsed');const t=document.getElementById('sidebarToggle');if(t)t.textContent='▶'}
|
|
430
|
-
|
|
431
|
-
// ── Mobile Sidebar ──
|
|
432
|
-
function toggleMobileSidebar(){
|
|
433
|
-
const s=document.getElementById('adminSidebar');
|
|
434
|
-
const o=document.getElementById('adminMobileOverlay');
|
|
435
|
-
const isOpen=s.classList.contains('mobile-open');
|
|
436
|
-
if(isOpen){closeMobileSidebar();}else{s.classList.add('mobile-open');o.classList.add('active');}
|
|
437
|
-
}
|
|
438
|
-
function closeMobileSidebar(){
|
|
439
|
-
document.getElementById('adminSidebar').classList.remove('mobile-open');
|
|
440
|
-
document.getElementById('adminMobileOverlay').classList.remove('active');
|
|
441
|
-
}
|
|
442
|
-
// Show hamburger on mobile + tap sidebar to expand on mobile
|
|
443
|
-
function checkMobile(){
|
|
444
|
-
const btn=document.getElementById('mobileMenuBtn');
|
|
445
|
-
if(btn)btn.style.display=window.innerWidth<=768?'grid':'none';
|
|
446
|
-
}
|
|
447
|
-
window.addEventListener('resize',checkMobile);
|
|
448
|
-
checkMobile();
|
|
449
|
-
// [v1.18.10] 移动端点击侧滑栏窄栏区域也可展开
|
|
450
|
-
document.getElementById('adminSidebar')?.addEventListener('click',function(e){
|
|
451
|
-
if(window.innerWidth>768)return;
|
|
452
|
-
// 如果已经是展开状态,不处理(让 nav-item 的 onclick 正常执行)
|
|
453
|
-
if(this.classList.contains('mobile-open'))return;
|
|
454
|
-
// 只在点击侧滑栏本身(非具体按钮/链接)时展开
|
|
455
|
-
if(e.target.closest('.nav-item')||e.target.closest('a')||e.target.closest('button'))return;
|
|
456
|
-
toggleMobileSidebar();
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
loadVersion();
|
|
460
|
-
setTimeout(()=>checkUpdate(false),30000);
|
|
461
|
-
function showConfirm(title,msg,onOk){
|
|
462
|
-
// 使用 data 属性存储回调,onclick 中调用它并正确处理 async
|
|
463
|
-
const id='confirmCallback';
|
|
464
|
-
window[id]=async()=>{try{await onOk();}catch(e){console.error(e);}delete window[id];};
|
|
465
|
-
$('modalContainer').innerHTML=`<div class="confirm-overlay" onclick="closeModal()"><div class="confirm-box" onclick="event.stopPropagation()">
|
|
466
|
-
<h4>${title}</h4><p>${msg}</p><div class="btns">
|
|
467
|
-
<button class="btn btn-danger" onclick="${id}();closeModal()">确认</button>
|
|
468
|
-
<button class="btn btn-ghost" onclick="closeModal()">取消</button></div></div></div>`;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function showPage(page, addHistory){
|
|
472
|
-
closeMobileSidebar();
|
|
473
|
-
currentPage=page;
|
|
474
|
-
$('content').setAttribute('data-page',page);
|
|
475
|
-
document.querySelectorAll('.nav-item').forEach((n,i)=>n.classList.toggle('active',Object.keys(pages)[i]===page));
|
|
476
|
-
$('pageTitle').textContent=pages[page]||page;
|
|
477
|
-
const renderers={dashboard:renderDashboard,agents:renderAgents,platforms:renderPlatforms,organization:renderOrganization,departments:renderDepartments,sessions:renderSessions,memory:renderMemory,permissions:renderPermissions,llm:renderLLM,system:renderSystem,executor:renderExecutor,skills:renderSkills,files:renderFiles,logs:renderLogs,tasks:renderTasks};
|
|
478
|
-
if(renderers[page]){
|
|
479
|
-
try{renderers[page]();}catch(e){console.error('Page render error:',e);$('content').innerHTML='<div class="empty" style="color:var(--danger)">页面加载失败: '+escHtml(e.message)+'</div>';}
|
|
480
|
-
}
|
|
481
|
-
// 记录到浏览器历史
|
|
482
|
-
if(addHistory!==false){
|
|
483
|
-
var _sub=window._navSubState||null;
|
|
484
|
-
var hash=page+(_sub?'~'+_sub:'');
|
|
485
|
-
history.pushState({page:page,sub:_sub},'','#'+hash);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// 导航历史系统:记录子页面跳转(如 viewSession、viewSessionRaw)
|
|
490
|
-
// _navHistory: [{page,sub}] 最多6步
|
|
491
|
-
var _navHistory=[];
|
|
492
|
-
var _navSubState=null;
|
|
493
|
-
|
|
494
|
-
function navigateTo(page, sub, renderFn){
|
|
495
|
-
// 保存当前状态到历史
|
|
496
|
-
_navHistory.push({page:currentPage,sub:window._navSubState});
|
|
497
|
-
if(_navHistory.length>6)_navHistory.shift();
|
|
498
|
-
currentPage=page;
|
|
499
|
-
_navSubState=sub;
|
|
500
|
-
$('content').setAttribute('data-page',page);
|
|
501
|
-
// 更新 URL hash
|
|
502
|
-
var hash=page+(sub?'~'+sub:'');
|
|
503
|
-
history.pushState({page:page,sub:sub},'','#'+hash);
|
|
504
|
-
// 更新导航高亮
|
|
505
|
-
document.querySelectorAll('.nav-item').forEach((n,i)=>n.classList.toggle('active',Object.keys(pages)[i]===page));
|
|
506
|
-
// 渲染页面
|
|
507
|
-
if(renderFn){
|
|
508
|
-
try{renderFn();}catch(e){console.error('Navigate render error:',e);}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function goBack(){
|
|
513
|
-
if(_navHistory.length>0){
|
|
514
|
-
var prev=_navHistory.pop();
|
|
515
|
-
currentPage=prev.page;
|
|
516
|
-
_navSubState=prev.sub;
|
|
517
|
-
var hash=prev.page+(prev.sub?'~'+prev.sub:'');
|
|
518
|
-
history.pushState({page:prev.page,sub:prev.sub},'','#'+hash);
|
|
519
|
-
// 找到对应的渲染函数
|
|
520
|
-
var renderers={dashboard:renderDashboard,agents:renderAgents,platforms:renderPlatforms,organization:renderOrganization,departments:renderDepartments,sessions:renderSessions,memory:renderMemory,permissions:renderPermissions,llm:renderLLM,system:renderSystem,executor:renderExecutor,skills:renderSkills,files:renderFiles,logs:renderLogs,tasks:renderTasks};
|
|
521
|
-
if(renderers[prev.page]){
|
|
522
|
-
try{renderers[prev.page]();}catch(e){}
|
|
523
|
-
}
|
|
524
|
-
document.querySelectorAll('.nav-item').forEach((n,i)=>n.classList.toggle('active',Object.keys(pages)[i]===prev.page));
|
|
525
|
-
}else{
|
|
526
|
-
// 没有内部历史,尝试浏览器后退
|
|
527
|
-
history.back();
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// 浏览器前进/后退按钮支持
|
|
532
|
-
window.addEventListener('popstate',function(e){
|
|
533
|
-
var s=e.state;
|
|
534
|
-
if(s&&s.page){
|
|
535
|
-
currentPage=s.page;
|
|
536
|
-
_navSubState=s.sub||null;
|
|
537
|
-
document.querySelectorAll('.nav-item').forEach((n,i)=>n.classList.toggle('active',Object.keys(pages)[i]===s.page));
|
|
538
|
-
var renderers={dashboard:renderDashboard,agents:renderAgents,platforms:renderPlatforms,organization:renderOrganization,departments:renderDepartments,sessions:renderSessions,memory:renderMemory,permissions:renderPermissions,llm:renderLLM,system:renderSystem,executor:renderExecutor,skills:renderSkills,files:renderFiles,logs:renderLogs,tasks:renderTasks};
|
|
539
|
-
if(renderers[s.page]){
|
|
540
|
-
try{renderers[s.page]();}catch(e){}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
// ========== Dashboard ==========
|
|
546
|
-
async function renderDashboard(){
|
|
547
|
-
const s = await api('/api/status');
|
|
548
|
-
const gl = s.global_llm || {};
|
|
549
|
-
const models = s.models || [];
|
|
550
|
-
const mem = s.memory || {};
|
|
551
|
-
const q = s.queue || {};
|
|
552
|
-
const runningAgents = s.running_agents || [];
|
|
553
|
-
|
|
554
|
-
// ── 辅助函数 ──
|
|
555
|
-
const modeLabel = m => ({text:'文本',image:'图片',video:'视频',audio:'音频'}[m] || m);
|
|
556
|
-
const modeIcon = m => ({text:'T',image:'🖼',video:'🎬',audio:'🎤'}[m] || m);
|
|
557
|
-
const providerLabel = p => ({openai:'OpenAI',anthropic:'Anthropic',ollama:'Ollama',zhipu:'智谱',custom:'自定义',gemini:'Google'}[p] || p);
|
|
558
|
-
const hasImage = modes => (modes || []).includes('image');
|
|
559
|
-
const isGlobalFallback = m => m.is_global_fallback && m.enabled;
|
|
560
|
-
|
|
561
|
-
// 找到全局默认模型(从模型库中 is_global_fallback 的启用模型)
|
|
562
|
-
const globalModel = models.find(m => isGlobalFallback(m)) || null;
|
|
563
|
-
|
|
564
|
-
// input_modes 标签 HTML
|
|
565
|
-
const modesHtml = (modes) => {
|
|
566
|
-
if (!modes || modes.length === 0) return '';
|
|
567
|
-
return '<div class="dash-model-modes">' + modes.map(m =>
|
|
568
|
-
'<span class="dash-mode-tag ' + m + '">' + modeLabel(m) + '</span>'
|
|
569
|
-
).join('') + '</div>';
|
|
570
|
-
};
|
|
571
|
-
|
|
572
|
-
// 模型卡片 HTML
|
|
573
|
-
const modelCardHtml = (m, isActive) => {
|
|
574
|
-
const multimodal = hasImage(m.input_modes);
|
|
575
|
-
return '<div class="dash-model-card' + (isActive ? ' active' : '') + '">' +
|
|
576
|
-
'<div class="dash-model-icon ' + (multimodal ? 'multimodal' : 'text') + '">' +
|
|
577
|
-
(multimodal ? '🖼' : '💬') +
|
|
578
|
-
'</div>' +
|
|
579
|
-
'<div class="dash-model-info">' +
|
|
580
|
-
'<div class="dash-model-name">' + esc(m.name || m.model || m.id) + '</div>' +
|
|
581
|
-
'<div class="dash-model-provider">' + providerLabel(m.provider) + (m.model && m.model !== m.id ? ' · ' + esc(m.model) : '') + '</div>' +
|
|
582
|
-
modesHtml(m.input_modes) +
|
|
583
|
-
'</div>' +
|
|
584
|
-
(isActive ? '<span class="dash-active-badge">默认</span>' : '') +
|
|
585
|
-
'</div>';
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
// ── 顶部统计卡片 ──
|
|
589
|
-
var statsHtml = '<div class="grid" style="grid-template-columns:repeat(auto-fill,minmax(180px,1fr))">' +
|
|
590
|
-
'<div class="stat"><div class="label">版本</div><div class="value" style="font-size:18px">' + esc(s.version || '-') + '</div></div>' +
|
|
591
|
-
'<div class="stat"><div class="label">已注册技能</div><div class="value">' + (s.skills || 0) + '</div></div>' +
|
|
592
|
-
'<div class="stat"><div class="label">模型库</div><div class="value">' + models.length + '</div></div>' +
|
|
593
|
-
'<div class="stat"><div class="label">记忆条目</div><div class="value">' + (mem.total_count || 0) + '</div></div>' +
|
|
594
|
-
'<div class="stat"><div class="label">会话数</div><div class="value">' + (mem.session_count || 0) + '</div></div>' +
|
|
595
|
-
'<div class="stat"><div class="label">全局记忆</div><div class="value">' + (mem.global_count || 0) + '</div></div>' +
|
|
596
|
-
(q.total_submitted ? '<div class="stat"><div class="label">任务队列</div><div class="value">' + q.total_submitted + '</div></div>' : '') +
|
|
597
|
-
'</div>';
|
|
598
|
-
|
|
599
|
-
// ── 模型区域 ──
|
|
600
|
-
var modelSection = '';
|
|
601
|
-
if (models.length > 0) {
|
|
602
|
-
var enabledModels = models.filter(m => m.enabled);
|
|
603
|
-
var disabledModels = models.filter(m => !m.enabled);
|
|
604
|
-
|
|
605
|
-
modelSection = '<div class="dash-section">' +
|
|
606
|
-
'<div class="dash-section-title"><span class="icon">🤖</span> 模型配置</div>';
|
|
607
|
-
|
|
608
|
-
// 默认模型高亮展示
|
|
609
|
-
if (globalModel) {
|
|
610
|
-
modelSection += '<div style="margin-bottom:12px">' +
|
|
611
|
-
'<div style="font-size:12px;color:var(--text3);margin-bottom:8px">当前全局默认模型</div>' +
|
|
612
|
-
modelCardHtml(globalModel, true) +
|
|
613
|
-
'</div>';
|
|
614
|
-
} else {
|
|
615
|
-
// 没有在模型库中找到全局默认,用 config.llm 显示
|
|
616
|
-
modelSection += '<div style="margin-bottom:12px">' +
|
|
617
|
-
'<div style="font-size:12px;color:var(--text3);margin-bottom:8px">当前全局模型</div>' +
|
|
618
|
-
modelCardHtml({id:gl.model, name:gl.model, provider:gl.provider, model:gl.model, input_modes:gl.input_modes, enabled:true}, true) +
|
|
619
|
-
'</div>';
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// 按类型分类
|
|
623
|
-
var textModels = enabledModels.filter(m => !hasImage(m.input_modes));
|
|
624
|
-
var multiModels = enabledModels.filter(m => hasImage(m.input_modes));
|
|
625
|
-
|
|
626
|
-
// 多模态模型(支持图片的)
|
|
627
|
-
if (multiModels.length > 0) {
|
|
628
|
-
modelSection += '<div style="font-size:12px;color:var(--text3);margin-bottom:8px;margin-top:12px">多模态模型(支持图片输入)</div>' +
|
|
629
|
-
'<div class="grid" style="grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;margin-bottom:8px">' +
|
|
630
|
-
multiModels.map(m => modelCardHtml(m, false)).join('') +
|
|
631
|
-
'</div>';
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// 纯文本模型
|
|
635
|
-
if (textModels.length > 0) {
|
|
636
|
-
modelSection += '<div style="font-size:12px;color:var(--text3);margin-bottom:8px;margin-top:12px">文本模型</div>' +
|
|
637
|
-
'<div class="grid" style="grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;margin-bottom:8px">' +
|
|
638
|
-
textModels.map(m => modelCardHtml(m, false)).join('') +
|
|
639
|
-
'</div>';
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// 已禁用模型
|
|
643
|
-
if (disabledModels.length > 0) {
|
|
644
|
-
modelSection += '<div style="font-size:12px;color:var(--text3);margin-bottom:8px;margin-top:12px;opacity:.6">已禁用 (' + disabledModels.length + ')</div>' +
|
|
645
|
-
'<div class="grid" style="grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px">' +
|
|
646
|
-
disabledModels.map(m => {
|
|
647
|
-
var card = modelCardHtml(m, false);
|
|
648
|
-
return card.replace('dash-model-card"', 'dash-model-card" style="opacity:.5"');
|
|
649
|
-
}).join('') +
|
|
650
|
-
'</div>';
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
modelSection += '</div>';
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// ── 运行中的 Agent ──
|
|
657
|
-
var runningSection = '<div class="dash-section">' +
|
|
658
|
-
'<div class="dash-section-title"><span class="icon">⚡</span> 运行中的任务</div>';
|
|
659
|
-
|
|
660
|
-
if (runningAgents.length > 0) {
|
|
661
|
-
runningSection += '<div style="display:flex;flex-direction:column;gap:8px">';
|
|
662
|
-
for (var ra of runningAgents) {
|
|
663
|
-
var agentName = ra.agent_path || '未知';
|
|
664
|
-
var taskMsg = ra.message || '处理中...';
|
|
665
|
-
var timeStr = '';
|
|
666
|
-
if (ra.started_at) {
|
|
667
|
-
var elapsed = Math.round((Date.now() / 1000) - ra.started_at);
|
|
668
|
-
if (elapsed < 60) timeStr = elapsed + '秒前';
|
|
669
|
-
else if (elapsed < 3600) timeStr = Math.floor(elapsed / 60) + '分钟前';
|
|
670
|
-
else timeStr = Math.floor(elapsed / 3600) + '小时前';
|
|
671
|
-
}
|
|
672
|
-
runningSection += '<div class="dash-running-item">' +
|
|
673
|
-
'<div class="dash-running-dot"></div>' +
|
|
674
|
-
'<div style="flex:1;min-width:0">' +
|
|
675
|
-
'<div class="dash-running-agent">' + esc(agentName) + '</div>' +
|
|
676
|
-
'<div class="dash-running-task" title="' + esc(taskMsg) + '">' + esc(taskMsg) + '</div>' +
|
|
677
|
-
'</div>' +
|
|
678
|
-
(timeStr ? '<div class="dash-running-time">' + timeStr + '</div>' : '') +
|
|
679
|
-
'</div>';
|
|
680
|
-
}
|
|
681
|
-
runningSection += '</div>';
|
|
682
|
-
} else {
|
|
683
|
-
runningSection += '<div class="dash-empty">当前没有运行中的任务</div>';
|
|
684
|
-
}
|
|
685
|
-
runningSection += '</div>';
|
|
686
|
-
|
|
687
|
-
// ── 组装 ──
|
|
688
|
-
$('content').innerHTML = statsHtml + modelSection + runningSection;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// ========== Agents (FULL REWRITE) ==========
|
|
692
|
-
async function renderAgents(){
|
|
693
|
-
const [agents,models,depts]=await Promise.all([api('/api/agents'),api('/api/models'),api('/api/departments')]);
|
|
694
|
-
allAgentsCache=Array.isArray(agents)?agents:[];allModelsCache=Array.isArray(models)?models:[];allDeptsCache=Array.isArray(depts)?depts:(depts?.tree||[]);
|
|
695
|
-
const deptMap={};(function buildDeptMap(list,pfx){
|
|
696
|
-
for(const d of list||[]){const path=pfx?(pfx+'/'+d.name):d.name;deptMap[path]={name:d.name,path};buildDeptMap(d.children||[],path)}
|
|
697
|
-
})(allDeptsCache,'');
|
|
698
|
-
let html=`<div class="flex justify-between items-center mb-16 flex-wrap gap-8">
|
|
699
|
-
<div style="color:var(--text2);font-size:13px">共 ${allAgentsCache.length} 个 Agent</div>
|
|
700
|
-
<div class="flex gap-8"><input id="agentSearch" placeholder="搜索 Agent..." style="width:220px" oninput="filterAgents()">
|
|
701
|
-
<button class="btn btn-primary" onclick="openCreateAgentModal()">+ 新建 Agent</button></div></div>`;
|
|
702
|
-
html+='<div id="agentList" class="grid" style="grid-template-columns:repeat(auto-fill,minmax(340px,1fr))">';
|
|
703
|
-
for(const a of allAgentsCache){
|
|
704
|
-
html+=agentCardHtml(a,deptMap);
|
|
705
|
-
}
|
|
706
|
-
html+='</div>';
|
|
707
|
-
$('content').innerHTML=html;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function agentCardHtml(a,deptMap){
|
|
711
|
-
const isSys=!!a.system;
|
|
712
|
-
const execMode=a.execution_mode==='local';
|
|
713
|
-
const modelName=a.model_id||a.model||'默认';
|
|
714
|
-
const dept=deptMap?.[a.department]||null;
|
|
715
|
-
const emoji=a.avatar_emoji||'🤖';
|
|
716
|
-
const color=a.avatar_color||'#6366f1';
|
|
717
|
-
const avatarHtml=a.avatar_image?'<img src="'+escHtml(a.avatar_image)+'" style="width:100%;height:100%;object-fit:cover">':emoji;
|
|
718
|
-
const enabled=a.enabled!==false;
|
|
719
|
-
return `<div class="agent-card" data-name="${escHtml(a.name||'')}" data-desc="${escHtml(a.description||'')}">
|
|
720
|
-
<div class="avatar" style="background:${color}22;border:2px solid ${color};overflow:hidden">${avatarHtml}</div>
|
|
721
|
-
<div class="info">
|
|
722
|
-
<h4>${escHtml(a.name||a.path)} ${isSys?'<span class="badge badge-purple">系统</span>':''} ${!enabled?'<span class="badge badge-red">已禁用</span>':''}</h4>
|
|
723
|
-
<p>${escHtml(a.description||'无描述')}</p>
|
|
724
|
-
<div class="meta">
|
|
725
|
-
<span class="tag" title="ID">${escHtml(a.id||'-')}</span>
|
|
726
|
-
<span class="tag">${execMode?'🟢本地':'🟡沙盒'}</span>
|
|
727
|
-
<span class="tag">🤖 ${escHtml(modelName)}</span>
|
|
728
|
-
${a.session_count?`<span class="tag">💬 ${a.session_count}</span>`:''}
|
|
729
|
-
${dept?`<span class="tag">🏢 ${escHtml(dept.name)}</span>`:''}
|
|
730
|
-
${a.created_at?`<span class="tag">${fmtDate(a.created_at)}</span>`:''}
|
|
731
|
-
</div>
|
|
732
|
-
</div>
|
|
733
|
-
<div class="flex flex-col gap-8" style="flex-shrink:0">
|
|
734
|
-
<button class="btn btn-sm" style="background:var(--success);color:#fff" onclick="event.stopPropagation();chatWithAgent('${escHtml(a.path)}')">对话</button>
|
|
735
|
-
<button class="btn btn-sm btn-ghost" onclick="event.stopPropagation();showWorkdirModal('${escHtml(a.path)}')">📁 工作目录</button>
|
|
736
|
-
<button class="btn btn-sm btn-primary" onclick="event.stopPropagation();openEditAgentModal('${escHtml(a.path)}')">编辑</button>
|
|
737
|
-
${!isSys?`<button class="btn btn-sm btn-danger" onclick="event.stopPropagation();confirmDeleteAgent('${escHtml(a.path).replace(/'/g,"\\'")}','${escHtml(a.name||a.path).replace(/'/g,"\\'")}')">删除</button>`:''}
|
|
738
|
-
</div>
|
|
739
|
-
</div>`;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function filterAgents(){
|
|
743
|
-
const q=($('agentSearch')?.value||'').toLowerCase();
|
|
744
|
-
document.querySelectorAll('#agentList .agent-card').forEach(el=>{
|
|
745
|
-
const n=(el.dataset.name||'').toLowerCase();const d=(el.dataset.desc||'').toLowerCase();
|
|
746
|
-
el.style.display=(n.includes(q)||d.includes(q))?'':'none';
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// [v1.18.5] 工作目录文件浏览
|
|
751
|
-
let _workdirCurrentPath='';
|
|
752
|
-
async function showWorkdirModal(agentPath){
|
|
753
|
-
_workdirCurrentPath='';
|
|
754
|
-
const title='📁 工作目录'+(agentPath?' ('+escHtml(agentPath)+')':'');
|
|
755
|
-
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal modal-wide" onclick="event.stopPropagation()" style="max-width:600px">
|
|
756
|
-
<div class="flex justify-between items-center mb-16">
|
|
757
|
-
<h3>${title}</h3>
|
|
758
|
-
<div class="flex gap-8"><button class="btn btn-sm btn-ghost" onclick="loadWorkdirFiles('')">根目录</button><button class="btn btn-sm btn-ghost" onclick="loadWorkdirFiles(_workdirCurrentPath)">刷新</button></div>
|
|
759
|
-
</div>
|
|
760
|
-
<div id="workdirBreadcrumb" style="font-size:12px;color:var(--text3);margin-bottom:8px"></div>
|
|
761
|
-
<div id="workdirContent"><div class="empty">加载中...</div></div>
|
|
762
|
-
<div class="flex gap-8 mt-16"><button class="btn btn-ghost" onclick="closeModal()">关闭</button></div>
|
|
763
|
-
</div></div>`;
|
|
764
|
-
loadWorkdirFiles('');
|
|
765
|
-
}
|
|
766
|
-
async function loadWorkdirFiles(subPath,recursive){
|
|
767
|
-
_workdirCurrentPath=subPath||'';
|
|
768
|
-
const params=new URLSearchParams();
|
|
769
|
-
if(subPath)params.set('path',subPath);
|
|
770
|
-
if(recursive)params.set('recursive','1');
|
|
771
|
-
const files=await api('/api/workdir/files?'+params.toString());
|
|
772
|
-
const bc=document.getElementById('workdirBreadcrumb');
|
|
773
|
-
const el=document.getElementById('workdirContent');
|
|
774
|
-
if(!el)return;
|
|
775
|
-
// 面包屑导航
|
|
776
|
-
if(subPath){
|
|
777
|
-
const parts=subPath.split('/');let crumbs=['<span style="cursor:pointer" onclick="loadWorkdirFiles(\'\')">根目录</span>'];
|
|
778
|
-
let acc='';
|
|
779
|
-
for(let i=0;i<parts.length;i++){acc+=(acc?'/':'')+parts[i];crumbs.push(' / <span style="cursor:pointer" onclick="loadWorkdirFiles(\''+acc+'\')">'+escHtml(parts[i])+'</span>')}
|
|
780
|
-
if(bc)bc.innerHTML=crumbs.join('');
|
|
781
|
-
}else{if(bc)bc.innerHTML='根目录'}
|
|
782
|
-
if(!files||!files.length){el.innerHTML='<div class="empty">暂无文件</div>';return}
|
|
783
|
-
// 排序:目录在前,文件在后
|
|
784
|
-
const dirs=files.filter(f=>f.type==='dir').sort((a,b)=>a.name.localeCompare(b.name));
|
|
785
|
-
const fils=files.filter(f=>f.type==='file').sort((a,b)=>a.name.localeCompare(b.name));
|
|
786
|
-
let html='<div class="table-wrap"><table><tr><th>名称</th><th>大小</th><th></th></tr>';
|
|
787
|
-
for(const d of dirs){
|
|
788
|
-
const dp=d.path||d.name;
|
|
789
|
-
html+=`<tr style="cursor:pointer" onclick="loadWorkdirFiles('${escHtml(dp)}')"><td>📂 ${escHtml(d.name)}</td><td>-</td><td></td></tr>`;
|
|
790
|
-
}
|
|
791
|
-
for(const f of fils){
|
|
792
|
-
const fp=f.path||f.name;
|
|
793
|
-
const sizeStr=f.size>1048576?(f.size/1048576).toFixed(1)+' MB':f.size>1024?(f.size/1024).toFixed(1)+' KB':f.size+' B';
|
|
794
|
-
html+=`<tr><td style="cursor:pointer" onclick="downloadWorkdirFile('${escHtml(fp)}')">📄 ${escHtml(f.name)}</td><td>${sizeStr}</td>
|
|
795
|
-
<td><button class="btn btn-sm btn-ghost" onclick="downloadWorkdirFile('${escHtml(fp)}')">下载</button></td></tr>`;
|
|
796
|
-
}
|
|
797
|
-
html+='</table></div>';
|
|
798
|
-
el.innerHTML=html;
|
|
799
|
-
}
|
|
800
|
-
function downloadWorkdirFile(relPath){
|
|
801
|
-
const link=document.createElement('a');
|
|
802
|
-
link.href=API+'/api/workdir/download/'+encodeURIComponent(relPath);
|
|
803
|
-
link.download='';
|
|
804
|
-
document.body.appendChild(link);
|
|
805
|
-
link.click();
|
|
806
|
-
document.body.removeChild(link);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// Create Agent Modal
|
|
810
|
-
function _flattenDepts(list,pfx,result){
|
|
811
|
-
result=result||[];
|
|
812
|
-
for(const d of list||[]){const path=pfx?(pfx+'/'+d.name):d.name;result.push({name:d.name,path,emoji:d.emoji});_flattenDepts(d.children||[],path,result);}
|
|
813
|
-
return result;
|
|
814
|
-
}
|
|
815
|
-
function openCreateAgentModal(){
|
|
816
|
-
const modelOpts=allModelsCache.map(m=>`<option value="${escHtml(m.id)}">${escHtml(m.name)} (${escHtml(m.provider)})</option>`).join('');
|
|
817
|
-
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal modal-wide" onclick="event.stopPropagation()">
|
|
818
|
-
<h3>🧠 新建 Agent</h3>
|
|
819
|
-
<div class="form-row">
|
|
820
|
-
<div class="form-group"><label>名称 *</label><input id="caName" placeholder="Agent 名称"></div>
|
|
821
|
-
<div class="form-group"><label>描述</label><input id="caDesc" placeholder="Agent 描述"></div>
|
|
822
|
-
</div>
|
|
823
|
-
<div class="form-row">
|
|
824
|
-
<div class="form-group"><label>头像</label>
|
|
825
|
-
<div class="flex gap-8 items-center" style="flex-wrap:wrap">
|
|
826
|
-
<div id="caAvatarPreview" class="avatar" style="width:48px;height:48px;background:#6366f122;border:2px solid #6366f1;font-size:24px">🤖</div>
|
|
827
|
-
<div style="flex:1;min-width:200px">
|
|
828
|
-
<div class="flex gap-8 items-center">
|
|
829
|
-
<input id="caEmoji" placeholder="🤖" maxlength="4" style="width:60px" oninput="$('caAvatarPreview').textContent=this.value||'🤖'">
|
|
830
|
-
<label class="btn btn-sm" style="cursor:pointer"><input type="file" accept="image/*" hidden onchange="handleAvatarUpload(this,'ca')">
|
|
831
|
-
📷 上传图片
|
|
832
|
-
</label>
|
|
833
|
-
</div>
|
|
834
|
-
<div id="caCropArea" style="display:none;margin-top:8px">
|
|
835
|
-
<div style="position:relative;display:inline-block">
|
|
836
|
-
<img id="caCropImg" draggable="false" style="max-width:300px;max-height:200px;border:1px solid #ddd;cursor:crosshair;user-select:none;-webkit-user-drag:none" onmousedown="startCrop(event,'ca')" ontouchstart="startCropTouch(event,'ca')">
|
|
837
|
-
<div id="caCropOverlay" style="position:absolute;border:2px dashed #4f46e5;background:rgba(79,70,229,0.15);display:none;pointer-events:none"></div>
|
|
838
|
-
</div>
|
|
839
|
-
<div class="flex gap-8 mt-8">
|
|
840
|
-
<button class="btn btn-sm btn-primary" onclick="confirmAvatarCrop('ca','${name||''}')">✂️ 裁剪并使用</button>
|
|
841
|
-
<button class="btn btn-sm btn-ghost" onclick="cancelAvatarCrop('ca')">取消</button>
|
|
842
|
-
</div>
|
|
843
|
-
</div>
|
|
844
|
-
<input type="hidden" id="caAvatarImage" value="">
|
|
845
|
-
</div>
|
|
846
|
-
</div>
|
|
847
|
-
</div>
|
|
848
|
-
<div class="form-group"><label>头像颜色</label><div class="flex gap-8 items-center"><input id="caColor" value="#6366f1" type="color" style="width:48px;height:34px;padding:2px"><input id="caColorText" value="#6366f1" style="flex:1" oninput="$('caColor').value=this.value"></div></div>
|
|
849
|
-
</div>
|
|
850
|
-
<div class="form-row">
|
|
851
|
-
<div class="form-group"><label>执行模式</label><select id="caExecMode"><option value="sandbox">沙盒 (Docker)</option><option value="local">本机</option></select></div>
|
|
852
|
-
<div class="form-group"><label>绑定模型</label><select id="caModelId"><option value="">使用全局默认</option>${modelOpts}</select></div>
|
|
853
|
-
</div>
|
|
854
|
-
<div class="form-group"><label>系统提示</label><textarea id="caPrompt" rows="4" placeholder="输入 Agent 的系统提示..."></textarea></div>
|
|
855
|
-
<div class="form-row">
|
|
856
|
-
<div class="form-group"><label>工作目录</label><input id="caWorkDir" placeholder="留空使用默认"></div>
|
|
857
|
-
<div class="form-group"><label>所属部门</label><select id="caDept"><option value="">无</option>${_flattenDepts(allDeptsCache).map(d=>`<option value="${escHtml(d.path)}">${escHtml((d.emoji||'📁')+' '+d.path)}</option>`).join('')}</select></div>
|
|
858
|
-
</div>
|
|
859
|
-
<div class="flex gap-8 mt-16">
|
|
860
|
-
<button class="btn btn-primary" onclick="doCreateAgent()">创建</button>
|
|
861
|
-
<button class="btn btn-ghost" onclick="closeModal()">取消</button>
|
|
862
|
-
</div>
|
|
863
|
-
</div></div>`;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
async function doCreateAgent(){
|
|
867
|
-
const name=$('caName').value.trim();
|
|
868
|
-
if(!name){showToast('请输入名称','danger');return}
|
|
869
|
-
const r=await api('/api/agents',{method:'POST',body:JSON.stringify({
|
|
870
|
-
name,description:$('caDesc').value,avatar_emoji:$('caEmoji').value,
|
|
871
|
-
avatar_color:$('caColorText').value,execution_mode:$('caExecMode').value,
|
|
872
|
-
model_id:$('caModelId').value,system_prompt:$('caPrompt').value,
|
|
873
|
-
work_dir:$('caWorkDir').value,department:$('caDept').value,
|
|
874
|
-
avatar_image:$('caAvatarImage')?.value||''
|
|
875
|
-
})});
|
|
876
|
-
if(r.error){showToast(r.error,'danger');return}
|
|
877
|
-
closeModal();showToast('Agent 创建成功','success');renderAgents();
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// Edit Agent Modal
|
|
881
|
-
async function openEditAgentModal(path){
|
|
882
|
-
window._currentEditAgentPath=path;
|
|
883
|
-
const a=await api(`/api/agents/${encodeURIComponent(path)}`);
|
|
884
|
-
if(a.error){showToast(a.error,'danger');return}
|
|
885
|
-
const isSys=!!a.system;
|
|
886
|
-
const modelOpts=allModelsCache.map(m=>`<option value="${escHtml(m.id)}" ${a.model_id===m.id?'selected':''}>${escHtml(m.name)} (${escHtml(m.provider)})</option>`).join('');
|
|
887
|
-
const backupOpts=allModelsCache.filter(m=>m.id!==a.model_id).map(m=>`<option value="${escHtml(m.id)}" ${(a.backup_model_ids||[]).includes(m.id)?'selected':''}>${escHtml(m.name)}</option>`).join('');
|
|
888
|
-
const deptOpts=_flattenDepts(allDeptsCache).map(d=>`<option value="${escHtml(d.path)}" ${a.department===d.path||a.department===d.name?'selected':''}>${escHtml((d.emoji||'📁')+' '+d.path)}</option>`).join('');
|
|
889
|
-
|
|
890
|
-
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal modal-wide" onclick="event.stopPropagation()">
|
|
891
|
-
<h3>🧠 ${escHtml(a.name||path)} ${isSys?'<span class="badge badge-purple">系统</span>':''}</h3>
|
|
892
|
-
<div class="tabs" id="editAgentTabs">
|
|
893
|
-
<div class="tab active" onclick="agentTabSwitch(this,'atBasic')">基本信息</div>
|
|
894
|
-
<div class="tab" onclick="agentTabSwitch(this,'atSoul')">人格系统</div>
|
|
895
|
-
<div class="tab" onclick="agentTabSwitch(this,'atKB')">知识库</div>
|
|
896
|
-
<div class="tab" onclick="agentTabSwitch(this,'atSessions')">会话历史</div>
|
|
897
|
-
<div class="tab" onclick="agentTabSwitch(this,'atSettings')">设置</div>
|
|
898
|
-
<div class="tab" onclick="agentTabSwitch(this,'atPerms')">🔑 权限</div>
|
|
899
|
-
</div>
|
|
900
|
-
|
|
901
|
-
<!-- 基本信息 -->
|
|
902
|
-
<div id="atBasic">
|
|
903
|
-
<div class="form-row">
|
|
904
|
-
<div class="form-group"><label>名称</label><input id="eaName" value="${escHtml(a.name||'')}" ${isSys?'disabled':''}></div>
|
|
905
|
-
<div class="form-group"><label>描述</label><input id="eaDesc" value="${escHtml(a.description||'')}" ${isSys?'disabled':''}></div>
|
|
906
|
-
</div>
|
|
907
|
-
<div class="form-row">
|
|
908
|
-
<div class="form-group"><label>头像</label>
|
|
909
|
-
<div class="flex gap-8 items-center" style="flex-wrap:wrap">
|
|
910
|
-
<div id="eaAvatarPreview" class="avatar" style="width:48px;height:48px;background:${escHtml(a.avatar_color||'#6366f1')}22;border:2px solid ${escHtml(a.avatar_color||'#6366f1')};font-size:24px;overflow:hidden">${a.avatar_image?'<img src="'+escHtml(a.avatar_image)+'" style="width:100%;height:100%;object-fit:cover">':escHtml(a.avatar_emoji||'🤖')}</div>
|
|
911
|
-
<div style="flex:1;min-width:200px">
|
|
912
|
-
<div class="flex gap-8 items-center">
|
|
913
|
-
<input id="eaEmoji" value="${escHtml(a.avatar_emoji||'')}" style="width:60px" oninput="if(!$('eaAvatarImage').value){$('eaAvatarPreview').textContent=this.value||'🤖';$('eaAvatarPreview').innerHTML=''}" ${isSys?'disabled':''}>
|
|
914
|
-
<label class="btn btn-sm" style="cursor:pointer" ${isSys?'style=\"pointer-events:none;opacity:0.5\"':''}><input type="file" accept="image/*" hidden onchange="handleAvatarUpload(this,'ea')" ${isSys?'disabled':''}>
|
|
915
|
-
📷 上传图片
|
|
916
|
-
</label>
|
|
917
|
-
${a.avatar_image?'<button class="btn btn-sm btn-ghost" onclick="removeAvatarImage("'+escHtml(path)+'")">🗑️ 移除图片</button>':''}
|
|
918
|
-
</div>
|
|
919
|
-
<div id="eaCropArea" style="display:none;margin-top:8px">
|
|
920
|
-
<div style="position:relative;display:inline-block">
|
|
921
|
-
<img id="eaCropImg" draggable="false" style="max-width:300px;max-height:200px;border:1px solid #ddd;cursor:crosshair;user-select:none;-webkit-user-drag:none" onmousedown="startCrop(event,'ea')" ontouchstart="startCropTouch(event,'ea')">
|
|
922
|
-
<div id="eaCropOverlay" style="position:absolute;border:2px dashed #4f46e5;background:rgba(79,70,229,0.15);display:none;pointer-events:none"></div>
|
|
923
|
-
</div>
|
|
924
|
-
<div class="flex gap-8 mt-8">
|
|
925
|
-
<button class="btn btn-sm btn-primary" onclick="confirmAvatarCrop('ea','${escHtml(path)}')">✂️ 裁剪并使用</button>
|
|
926
|
-
<button class="btn btn-sm btn-ghost" onclick="cancelAvatarCrop('ea')">取消</button>
|
|
927
|
-
</div>
|
|
928
|
-
</div>
|
|
929
|
-
<input type="hidden" id="eaAvatarImage" value="${escHtml(a.avatar_image||'')}">
|
|
930
|
-
</div>
|
|
931
|
-
</div>
|
|
932
|
-
</div>
|
|
933
|
-
<div class="form-group"><label>头像颜色</label><div class="flex gap-8 items-center"><input id="eaColor" value="${escHtml(a.avatar_color||'#6366f1')}" type="color" style="width:48px;height:34px;padding:2px" ${isSys?'disabled':''}><input id="eaColorText" value="${escHtml(a.avatar_color||'#6366f1')}" style="flex:1" oninput="$('eaColor').value=this.value" ${isSys?'disabled':''}></div></div>
|
|
934
|
-
</div>
|
|
935
|
-
<div class="form-group"><label>绑定模型</label><select id="eaModelId"><option value="">使用全局默认</option>${modelOpts}</select></div>
|
|
936
|
-
<div class="form-group"><label>备用模型</label><select id="eaBackupModels" multiple style="min-height:60px">${backupOpts}</select><div style="font-size:11px;color:var(--text2);margin-top:4px">按住 Ctrl/Cmd 多选</div></div>
|
|
937
|
-
<div class="form-row">
|
|
938
|
-
<div class="form-group"><label>Agent ID <span style="color:var(--text2)">(只读)</span></label><input id="eaId" value="${escHtml(a.id||'-')}" disabled></div>
|
|
939
|
-
<div class="form-group"><label>创建时间 <span style="color:var(--text2)">(只读)</span></label><input id="eaCreated" value="${fmtDate(a.created_at)}" disabled></div>
|
|
940
|
-
</div>
|
|
941
|
-
<div class="form-group"><label>工作目录</label><input id="eaWorkDir" value="${escHtml(a.work_dir||'')}" placeholder="留空使用默认"></div>
|
|
942
|
-
<div class="form-group"><label>系统提示</label><textarea id="eaPrompt" rows="4" ${isSys?'disabled':''}></textarea></div>
|
|
943
|
-
<div class="flex gap-8 mt-16">
|
|
944
|
-
<button class="btn btn-primary" onclick="doSaveAgent('${escHtml(path)}')">保存</button>
|
|
945
|
-
<button class="btn btn-ghost" onclick="closeModal()">关闭</button>
|
|
946
|
-
</div>
|
|
947
|
-
</div>
|
|
948
|
-
|
|
949
|
-
<!-- 人格系统 -->
|
|
950
|
-
<div id="atSoul" class="hidden">
|
|
951
|
-
<div class="form-group"><label>soul.md</label><textarea id="eaSoul" rows="10" style="min-height:200px"></textarea>
|
|
952
|
-
<button class="btn btn-sm btn-primary mt-8" onclick="doSaveSoul('${escHtml(path)}')">保存 Soul</button></div>
|
|
953
|
-
<div class="form-group"><label>identity.md</label><textarea id="eaIdentity" rows="10" style="min-height:200px"></textarea>
|
|
954
|
-
<button class="btn btn-sm btn-primary mt-8" onclick="doSaveIdentity('${escHtml(path)}')">保存 Identity</button></div>
|
|
955
|
-
<div class="form-group"><label>user.md</label><textarea id="eaUser" rows="6" style="min-height:120px"></textarea>
|
|
956
|
-
<button class="btn btn-sm btn-primary mt-8" onclick="doSaveUser('${escHtml(path)}')">保存 User</button></div>
|
|
957
|
-
</div>
|
|
958
|
-
|
|
959
|
-
<!-- 知识库 -->
|
|
960
|
-
<div id="atKB" class="hidden">
|
|
961
|
-
<div id="kbContent"><div class="empty">加载中...</div></div>
|
|
962
|
-
</div>
|
|
963
|
-
|
|
964
|
-
<!-- 会话历史 -->
|
|
965
|
-
<div id="atSessions" class="hidden">
|
|
966
|
-
<div id="sessionsContent"><div class="empty">加载中...</div></div>
|
|
967
|
-
</div>
|
|
968
|
-
|
|
969
|
-
<!-- 设置 -->
|
|
970
|
-
<div id="atSettings" class="hidden">
|
|
971
|
-
<div class="form-group"><label>启用状态</label>
|
|
972
|
-
<label class="toggle"><input type="checkbox" id="eaEnabled" ${a.enabled!==false?'checked':''}><span class="slider"></span></label></div>
|
|
973
|
-
<div class="form-group"><label>部门</label><select id="eaDept"><option value="">无</option>${_flattenDepts(allDeptsCache).map(d=>`<option value="${escHtml(d.path)}" ${a.department===d.path||a.department===d.name?'selected':''}>${escHtml((d.emoji||'📁')+' '+d.path)}</option>`).join('')}</select></div>
|
|
974
|
-
<div class="flex gap-8 mt-16">
|
|
975
|
-
<button class="btn btn-primary" onclick="doSaveAgentSettings('${escHtml(path)}')">保存设置</button>
|
|
976
|
-
</div>
|
|
977
|
-
</div>
|
|
978
|
-
|
|
979
|
-
<!-- 权限 -->
|
|
980
|
-
<div id="atPerms" class="hidden">
|
|
981
|
-
<div id="atPermsContent"><div class="empty">加载中...</div></div>
|
|
982
|
-
</div>
|
|
983
|
-
</div></div>`;
|
|
984
|
-
// [v1.20.2] 安全设置 textarea 内容(避免模板字符串注入和 HTML 实体问题)
|
|
985
|
-
if($('eaPrompt'))$('eaPrompt').value=a.system_prompt||'';
|
|
986
|
-
if($('eaSoul'))$('eaSoul').value=a.soul||'';
|
|
987
|
-
if($('eaIdentity'))$('eaIdentity').value=a.identity||'';
|
|
988
|
-
if($('eaUser'))$('eaUser').value=a.user||'';
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
function agentTabSwitch(el,tabId){
|
|
992
|
-
el.parentElement.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
|
993
|
-
el.classList.add('active');
|
|
994
|
-
const allTabs=['atBasic','atSoul','atKB','atSessions','atSettings','atPerms'];
|
|
995
|
-
allTabs.forEach(id=>$(id).classList.toggle('hidden',id!==tabId));
|
|
996
|
-
// Lazy load
|
|
997
|
-
if(tabId==='atKB')loadAgentKB();
|
|
998
|
-
if(tabId==='atSessions')loadAgentSessions();
|
|
999
|
-
if(tabId==='atPerms')loadAgentPerms();
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
async function doSaveAgent(path){
|
|
1003
|
-
const r=await api(`/api/agents/${encodeURIComponent(path)}`,{method:'PUT',body:JSON.stringify({
|
|
1004
|
-
name:$('eaName').value,
|
|
1005
|
-
description:$('eaDesc').value,avatar_emoji:$('eaEmoji').value,avatar_color:$('eaColorText').value,
|
|
1006
|
-
model_id:$('eaModelId').value,
|
|
1007
|
-
backup_model_ids:Array.from($('eaBackupModels').selectedOptions).map(o=>o.value),
|
|
1008
|
-
work_dir:$('eaWorkDir').value,system_prompt:$('eaPrompt').value,
|
|
1009
|
-
avatar_image:$('eaAvatarImage')?.value||''
|
|
1010
|
-
})});
|
|
1011
|
-
if(r.error){showToast(r.error,'danger');return}
|
|
1012
|
-
showToast('已保存','success');
|
|
1013
|
-
// [v1.20.2] 如果 agent 被重命名,更新当前编辑路径,防止后续保存出现 not found
|
|
1014
|
-
if(r.renamed_to){window._currentEditAgentPath=r.renamed_to}
|
|
1015
|
-
renderAgents();
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
async function doSaveAgentSettings(path){
|
|
1019
|
-
const r=await api(`/api/agents/${encodeURIComponent(path)}`,{method:'PUT',body:JSON.stringify({
|
|
1020
|
-
enabled:$('eaEnabled').checked,department:$('eaDept').value
|
|
1021
|
-
})});
|
|
1022
|
-
if(r.error){showToast(r.error,'danger');return}
|
|
1023
|
-
showToast('设置已保存','success');
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
async function doSaveSoul(path){const r=await api(`/api/agents/${encodeURIComponent(path)}/soul`,{method:'PUT',body:JSON.stringify({soul:$('eaSoul').value})});if(r.error){showToast(r.error,'danger');return}showToast('Soul.md 已保存','success')}
|
|
1027
|
-
async function doSaveIdentity(path){const r=await api(`/api/agents/${encodeURIComponent(path)}/identity`,{method:'PUT',body:JSON.stringify({identity:$('eaIdentity').value})});if(r.error){showToast(r.error,'danger');return}showToast('Identity.md 已保存','success')}
|
|
1028
|
-
async function doSaveUser(path){const r=await api(`/api/agents/${encodeURIComponent(path)}/user`,{method:'PUT',body:JSON.stringify({user:$('eaUser').value})});if(r.error){showToast(r.error,'danger');return}showToast('User.md 已保存','success')}
|
|
1029
|
-
|
|
1030
|
-
async function loadAgentKB(){
|
|
1031
|
-
// Find current agent path from the modal title or store it
|
|
1032
|
-
const title=$('editAgentTabs')?.parentElement?.querySelector('h3')?.textContent||'';
|
|
1033
|
-
const match=title.match(/🧠\s+(.+)/);
|
|
1034
|
-
if(!match)return;
|
|
1035
|
-
// We need the agent path - store it during openEditAgentModal
|
|
1036
|
-
const path=window._currentEditAgentPath;
|
|
1037
|
-
if(!path)return;
|
|
1038
|
-
const kb=await api(`/api/agents/${encodeURIComponent(path)}/knowledge`);
|
|
1039
|
-
const files=Array.isArray(kb)?kb:(kb?.files||[]);
|
|
1040
|
-
let html=`<div class="flex justify-between items-center mb-16">
|
|
1041
|
-
<h4 style="font-size:14px;color:var(--text2)">知识库文件 (${files.length})</h4>
|
|
1042
|
-
<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>`;
|
|
1043
|
-
if(files.length===0){html+='<div class="empty">暂无知识库文件</div>';}
|
|
1044
|
-
else{html+='<div class="table-wrap"><table><tr><th>文件名</th><th>大小</th><th>操作</th></tr>';
|
|
1045
|
-
for(const f of files){html+=`<tr><td>${escHtml(f.name||f.filename||'')}</td><td>${f.size?f.size>1024?(f.size/1024).toFixed(1)+' KB':f.size+' B':'-'}</td>
|
|
1046
|
-
<td><button class="btn btn-sm btn-ghost" onclick="viewAgentKB('${escHtml(path)}','${escHtml(f.name||f.filename||'')}')">查看</button>
|
|
1047
|
-
<button class="btn btn-sm btn-danger" onclick="deleteAgentKB('${escHtml(path)}','${escHtml(f.name||f.filename||'')}')">删除</button></td></tr>`}
|
|
1048
|
-
html+='</table></div>';}
|
|
1049
|
-
$('kbContent').innerHTML=html;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
async function uploadAgentKB(path,folderMode){
|
|
1053
|
-
const input=document.createElement('input');input.type='file';input.multiple=true;
|
|
1054
|
-
if(folderMode){input.webkitdirectory=true;}
|
|
1055
|
-
input.onchange=async()=>{
|
|
1056
|
-
const fd=new FormData();
|
|
1057
|
-
for(const f of input.files){
|
|
1058
|
-
if(f.webkitRelativePath){fd.append('files',f,{headers:{'X-File-Path':f.webkitRelativePath}});}
|
|
1059
|
-
else{fd.append('files',f);}
|
|
1060
|
-
}
|
|
1061
|
-
showToast('正在上传...','info');
|
|
1062
|
-
const r=await fetch(API+`/api/agents/${encodeURIComponent(path)}/knowledge/upload`,{method:'POST',body:fd});
|
|
1063
|
-
const data=await r.json();
|
|
1064
|
-
if(data.error){showToast(data.error,'danger');return}
|
|
1065
|
-
const total=data.results?data.results.length:0;
|
|
1066
|
-
const okCount=data.results?data.results.filter(x=>x.ok).length:0;
|
|
1067
|
-
showToast(`上传完成: ${okCount}/${total} 文件成功`,okCount===total?'success':'warning');
|
|
1068
|
-
loadAgentKB(path);
|
|
1069
|
-
};input.click();
|
|
1070
|
-
}
|
|
1071
|
-
async function viewAgentKB(path,filename){
|
|
1072
|
-
const r=await fetch(API+'/api/agents/'+encodeURIComponent(path)+'/knowledge/file?path='+encodeURIComponent(filename));
|
|
1073
|
-
const ct=r.headers.get('content-type')||'';
|
|
1074
|
-
if(ct.includes('application/json')){
|
|
1075
|
-
const data=await r.json();
|
|
1076
|
-
if(data.error){showToast(data.error,'danger');return}
|
|
1077
|
-
const content=typeof data==='string'?data:(data.content||JSON.stringify(data,null,2));
|
|
1078
|
-
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:800px">
|
|
1079
|
-
<h3>${escHtml(filename)}</h3>
|
|
1080
|
-
<div class="log-viewer" style="max-height:60vh;white-space:pre-wrap">${escHtml(content.slice(0,5000))}${content.length>5000?'\n...(已截断)':''}</div>
|
|
1081
|
-
<div class="flex gap-8 mt-16"><button class="btn btn-ghost" onclick="closeModal()">关闭</button></div>
|
|
1082
|
-
</div></div>`;
|
|
1083
|
-
} else {
|
|
1084
|
-
// 二进制文件 — 下载
|
|
1085
|
-
const blob=await r.blob();
|
|
1086
|
-
const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=filename;a.click();
|
|
1087
|
-
URL.revokeObjectURL(a.href);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
async function deleteAgentKB(path,filename){
|
|
1091
|
-
if(!confirm('确认删除 '+filename+'?'))return;
|
|
1092
|
-
await api(`/api/agents/${encodeURIComponent(path)}/knowledge?path=${encodeURIComponent(filename)}`,{method:'DELETE'});
|
|
1093
|
-
showToast('已删除','success');loadAgentKB(path);
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
async function loadAgentSessions(){
|
|
1097
|
-
const path=window._currentEditAgentPath;if(!path)return;
|
|
1098
|
-
const data=await api(`/api/agents/${encodeURIComponent(path)}/sessions`);
|
|
1099
|
-
const sessions=Array.isArray(data)?data:(data?.sessions||[]);
|
|
1100
|
-
let html=`<div class="flex justify-between items-center mb-16"><h4 style="font-size:14px;color:var(--text2)">会话 (${sessions.length})</h4></div>`;
|
|
1101
|
-
if(sessions.length===0){html+='<div class="empty">暂无会话</div>';}
|
|
1102
|
-
else{html+='<div class="table-wrap"><table><tr><th>会话</th><th>消息数</th><th>最后活动</th><th></th></tr>';
|
|
1103
|
-
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>`}
|
|
1104
|
-
html+='</table></div>';}
|
|
1105
|
-
$('sessionsContent').innerHTML=html;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
async function viewSessionMsgs(sid){
|
|
1109
|
-
const msgs=await api(`/api/sessions/${encodeURIComponent(sid)}/messages?limit=100`);
|
|
1110
|
-
if(!Array.isArray(msgs)){$('sessionsContent').innerHTML='<div class="empty">加载失败</div>';return;}
|
|
1111
|
-
let html='<h4 style="margin-bottom:12px">会话消息 <span class="badge badge-blue">'+msgs.length+' 条</span></h4>';
|
|
1112
|
-
html+='<div style="max-height:500px;overflow-y:auto">';
|
|
1113
|
-
for(const m of msgs){
|
|
1114
|
-
const role=m.role||'assistant';
|
|
1115
|
-
const key=m.key||'';
|
|
1116
|
-
const content=m.content||'';
|
|
1117
|
-
const time=(m.time||'').slice(0,19);
|
|
1118
|
-
if(role==='tool'){
|
|
1119
|
-
const isResult=key==='tool_result';
|
|
1120
|
-
const isCall=key==='tool_call';
|
|
1121
|
-
const icon=isResult?'📋':(isCall?'⚙️':'🔧');
|
|
1122
|
-
const label=isResult?'工具执行结果':(isCall?'工具调用':'工具过程');
|
|
1123
|
-
const isOk=isResult&&!content.includes('失败');
|
|
1124
|
-
const badge=isResult?`<span class="badge ${isOk?'badge-green':'badge-red'}" style="margin-left:6px">${isOk?'成功':'失败'}</span>`:'';
|
|
1125
|
-
html+=`<details style="margin:6px 0;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden"><summary style="padding:8px 12px;cursor:pointer;font-size:13px;color:var(--text2);background:var(--surface2)">${icon} ${label}${badge}</summary><div style="padding:8px 12px;background:var(--surface);font-size:12px;white-space:pre-wrap;word-break:break-all;max-height:300px;overflow-y:auto;color:var(--text2)">${escHtml(content.length>1500?content.slice(0,1500)+'\\n... (共'+content.length+'字符)':content)}</div></details>`;
|
|
1126
|
-
}else{
|
|
1127
|
-
const isUser=role==='user';
|
|
1128
|
-
const bg=isUser?'var(--primary)':'var(--surface2)';
|
|
1129
|
-
const fg=isUser?'#fff':'var(--text)';
|
|
1130
|
-
const align=isUser?'text-align:right':'';
|
|
1131
|
-
const avatar=isUser?'👤':'🤖';
|
|
1132
|
-
const displayContent=content.length>500?content.slice(0,500)+'...':content;
|
|
1133
|
-
html+=`<div style="margin:8px 0;display:flex;gap:8px;${align}">`;
|
|
1134
|
-
if(!isUser)html+=`<div style="flex-shrink:0;font-size:16px;margin-top:2px">${avatar}</div>`;
|
|
1135
|
-
html+=`<div class="msg-bubble-wrap" style="min-width:0"><div style="font-size:11px;color:var(--text3);margin-bottom:2px">${isUser?'用户':'助手'} <span style="margin-left:4px">${time}</span></div><div style="background:${bg};color:${fg};padding:10px 14px;border-radius:var(--radius);font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word">${escHtml(displayContent)}</div></div>`;
|
|
1136
|
-
if(isUser)html+=`<div style="flex-shrink:0;font-size:16px;margin-top:2px">${avatar}</div>`;
|
|
1137
|
-
html+=`</div>`;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
html+='</div><div class="flex gap-8 mt-8"><button class="btn btn-ghost" onclick="loadAgentSessions()">返回</button></div>';
|
|
1141
|
-
$('sessionsContent').innerHTML=html;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
async function loadAgentPerms(){
|
|
1145
|
-
const path=window._currentEditAgentPath;if(!path)return;
|
|
1146
|
-
const [agentPerms,globalPerms,agentInfo]=await Promise.all([
|
|
1147
|
-
api('/api/permissions/'+encodeURIComponent(path)),
|
|
1148
|
-
api('/api/permissions'),
|
|
1149
|
-
api(`/api/agents/${encodeURIComponent(path)}`)
|
|
1150
|
-
]);
|
|
1151
|
-
const perms=globalPerms.all_permissions||[];
|
|
1152
|
-
const labels=globalPerms.labels||{};
|
|
1153
|
-
const defaults=globalPerms.defaults||{};
|
|
1154
|
-
const ap=agentPerms.permissions||{};
|
|
1155
|
-
const execMode=agentInfo.execution_mode||'sandbox';
|
|
1156
|
-
const isSys=!!agentInfo.system;
|
|
1157
|
-
|
|
1158
|
-
// ── 执行模式卡片 ──
|
|
1159
|
-
let html='<div class="card" style="margin-bottom:16px">';
|
|
1160
|
-
html+='<h3 style="font-size:14px;color:var(--text2);margin-bottom:8px">⚡ 执行环境</h3>';
|
|
1161
|
-
html+='<p style="color:var(--text2);font-size:12px;margin-bottom:12px">选择 Agent 代码执行的运行环境。沙盒模式隔离更安全,本机模式可使用全部本地资源。</p>';
|
|
1162
|
-
html+='<div style="display:flex;gap:12px">';
|
|
1163
|
-
html+=`<label style="flex:1;display:flex;align-items:center;gap:10px;padding:14px 16px;border-radius:var(--radius);border:2px solid ${execMode==='sandbox'?'var(--primary)':'var(--border)'};cursor:pointer;background:${execMode==='sandbox'?'var(--accent-light)':'transparent'};transition:all .15s" onclick="document.getElementById('permExecSandbox').checked=true;updateExecModeUI()">`;
|
|
1164
|
-
html+=`<input type="radio" name="permExecMode" id="permExecSandbox" value="sandbox" ${execMode==='sandbox'?'checked':''} style="display:none">`;
|
|
1165
|
-
html+=`<span style="font-size:28px">🐳</span><div><div style="font-weight:600;font-size:14px">沙盒模式</div><div style="font-size:11px;color:var(--text2);margin-top:2px">Docker 容器隔离,安全受限</div></div></label>`;
|
|
1166
|
-
html+=`<label style="flex:1;display:flex;align-items:center;gap:10px;padding:14px 16px;border-radius:var(--radius);border:2px solid ${execMode==='local'?'var(--primary)':'var(--border)'};cursor:pointer;background:${execMode==='local'?'var(--accent-light)':'transparent'};transition:all .15s" onclick="document.getElementById('permExecLocal').checked=true;updateExecModeUI()">`;
|
|
1167
|
-
html+=`<input type="radio" name="permExecMode" id="permExecLocal" value="local" ${execMode==='local'?'checked':''} style="display:none">`;
|
|
1168
|
-
html+=`<span style="font-size:28px">💻</span><div><div style="font-weight:600;font-size:14px">本机模式</div><div style="font-size:11px;color:var(--text2);margin-top:2px">直接在本机运行,完整权限</div></div></label>`;
|
|
1169
|
-
html+='</div></div>';
|
|
1170
|
-
|
|
1171
|
-
// ── 功能权限卡片 ──
|
|
1172
|
-
html+='<div class="card" style="margin-bottom:16px">';
|
|
1173
|
-
html+='<h3 style="font-size:14px;color:var(--text2);margin-bottom:8px">🔑 功能权限</h3>';
|
|
1174
|
-
html+='<p style="color:var(--text2);font-size:12px;margin-bottom:12px">精细控制 Agent 的各项能力。未设置的项目将使用全局默认值。</p>';
|
|
1175
|
-
html+='<div class="grid grid-3" style="gap:12px">';
|
|
1176
|
-
for(const p of perms){
|
|
1177
|
-
const label=labels[p]||p;
|
|
1178
|
-
const defVal=defaults[p]!==false;
|
|
1179
|
-
const curVal=ap[p]!==undefined?ap[p]:defVal;
|
|
1180
|
-
const checked=curVal?'checked':'';
|
|
1181
|
-
const isDefault=ap[p]===undefined?'<span style=\"color:var(--text3);font-size:11px;margin-left:4px\">(默认)</span>':'';
|
|
1182
|
-
html+=`<div class="form-group" style="display:flex;align-items:center;gap:10px"><label style="flex:1;font-weight:500">${label}${isDefault}</label><input type="checkbox" id="agent_perm_${p}" ${checked} style="width:18px;height:18px;cursor:pointer"></div>`;
|
|
1183
|
-
}
|
|
1184
|
-
html+='</div></div>';
|
|
1185
|
-
|
|
1186
|
-
// ── 操作按钮 ──
|
|
1187
|
-
html+='<div class="flex gap-8">';
|
|
1188
|
-
html+='<button class="btn btn-primary" onclick="saveAgentPermsFromTab()">💾 保存全部</button>';
|
|
1189
|
-
html+='<button class="btn btn-ghost" onclick="resetAgentPermsFromTab()">🔄 重置功能权限为默认</button>';
|
|
1190
|
-
html+='</div>';
|
|
1191
|
-
$('atPermsContent').innerHTML=html;
|
|
1192
|
-
}
|
|
1193
|
-
function updateExecModeUI(){
|
|
1194
|
-
const sandbox=document.getElementById('permExecSandbox');
|
|
1195
|
-
const local=document.getElementById('permExecLocal');
|
|
1196
|
-
if(!sandbox||!local)return;
|
|
1197
|
-
const isLocal=local.checked;
|
|
1198
|
-
[sandbox.parentElement,local.parentElement].forEach(el=>{
|
|
1199
|
-
const radio=el.querySelector('input[type=radio]');
|
|
1200
|
-
if(radio.checked){el.style.borderColor='var(--primary)';el.style.background='var(--accent-light)'}
|
|
1201
|
-
else{el.style.borderColor='var(--border)';el.style.background='transparent'}
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
1204
|
-
async function saveAgentPermsFromTab(){
|
|
1205
|
-
const path=window._currentEditAgentPath;if(!path)return;
|
|
1206
|
-
// 1. 保存执行模式
|
|
1207
|
-
const execSandbox=document.getElementById('permExecSandbox');
|
|
1208
|
-
const execLocal=document.getElementById('permExecLocal');
|
|
1209
|
-
const newExecMode=(execSandbox&&execSandbox.checked)?'sandbox':((execLocal&&execLocal.checked)?'local':'sandbox');
|
|
1210
|
-
const execR=await api(`/api/agents/${encodeURIComponent(path)}`,{method:'PUT',body:JSON.stringify({execution_mode:newExecMode})});
|
|
1211
|
-
if(execR.error){showToast('执行模式保存失败: '+execR.error,'danger');return}
|
|
1212
|
-
|
|
1213
|
-
// 2. 保存功能权限
|
|
1214
|
-
const [agentPerms,globalPerms]=await Promise.all([
|
|
1215
|
-
api('/api/permissions/'+encodeURIComponent(path)),
|
|
1216
|
-
api('/api/permissions')
|
|
1217
|
-
]);
|
|
1218
|
-
const perms=globalPerms.all_permissions||[];
|
|
1219
|
-
const defaults=globalPerms.defaults||{};
|
|
1220
|
-
const data={};
|
|
1221
|
-
for(const p of perms){
|
|
1222
|
-
const el=document.getElementById('agent_perm_'+p);
|
|
1223
|
-
if(!el)continue;
|
|
1224
|
-
const defVal=defaults[p]!==false;
|
|
1225
|
-
const curVal=el.checked;
|
|
1226
|
-
if(curVal!==defVal){data[p]=curVal}
|
|
1227
|
-
}
|
|
1228
|
-
const r=await api('/api/permissions/'+encodeURIComponent(path),{method:'PUT',body:JSON.stringify(data)});
|
|
1229
|
-
if(r.error){showToast('权限保存失败: '+r.error,'danger');return}
|
|
1230
|
-
showToast('权限配置已保存','success');
|
|
1231
|
-
}
|
|
1232
|
-
async function resetAgentPermsFromTab(){
|
|
1233
|
-
const path=window._currentEditAgentPath;if(!path)return;
|
|
1234
|
-
if(!confirm('确认将此 Agent 的权限重置为全局默认值?'))return;
|
|
1235
|
-
const r=await api('/api/permissions/'+encodeURIComponent(path),{method:'DELETE'});
|
|
1236
|
-
if(r.error){showToast(r.error,'danger');return}
|
|
1237
|
-
showToast('已重置为默认权限','success');
|
|
1238
|
-
loadAgentPerms();
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
function confirmDeleteAgent(path,name){
|
|
1242
|
-
showConfirm('删除 Agent','确认删除 Agent "'+escHtml(name)+'" 吗?\n\n删除后将同时清理:\n 📁 工作目录及所有文件\n 💬 该 Agent 的所有会话历史\n 🧠 相关记忆数据\n 🔗 所有子 Agent\n\n⚠️ 此操作不可撤销!',async()=>{
|
|
1243
|
-
const r=await api(`/api/agents/${encodeURIComponent(path)}`,{method:'DELETE'});
|
|
1244
|
-
if(r.error){showToast(r.error,'danger');return}
|
|
1245
|
-
closeModal();showToast('已删除','success');renderAgents();
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
// 直接以执行模式打开与指定 Agent 的对话
|
|
1250
|
-
function chatWithAgent(path){
|
|
1251
|
-
window.location.href='/ui/chat/chat_container.html?agent='+encodeURIComponent(path)+'&mode=exec';
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
// ========== Platforms ==========
|
|
1255
|
-
async function renderPlatforms(){
|
|
1256
|
-
const ps=await api('/api/platforms');
|
|
1257
|
-
if(ps.error){$('content').innerHTML='<div class="empty" style="color:var(--danger)">加载失败: '+escHtml(ps.error)+'</div>';return}
|
|
1258
|
-
const icons={telegram:'📱',discord:'🎮',feishu:'🐦',qq:'🐧',wechat:'💚',whatsapp:'💬'};
|
|
1259
|
-
let html=`<div class="flex justify-between items-center mb-16">
|
|
1260
|
-
<div style="color:var(--text2);font-size:13px">共 ${ps.length} 个平台实例</div>
|
|
1261
|
-
<button class="btn btn-primary" onclick="showAddPlatformModal()">+ 添加平台</button></div>`;
|
|
1262
|
-
if(!ps.length){html+='<div class="empty">暂无聊天平台配置,点击上方按钮添加</div>';$('content').innerHTML=html;return}
|
|
1263
|
-
html+='<div class="grid">';
|
|
1264
|
-
for(const p of ps){
|
|
1265
|
-
const pid=p.id||p.platform;
|
|
1266
|
-
const displayName=p.display_name||(icons[p.platform]||'📡')+' '+p.platform;
|
|
1267
|
-
const bindInfo=p.bind_agent?'绑定: '+escHtml(p.bind_agent):(p.bind_agents&&p.bind_agents.length?'绑定: '+p.bind_agents.map(a=>escHtml(a)).join(', '):'');
|
|
1268
|
-
const _supportsQR=['telegram','wechat','whatsapp'].includes(p.platform);
|
|
1269
|
-
html+=`<div class="card"><div class="flex justify-between items-center">
|
|
1270
|
-
<h3 style="color:var(--text)">${icons[p.platform]||'📡'} ${escHtml(p.display_name||p.platform)}</h3>
|
|
1271
|
-
<span class="badge ${p.enabled?'badge-green':'badge-red'}">${p.enabled?'已启用':'未启用'}</span>
|
|
1272
|
-
</div><p style="font-size:13px;color:var(--text2);margin-top:8px">Token: ${p.token?'已配置':'未配置'} ${p.app_id?'· App ID: '+escHtml(p.app_id):''}</p>
|
|
1273
|
-
${bindInfo?'<p style="font-size:12px;color:var(--text3);margin-top:4px">'+bindInfo+'</p>':''}
|
|
1274
|
-
<div class="mt-8 flex gap-8">
|
|
1275
|
-
<button class="btn btn-sm ${p.enabled?'btn-danger':'btn-success'}" onclick="togglePlatform('${escHtml(pid)}',${!p.enabled})">${p.enabled?'停用':'启用'}</button>
|
|
1276
|
-
<button class="btn btn-sm btn-ghost" onclick="showEditPlatformModal('${escHtml(pid)}')">配置</button>
|
|
1277
|
-
${_supportsQR?'<button class="btn btn-sm btn-primary" onclick="showPlatformQRModal(\''+escHtml(pid)+'\')">📱 QR绑定</button>':''}
|
|
1278
|
-
</div></div>`;
|
|
1279
|
-
}
|
|
1280
|
-
html+='</div>';$('content').innerHTML=html;
|
|
1281
|
-
}
|
|
1282
|
-
async function togglePlatform(id,enable){
|
|
1283
|
-
const r=await api(`/api/platforms/${id}`,{method:'PUT',body:JSON.stringify({enabled})});
|
|
1284
|
-
if(r.error){showToast('操作失败: '+r.error,'danger');return}
|
|
1285
|
-
showToast(enable?'平台已启用':'平台已停用',enable?'success':'info');
|
|
1286
|
-
renderPlatforms();
|
|
1287
|
-
}
|
|
1288
|
-
function showAddPlatformModal(){
|
|
1289
|
-
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:520px">
|
|
1290
|
-
<h3>+ 添加聊天平台</h3>
|
|
1291
|
-
<div class="form-group"><label>平台类型</label><select id="apType" onchange="onPlatformTypeChange()">
|
|
1292
|
-
<option value="telegram">Telegram</option><option value="discord">Discord</option>
|
|
1293
|
-
<option value="feishu">飞书</option><option value="qq">QQ</option><option value="wechat">微信</option>
|
|
1294
|
-
<option value="whatsapp">WhatsApp</option>
|
|
1295
|
-
</select></div>
|
|
1296
|
-
<div class="form-group"><label>Token / Access Token</label><input id="apToken" placeholder="Bot Token 或 Access Token"></div>
|
|
1297
|
-
<div id="apAppIdGroup" class="form-group"><label>App ID</label><input id="apAppId" placeholder="应用ID (可选)"></div>
|
|
1298
|
-
<div id="apWebhookGroup" class="form-group"><label>Webhook URL</label><input id="apWebhook" placeholder="Webhook 地址 (可选)"></div>
|
|
1299
|
-
<div class="form-group"><label>绑定 Agent</label>
|
|
1300
|
-
<div class="flex gap-8">
|
|
1301
|
-
<input id="apBindAgent" placeholder="手输 Agent ID (可选)" style="flex:1">
|
|
1302
|
-
<button class="btn btn-sm btn-ghost" onclick="showAgentSelectorForPlatform('apBindAgent','apBindAgents')">选择 Agent</button>
|
|
1303
|
-
</div>
|
|
1304
|
-
<div id="apBindAgents" style="font-size:11px;color:var(--text3);margin-top:4px"></div>
|
|
1305
|
-
<div class="hint">手输 Agent ID 或点击"选择 Agent"从部门中选择。支持绑定多个 Agent。</div>
|
|
1306
|
-
</div>
|
|
1307
|
-
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="doAddPlatform()">添加</button><button class="btn btn-ghost" onclick="closeModal()">取消</button></div>
|
|
1308
|
-
</div></div>`;
|
|
1309
|
-
}
|
|
1310
|
-
function onPlatformTypeChange(){
|
|
1311
|
-
var t=$('apType').value;
|
|
1312
|
-
var showAppId=(t==='feishu'||t==='whatsapp');
|
|
1313
|
-
var showWebhook=(t==='whatsapp');
|
|
1314
|
-
if($('apAppIdGroup'))$('apAppIdGroup').style.display=showAppId?'':'none';
|
|
1315
|
-
if($('apWebhookGroup'))$('apWebhookGroup').style.display=showWebhook?'':'none';
|
|
1316
|
-
}
|
|
1317
|
-
async function doAddPlatform(){
|
|
1318
|
-
var bindAgent=$('apBindAgent').value.trim();
|
|
1319
|
-
var bindAgents=[];
|
|
1320
|
-
var selectedEls=document.querySelectorAll('#apBindAgents .selected-agent-tag');
|
|
1321
|
-
selectedEls.forEach(function(el){var v=el.getAttribute('data-agent');if(v)bindAgents.push(v);});
|
|
1322
|
-
if(bindAgent)bindAgents.unshift(bindAgent);
|
|
1323
|
-
bindAgents=[...new Set(bindAgents)];
|
|
1324
|
-
const r=await api('/api/platforms',{method:'POST',body:JSON.stringify({platform:$('apType').value,token:$('apToken').value,app_id:$('apAppId').value,webhook_url:$('apWebhook').value,bind_agent:bindAgents[0]||'',bind_agents:bindAgents})});
|
|
1325
|
-
if(r.error){showToast(r.error,'danger');return}closeModal();showToast('已添加: '+(r.display_name||r.platform),'success');renderPlatforms();
|
|
1326
|
-
}
|
|
1327
|
-
async function showEditPlatformModal(id){
|
|
1328
|
-
const p=await api(`/api/platforms/${id}`);
|
|
1329
|
-
if(p.error){showToast(p.error,'danger');return}
|
|
1330
|
-
const pid=p.id||id;
|
|
1331
|
-
var bindAgentVal=p.bind_agent||'';
|
|
1332
|
-
var bindAgentsArr=p.bind_agents||[];
|
|
1333
|
-
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:520px">
|
|
1334
|
-
<h3>配置 ${escHtml(p.display_name||p.platform)}</h3>
|
|
1335
|
-
<div class="form-group"><label>显示名称</label><input id="epDisplayName" value="${escHtml(p.display_name||'')}" placeholder="留空自动生成"></div>
|
|
1336
|
-
<div class="form-group"><label>Token / Access Token</label><input id="epToken" value="${escHtml(p.token||'')}" placeholder="Bot Token"></div>
|
|
1337
|
-
<div id="epAppIdGroup" class="form-group"><label>App ID</label><input id="epAppId" value="${escHtml(p.app_id||'')}" placeholder="应用ID"></div>
|
|
1338
|
-
<div class="form-group"><label>App Secret</label><input id="epSecret" type="password" placeholder="${p.has_secret?'已配置(留空不修改)':'未配置'}"></div>
|
|
1339
|
-
<div id="epWebhookGroup" class="form-group"><label>Webhook URL</label><input id="epWebhook" value="${escHtml(p.webhook_url||'')}" placeholder="Webhook 地址"></div>
|
|
1340
|
-
<div class="form-group"><label>允许用户 (逗号分隔)</label><input id="epUsers" value="${escHtml((p.allowed_users||[]).join(','))}" placeholder="user1,user2"></div>
|
|
1341
|
-
<div class="form-group"><label>绑定 Agent</label>
|
|
1342
|
-
<div class="flex gap-8">
|
|
1343
|
-
<input id="epBindAgent" value="${escHtml(bindAgentVal)}" placeholder="手输 Agent ID (可选)" style="flex:1">
|
|
1344
|
-
<button class="btn btn-sm btn-ghost" onclick="showAgentSelectorForPlatform('epBindAgent','epBindAgents')">选择 Agent</button>
|
|
1345
|
-
</div>
|
|
1346
|
-
<div id="epBindAgents" style="font-size:11px;color:var(--text3);margin-top:4px">${bindAgentsArr.map(function(a){return '<span class="selected-agent-tag" data-agent="'+escHtml(a)+'" style="display:inline-block;background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:1px 6px;margin:2px;font-size:11px;cursor:pointer" onclick="this.remove()" title="点击移除">'+escHtml(a)+' ×</span>';}).join('')}</div>
|
|
1347
|
-
<div class="hint">手输 Agent ID 或点击"选择 Agent"从部门中选择。一个平台可以绑定多个 Agent。</div>
|
|
1348
|
-
</div>
|
|
1349
|
-
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="doSavePlatform('${escHtml(pid)}')">保存</button><button class="btn btn-danger" onclick="doDeletePlatform('${escHtml(pid)}')">删除平台</button><button class="btn btn-ghost" onclick="closeModal()">关闭</button></div>
|
|
1350
|
-
</div></div>`;
|
|
1351
|
-
}
|
|
1352
|
-
async function doSavePlatform(id){
|
|
1353
|
-
var bindAgent=$('epBindAgent').value.trim();
|
|
1354
|
-
var bindAgents=[];
|
|
1355
|
-
var selectedEls=document.querySelectorAll('#epBindAgents .selected-agent-tag');
|
|
1356
|
-
selectedEls.forEach(function(el){var v=el.getAttribute('data-agent');if(v)bindAgents.push(v);});
|
|
1357
|
-
if(bindAgent)bindAgents.unshift(bindAgent);
|
|
1358
|
-
bindAgents=[...new Set(bindAgents)];
|
|
1359
|
-
const r=await api(`/api/platforms/${id}`,{method:'PUT',body:JSON.stringify({display_name:$('epDisplayName').value,token:$('epToken').value,app_id:$('epAppId').value,app_secret:$('epSecret').value||undefined,webhook_url:$('epWebhook').value,allowed_users:$('epUsers').value.split(',').map(s=>s.trim()).filter(Boolean),bind_agent:bindAgents[0]||'',bind_agents:bindAgents})});
|
|
1360
|
-
if(r.error){showToast(r.error,'danger');return}closeModal();showToast('已保存','success');renderPlatforms();
|
|
1361
|
-
}
|
|
1362
|
-
async function doDeletePlatform(id){
|
|
1363
|
-
showConfirm('删除平台','确认删除该平台实例吗?',async()=>{
|
|
1364
|
-
const r=await api(`/api/platforms/${id}`,{method:'DELETE'});if(r.error){showToast(r.error,'danger');closeModal();return}closeModal();showToast('已删除','success');renderPlatforms();
|
|
1365
|
-
});
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
// [v1.20.3] QR 码绑定模态框
|
|
1369
|
-
let _qrPollTimer=null;
|
|
1370
|
-
async function showPlatformQRModal(platformId){
|
|
1371
|
-
if(_qrPollTimer){clearInterval(_qrPollTimer);_qrPollTimer=null}
|
|
1372
|
-
$('modalContainer').innerHTML='<div class="modal-overlay" onclick="closeModal();if(_qrPollTimer){clearInterval(_qrPollTimer);_qrPollTimer=null}"><div class="modal" onclick="event.stopPropagation()" style="max-width:420px;text-align:center"><h3>📱 QR 码绑定</h3><p style="font-size:13px;color:var(--text2);margin:8px 0">使用手机扫描下方二维码以绑定账号</p><div id="qrStatus" style="margin:12px 0;padding:12px;border-radius:var(--radius-sm);background:var(--bg3);font-size:13px;color:var(--text2)">正在请求 QR 码...</div><div id="qrCodeContainer" style="display:flex;justify-content:center;padding:16px 0"><div id="qrCodeImg" style="width:256px;height:256px;border:1px solid var(--bg4);border-radius:8px;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:13px">加载中...</div></div><div id="qrActions" style="display:none;margin-top:12px"><button class="btn btn-sm btn-ghost" onclick="requestNewQR(\''+escHtml(platformId)+'\')">🔄 刷新 QR 码</button></div><div class="flex gap-8 mt-16"><button class="btn btn-ghost" onclick="closeModal();if(_qrPollTimer){clearInterval(_qrPollTimer);_qrPollTimer=null}">关闭</button></div></div></div>';
|
|
1373
|
-
var r=await api('/api/platforms/'+encodeURIComponent(platformId)+'/qr',{method:'POST'});
|
|
1374
|
-
if(r.error){$('qrStatus').textContent='错误: '+escHtml(r.error);return}
|
|
1375
|
-
if(r.qr_code){
|
|
1376
|
-
$('qrCodeImg').innerHTML='<img src="data:image/png;base64,'+r.qr_code+'" style="width:256px;height:256px;border-radius:8px" alt="QR Code">';
|
|
1377
|
-
$('qrStatus').innerHTML='<span style="color:var(--warn)">⏳ 等待扫码...</span>';
|
|
1378
|
-
$('qrActions').style.display='block';
|
|
1379
|
-
}else if(r.status==='connected'){
|
|
1380
|
-
$('qrStatus').innerHTML='<span style="color:var(--success)">✅ 已连接</span>';
|
|
1381
|
-
$('qrCodeImg').innerHTML='<div style="color:var(--success);font-size:24px;padding:40px">✅<br><span style="font-size:14px">已成功连接</span></div>';
|
|
1382
|
-
}else{$('qrStatus').textContent=r.error||'QR 码生成失败,请检查平台配置';}
|
|
1383
|
-
_qrPollTimer=setInterval(async()=>{
|
|
1384
|
-
try{
|
|
1385
|
-
var s=await api('/api/platforms/'+encodeURIComponent(platformId)+'/qr');
|
|
1386
|
-
if(s.status==='connected'){
|
|
1387
|
-
$('qrStatus').innerHTML='<span style="color:var(--success)">✅ 已连接</span>';
|
|
1388
|
-
$('qrCodeImg').innerHTML='<div style="color:var(--success);font-size:24px;padding:40px">✅<br><span style="font-size:14px">已成功连接</span></div>';
|
|
1389
|
-
$('qrActions').style.display='none';
|
|
1390
|
-
clearInterval(_qrPollTimer);_qrPollTimer=null;
|
|
1391
|
-
showToast('QR 绑定成功!','success');renderPlatforms();
|
|
1392
|
-
}else if(s.status==='waiting_scan'&&s.qr_code){
|
|
1393
|
-
$('qrCodeImg').innerHTML='<img src="data:image/png;base64,'+s.qr_code+'" style="width:256px;height:256px;border-radius:8px" alt="QR Code">';
|
|
1394
|
-
$('qrStatus').innerHTML='<span style="color:var(--warn)">⏳ 等待扫码...</span>';
|
|
1395
|
-
$('qrActions').style.display='block';
|
|
1396
|
-
}else if(s.status==='disconnected'){
|
|
1397
|
-
$('qrStatus').innerHTML='<span style="color:var(--danger)">❌ 已断开</span>';
|
|
1398
|
-
$('qrActions').style.display='block';
|
|
1399
|
-
}
|
|
1400
|
-
}catch(e){}
|
|
1401
|
-
},3000);
|
|
1402
|
-
}
|
|
1403
|
-
async function requestNewQR(platformId){
|
|
1404
|
-
$('qrStatus').textContent='正在刷新 QR 码...';
|
|
1405
|
-
$('qrCodeImg').innerHTML='加载中...';
|
|
1406
|
-
var r=await api('/api/platforms/'+encodeURIComponent(platformId)+'/qr',{method:'POST'});
|
|
1407
|
-
if(r.error){$('qrStatus').textContent='错误: '+escHtml(r.error);return}
|
|
1408
|
-
if(r.qr_code){
|
|
1409
|
-
$('qrCodeImg').innerHTML='<img src="data:image/png;base64,'+r.qr_code+'" style="width:256px;height:256px;border-radius:8px" alt="QR Code">';
|
|
1410
|
-
$('qrStatus').innerHTML='<span style="color:var(--warn)">⏳ 等待扫码...</span>';
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// Agent 选择器(从部门树选择 Agent)
|
|
1415
|
-
async function showAgentSelectorForPlatform(inputId,containerId){
|
|
1416
|
-
try{
|
|
1417
|
-
var agents=await api('/api/agents');
|
|
1418
|
-
if(!Array.isArray(agents)){showToast('加载 Agent 列表失败','danger');return}
|
|
1419
|
-
var agentList=agents.filter(function(a){return!a.system&&a.path!=='default'});
|
|
1420
|
-
var deptMap={};
|
|
1421
|
-
agentList.forEach(function(a){
|
|
1422
|
-
var dept=a.department||'未分组';
|
|
1423
|
-
if(!deptMap[dept])deptMap[dept]=[];
|
|
1424
|
-
deptMap[dept].push(a);
|
|
1425
|
-
});
|
|
1426
|
-
var html='<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:480px;max-height:70vh;overflow-y:auto">';
|
|
1427
|
-
html+='<h3>选择 Agent</h3>';
|
|
1428
|
-
for(var dept in deptMap){
|
|
1429
|
-
html+='<div style="margin-bottom:12px"><div style="font-size:13px;font-weight:600;color:var(--text2);margin-bottom:4px">📁 '+escHtml(dept)+'</div>';
|
|
1430
|
-
deptMap[dept].forEach(function(a){
|
|
1431
|
-
var selected=false;
|
|
1432
|
-
var existing=document.querySelectorAll('#'+containerId+' .selected-agent-tag');
|
|
1433
|
-
existing.forEach(function(el){if(el.getAttribute('data-agent')===a.path)selected=true;});
|
|
1434
|
-
html+='<label style="display:flex;align-items:center;gap:8px;padding:4px 0;cursor:pointer;font-size:13px"><input type="checkbox" value="'+escHtml(a.path)+'" '+(selected?'checked':'')+' onchange="toggleAgentSelection(this,\''+inputId+'\',\''+containerId+'\')"> '+(a.avatar_emoji||'🤖')+' '+escHtml(a.name)+' <span style="color:var(--text3);font-size:11px">('+escHtml(a.path)+')</span></label>';
|
|
1435
|
-
});
|
|
1436
|
-
html+='</div>';
|
|
1437
|
-
}
|
|
1438
|
-
html+='<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="closeModal()">确定</button></div></div></div>';
|
|
1439
|
-
$('modalContainer').innerHTML=html;
|
|
1440
|
-
}catch(e){showToast('加载失败: '+e.message,'danger')}
|
|
1441
|
-
}
|
|
1442
|
-
function toggleAgentSelection(cb,inputId,containerId){
|
|
1443
|
-
var agentPath=cb.value;
|
|
1444
|
-
var container=document.getElementById(containerId);
|
|
1445
|
-
if(cb.checked){
|
|
1446
|
-
// 添加 tag
|
|
1447
|
-
if(!container.querySelector('[data-agent="'+agentPath+'"]')){
|
|
1448
|
-
var tag=document.createElement('span');
|
|
1449
|
-
tag.className='selected-agent-tag';
|
|
1450
|
-
tag.setAttribute('data-agent',agentPath);
|
|
1451
|
-
tag.style.cssText='display:inline-block;background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:1px 6px;margin:2px;font-size:11px;cursor:pointer';
|
|
1452
|
-
tag.title='点击移除';
|
|
1453
|
-
tag.textContent=agentPath+' ×';
|
|
1454
|
-
tag.onclick=function(){this.remove();};
|
|
1455
|
-
container.appendChild(tag);
|
|
1456
|
-
}
|
|
1457
|
-
// 同步到输入框
|
|
1458
|
-
var first=container.querySelector('.selected-agent-tag');
|
|
1459
|
-
if(first)document.getElementById(inputId).value=first.getAttribute('data-agent');
|
|
1460
|
-
}else{
|
|
1461
|
-
var existing=container.querySelector('[data-agent="'+agentPath+'"]');
|
|
1462
|
-
if(existing)existing.remove();
|
|
1463
|
-
var first=container.querySelector('.selected-agent-tag');
|
|
1464
|
-
document.getElementById(inputId).value=first?first.getAttribute('data-agent'):'';
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// ========== Sessions ==========
|
|
1469
|
-
async function renderSessions(){
|
|
1470
|
-
const ss=await api('/api/sessions');
|
|
1471
|
-
let html='<div class="table-wrap"><table><tr><th>会话</th><th>Agent</th><th>消息数</th><th>最后活动</th><th>操作</th></tr>';
|
|
1472
|
-
for(const s of (ss||[])){
|
|
1473
|
-
// 从 session_id 提取 agent 名 (格式: agent_web_timestamp)
|
|
1474
|
-
const parts=(s.id||'').split('_web_');
|
|
1475
|
-
const agentName=parts[0]||'default';
|
|
1476
|
-
const displayName=s.display_name||s.id;
|
|
1477
|
-
html+=`<tr><td title="${escHtml(s.id)}">${escHtml(displayName.length>30?displayName.slice(0,30)+'...':displayName)}</td><td>${escHtml(agentName)}</td><td>${s.messages}</td><td>${s.last?.slice(0,19)||''}</td>
|
|
1478
|
-
<td><button class="btn btn-sm" style="background:var(--success);color:#fff" onclick="enterSession('${escHtml(s.id)}','${escHtml(agentName)}')">切入</button>
|
|
1479
|
-
<button class="btn btn-sm btn-ghost" onclick="viewSession('${s.id}')">查看</button>
|
|
1480
|
-
<button class="btn btn-sm btn-danger" onclick="clearSession('${s.id}')">清除</button></td></tr>`;}
|
|
1481
|
-
html+='</table></div>';if(!ss||!ss.length)html='<div class="empty">暂无会话</div>';
|
|
1482
|
-
$('content').innerHTML=html;
|
|
1483
|
-
}
|
|
1484
|
-
async function viewSession(sid){
|
|
1485
|
-
window._viewSessionSid=sid;window._viewSessionOffset=0;
|
|
1486
|
-
_navSubState='view:'+sid;
|
|
1487
|
-
navigateTo('sessions','view:'+sid,_loadSessionMessages);
|
|
1488
|
-
}
|
|
1489
|
-
async function _loadSessionMessages(){
|
|
1490
|
-
const sid=window._viewSessionSid;if(!sid)return;
|
|
1491
|
-
const offset=window._viewSessionOffset||0;
|
|
1492
|
-
const msgs=await api(`/api/sessions/${encodeURIComponent(sid)}/messages?limit=100&offset=${offset}`);
|
|
1493
|
-
if(!Array.isArray(msgs)){showToast('加载失败','danger');return}
|
|
1494
|
-
const hasMore=msgs.length>=100;
|
|
1495
|
-
let html='<h3 style="margin-bottom:12px">会话: '+escHtml(sid)+' <span class="badge badge-blue">'+msgs.length+' 条</span></h3>';
|
|
1496
|
-
html+='<div style="max-height:600px;overflow-y:auto">';
|
|
1497
|
-
for(let i=0;i<msgs.length;i++){
|
|
1498
|
-
const m=msgs[i];
|
|
1499
|
-
const mid='msg_'+i;
|
|
1500
|
-
const role=m.role||'assistant';
|
|
1501
|
-
const key=m.key||'';
|
|
1502
|
-
if(role==='tool'){
|
|
1503
|
-
const isResult=key==='tool_result';
|
|
1504
|
-
const isCall=key==='tool_call';
|
|
1505
|
-
const icon=isResult?'📋':(isCall?'⚙️':'🔧');
|
|
1506
|
-
const label=isResult?'工具执行结果':(isCall?'工具调用':'工具过程');
|
|
1507
|
-
const isOk=isResult&&!((m.content||'').includes('失败'));
|
|
1508
|
-
const badge=isResult?`<span class="badge ${isOk?'badge-green':'badge-red'}" style="margin-left:6px">${isOk?'成功':'失败'}</span>`:'';
|
|
1509
|
-
const tc=(m.content||'');
|
|
1510
|
-
const tcTrunc=tc.length>800;
|
|
1511
|
-
html+=`<details style="margin:4px 0;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden" ${tcTrunc?'':'open'}><summary style="padding:8px 12px;cursor:pointer;font-size:13px;color:var(--text2);background:var(--surface2)">${icon} ${label}${badge}</summary><div style="padding:8px 12px;background:var(--surface);font-size:12px;white-space:pre-wrap;word-break:break-all;color:var(--text2)">${escHtml(tcTrunc?tc.slice(0,800)+'... (共'+tc.length+'字符)':'')}</div></details>`;
|
|
1512
|
-
} else {
|
|
1513
|
-
const isUser=role==='user';
|
|
1514
|
-
const bg=isUser?'var(--primary)':'var(--surface2)';
|
|
1515
|
-
const fg=isUser?'#fff':'var(--text)';
|
|
1516
|
-
const avatar=isUser?'👤':'🤖';
|
|
1517
|
-
const time=(m.time||'').slice(0,19);
|
|
1518
|
-
const content=(m.content||'');
|
|
1519
|
-
const long=content.length>500;
|
|
1520
|
-
const displayContent=long?content.slice(0,500)+'... (共'+content.length+'字符)':content;
|
|
1521
|
-
html+=`<div style="margin:8px 0;display:flex;gap:8px;${isUser?'flex-direction:row-reverse':''}">`;
|
|
1522
|
-
html+=`<div style="flex-shrink:0;font-size:16px;margin-top:2px">${avatar}</div>`;
|
|
1523
|
-
html+=`<div class="msg-bubble-wrap" style="min-width:0"><div style="font-size:11px;color:var(--text3);margin-bottom:2px;${isUser?'text-align:right':''}">${isUser?'用户':'助手'} <span style="margin-left:4px">${time}</span></div>`;
|
|
1524
|
-
html+=`<div style="background:${bg};color:${fg};padding:10px 14px;border-radius:var(--radius);font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word">${escHtml(displayContent)}</div>`;
|
|
1525
|
-
if(long)html+=`<button class="btn btn-sm btn-ghost" style="margin-top:4px;font-size:11px" onclick="this.previousElementSibling.textContent=this.dataset.full;this.remove()" data-full="${escHtml(content).replace(/"/g,'"')}">展开全部 (${content.length}字符)</button>`;
|
|
1526
|
-
html+=`</div></div>`;
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
html+='</div>';
|
|
1530
|
-
if(hasMore)html+=`<button class="btn btn-ghost mt-8" onclick="window._viewSessionOffset=${offset+100};_loadSessionMessages()">加载更多...</button>`;
|
|
1531
|
-
html+='<div class="flex gap-8 mt-8"><button class="btn btn-ghost" onclick="goBack()">返回</button>';
|
|
1532
|
-
html+=`<button class="btn" style="background:var(--accent);color:#fff" onclick="viewSessionRaw('${escHtml(sid)}')">Raw 原始消息</button>`;
|
|
1533
|
-
html+=`<button class="btn btn-primary" onclick="enterSession('${escHtml(sid)}','${escHtml(sid.split('_web_')[0]||'default')}')">在聊天中查看完整记录</button></div>`;
|
|
1534
|
-
$('content').innerHTML=html;
|
|
1535
|
-
}
|
|
1536
|
-
// ========== Raw 原始消息查看 ==========
|
|
1537
|
-
async function viewSessionRaw(sid){
|
|
1538
|
-
window._viewSessionSid=sid;
|
|
1539
|
-
_navSubState='raw:'+sid;
|
|
1540
|
-
// 不用 navigateTo 因为需要先 fetch 数据再渲染,直接记录历史后继续
|
|
1541
|
-
_navHistory.push({page:currentPage,sub:window._navSubState});
|
|
1542
|
-
if(_navHistory.length>6)_navHistory.shift();
|
|
1543
|
-
var hash='sessions'+'~raw:'+sid;
|
|
1544
|
-
history.pushState({page:'sessions',sub:'raw:'+sid},'','#'+hash);
|
|
1545
|
-
|
|
1546
|
-
const msgs=await api(`/api/session/raw?sid=${encodeURIComponent(sid)}&limit=5000`);
|
|
1547
|
-
if(!Array.isArray(msgs)){showToast('加载失败','danger');return}
|
|
1548
|
-
// 按时间分组(同秒内合并)
|
|
1549
|
-
const keyLabelMap={'llm_output':'LLM 输出','llm_input':'LLM 输入','tool_call':'工具调用','tool_result':'工具结果','tool_result_raw':'工具原始数据','reasoning':'推理过程','conversation_insight':'会话洞察','':'对话','llm_request':'LLM 请求'};
|
|
1550
|
-
let html=`<h3 style="margin-bottom:12px">Raw: ${escHtml(sid)} <span class="badge badge-blue">${msgs.length} 条</span></h3>`;
|
|
1551
|
-
// 筛选按钮
|
|
1552
|
-
html+=`<div style="margin-bottom:10px;display:flex;gap:6px;flex-wrap:wrap" id="rawFilterBar">`;
|
|
1553
|
-
html+=`<button class="btn btn-sm" style="background:var(--accent);color:#fff" data-filter="all" onclick="rawFilter('all',this)">全部</button>`;
|
|
1554
|
-
// 收集所有 key 类型
|
|
1555
|
-
const keys=[...new Set(msgs.map(m=>m.key||''))];
|
|
1556
|
-
for(const k of keys){
|
|
1557
|
-
const label=keyLabelMap[k]||k||'对话';
|
|
1558
|
-
const count=msgs.filter(m=>(m.key||'')===k).length;
|
|
1559
|
-
html+=`<button class="btn btn-sm btn-ghost" data-filter="${escHtml(k)}" onclick="rawFilter('${escHtml(k)}',this)">${escHtml(label)} (${count})</button>`;
|
|
1560
|
-
}
|
|
1561
|
-
html+=`</div>`;
|
|
1562
|
-
// 时间索引导航
|
|
1563
|
-
html+=`<div class="raw-time-nav" id="rawTimeNav"></div>`;
|
|
1564
|
-
// 消息列表
|
|
1565
|
-
html+=`<div style="max-height:65vh;overflow-y:auto;font-family:monospace" id="rawMsgList">`;
|
|
1566
|
-
for(let i=0;i<msgs.length;i++){
|
|
1567
|
-
const m=msgs[i];
|
|
1568
|
-
const role=m.role||'?';
|
|
1569
|
-
const key=m.key||'';
|
|
1570
|
-
const time=(m.time||'').slice(0,19);
|
|
1571
|
-
const content=(m.content||'');
|
|
1572
|
-
const keyLabel=keyLabelMap[key]||key;
|
|
1573
|
-
// 颜色标识
|
|
1574
|
-
let borderColor='var(--border)';
|
|
1575
|
-
let bgColor='var(--surface)';
|
|
1576
|
-
if(key==='llm_input'){borderColor='#06b6d4';bgColor='#001a1f'}
|
|
1577
|
-
else if(key==='llm_output'){borderColor='#e6a817';bgColor='#1a1700'}
|
|
1578
|
-
else if(key==='tool_call'){borderColor='#3b82f6';bgColor='#001029'}
|
|
1579
|
-
else if(key==='tool_result'){borderColor='#22c55e';bgColor='#001a0d'}
|
|
1580
|
-
else if(key==='tool_result_raw'){borderColor='#10b981';bgColor='#001510'}
|
|
1581
|
-
else if(key==='reasoning'){borderColor='#a855f7';bgColor='#0d0020'}
|
|
1582
|
-
else if(role==='user'){borderColor='var(--primary)';bgColor='var(--surface)'}
|
|
1583
|
-
else if(role==='assistant'){borderColor='#6b7280';bgColor='var(--surface2)'}
|
|
1584
|
-
const mid='raw_'+i;
|
|
1585
|
-
html+=`<div class="raw-item" data-key="${escHtml(key)}" data-time="${escHtml(time)}" style="margin:2px 0;border-left:3px solid ${borderColor};background:${bgColor};border-radius:0 4px 4px 0;overflow:hidden">`;
|
|
1586
|
-
html+=`<div style="padding:4px 10px;font-size:11px;color:var(--text3);display:flex;justify-content:space-between;align-items:center;cursor:pointer" onclick="document.getElementById('${mid}').classList.toggle('raw-collapsed')">`;
|
|
1587
|
-
html+=`<span><span style="color:var(--text);font-weight:600">${escHtml(role)}</span>`;
|
|
1588
|
-
if(keyLabel)html+=` <span class="badge badge-blue" style="font-size:10px">${escHtml(keyLabel)}</span>`;
|
|
1589
|
-
html+=`</span>`;
|
|
1590
|
-
html+=`<span>${escHtml(time)} <span style="margin-left:4px;opacity:0.5">${content.length}字符</span></span>`;
|
|
1591
|
-
html+=`</div>`;
|
|
1592
|
-
html+=`<div id="${mid}" class="raw-body" style="padding:4px 10px 8px;font-size:12px;white-space:pre-wrap;word-break:break-all;color:var(--text2);max-height:400px;overflow-y:auto;transition:max-height 0.2s">`;
|
|
1593
|
-
html+=escHtml(content.length>8000?content.slice(0,8000)+'\n... (共'+content.length+'字符)':content);
|
|
1594
|
-
if(content.length>8000)html+=`<button class="btn btn-sm btn-ghost" style="margin-top:4px;font-size:10px" onclick="event.stopPropagation();this.parentElement.textContent=this.dataset.full;this.remove()" data-full="${escHtml(content).replace(/"/g,'"')}">展开全部 (${content.length}字符)</button>`;
|
|
1595
|
-
html+=`</div></div>`;
|
|
1596
|
-
}
|
|
1597
|
-
html+=`</div>`;
|
|
1598
|
-
html+=`<div class="flex gap-8 mt-8"><button class="btn btn-ghost" onclick="goBack()">返回查看</button>`;
|
|
1599
|
-
html+=`<button class="btn btn-ghost" onclick="goBack()">返回会话列表</button></div>`;
|
|
1600
|
-
$('content').innerHTML=html;
|
|
1601
|
-
// 生成时间索引
|
|
1602
|
-
_buildTimeNav(msgs);
|
|
1603
|
-
}
|
|
1604
|
-
function rawFilter(key,btn){
|
|
1605
|
-
const items=document.querySelectorAll('.raw-item');
|
|
1606
|
-
for(const item of items){
|
|
1607
|
-
if(key==='all'||item.dataset.key===key){item.style.display=''}
|
|
1608
|
-
else{item.style.display='none'}
|
|
1609
|
-
}
|
|
1610
|
-
// 更新按钮样式
|
|
1611
|
-
const bar=document.getElementById('rawFilterBar');
|
|
1612
|
-
if(bar){
|
|
1613
|
-
bar.querySelectorAll('.btn').forEach(b=>{b.className='btn btn-sm btn-ghost'});
|
|
1614
|
-
if(btn)btn.className='btn btn-sm';
|
|
1615
|
-
if(btn)btn.style.background='var(--accent)';
|
|
1616
|
-
if(btn)btn.style.color='#fff';
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
function _buildTimeNav(msgs){
|
|
1620
|
-
// 按分钟分组生成时间跳转锚点
|
|
1621
|
-
const nav=document.getElementById('rawTimeNav');
|
|
1622
|
-
if(!nav||!msgs.length)return;
|
|
1623
|
-
const minutes={};
|
|
1624
|
-
for(let i=0;i<msgs.length;i++){
|
|
1625
|
-
const t=(msgs[i].time||'').slice(0,16); // YYYY-MM-DDTHH:MM
|
|
1626
|
-
if(!minutes[t])minutes[t]=[];
|
|
1627
|
-
minutes[t].push(i);
|
|
1628
|
-
}
|
|
1629
|
-
const times=Object.keys(minutes);
|
|
1630
|
-
if(times.length<=1)return;
|
|
1631
|
-
let navHtml='<span style="color:var(--text3);font-size:12px">时间索引:</span> ';
|
|
1632
|
-
for(const t of times){
|
|
1633
|
-
navHtml+=`<a href="javascript:void(0)" class="raw-time-link" onclick="document.querySelectorAll('.raw-item')[${minutes[t][0]}].scrollIntoView({behavior:'smooth',block:'center'})">${escHtml(t.slice(11))}</a>`;
|
|
1634
|
-
}
|
|
1635
|
-
nav.innerHTML=navHtml;
|
|
1636
|
-
}
|
|
1637
|
-
async function clearSession(sid){await api(`/api/sessions/${encodeURIComponent(sid)}`,{method:'DELETE'});renderSessions()}
|
|
1638
|
-
// 切入会话: 打开聊天页面并自动加载指定会话
|
|
1639
|
-
function enterSession(sid,agentName){
|
|
1640
|
-
window.location.href='/ui/chat/chat_container.html?agent='+encodeURIComponent(agentName)+'&mode=exec&session='+encodeURIComponent(sid);
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
// ========== Memory ==========
|
|
1644
|
-
let _memCategory='global';
|
|
1645
|
-
let _memAgentFilter=''; // [v1.18.9] agent筛选
|
|
1646
|
-
async function renderMemory(){
|
|
1647
|
-
// [v1.18.9] 加载 agent 列表供筛选
|
|
1648
|
-
let agentOpts='<option value="">全部 Agent</option>';
|
|
1649
|
-
try{const ags=await api('/api/agents');
|
|
1650
|
-
if(ags&&ags.length){for(const a of ags){agentOpts+=`<option value="${escHtml(a.path||a.name)}" ${_memAgentFilter===(a.path||a.name)?'selected':''}>${escHtml(a.name||a.path)}</option>`;}
|
|
1651
|
-
}}catch(e){}
|
|
1652
|
-
let stats={},lt=[];
|
|
1653
|
-
try{stats=await api('/api/memory/stats')}catch(e){stats={error:e.message}}
|
|
1654
|
-
try{lt=await api('/api/memory/list?category='+encodeURIComponent(_memCategory))}catch(e){lt=[]}
|
|
1655
|
-
if(stats.error){$('content').innerHTML='<div class="empty" style="color:var(--danger)">记忆系统异常: '+escHtml(stats.error)+'</div>';return}
|
|
1656
|
-
// [v1.18.9] 按 agent 筛选
|
|
1657
|
-
if(_memAgentFilter){lt=lt.filter(e=>(e.agent_id||e.session_id||'').includes(_memAgentFilter));}
|
|
1658
|
-
const cats=[{k:'global',l:'全局记忆'},{k:'session',l:'会话记忆'}];
|
|
1659
|
-
let tabHtml='<div class="flex gap-8 mb-8" style="flex-wrap:wrap">';
|
|
1660
|
-
for(const c of cats){
|
|
1661
|
-
const active=c.k===_memCategory?'btn-primary':'btn-ghost';
|
|
1662
|
-
const count=stats[c.k+'_count']||0;
|
|
1663
|
-
tabHtml+=`<button class="btn ${active}" onclick="_memCategory='${c.k}';renderMemory()">${c.l} (${count})</button>`;
|
|
1664
|
-
}
|
|
1665
|
-
tabHtml+='</div>';
|
|
1666
|
-
let html=`<div class="grid grid-3" style="margin-bottom:16px">
|
|
1667
|
-
<div class="stat"><div class="label">总计</div><div class="value">${stats.total_count||0}</div></div>
|
|
1668
|
-
<div class="stat"><div class="label">全局记忆</div><div class="value">${stats.global_count||0}</div></div>
|
|
1669
|
-
<div class="stat"><div class="label">会话记忆</div><div class="value">${stats.session_count||0}</div></div></div>`;
|
|
1670
|
-
html+=tabHtml;
|
|
1671
|
-
html+='<div class="flex gap-8 mb-16" style="flex-wrap:wrap"><select id="memAgentFilter" onchange="_memAgentFilter=this.value;renderMemory()" style="width:auto;min-width:140px">'+agentOpts+'</select><input id="memSearch" placeholder="搜索记忆..." onkeydown="if(event.key===\'Enter\')searchMemory()" style="max-width:300px"><button class="btn btn-primary" onclick="searchMemory()">搜索</button><button class="btn btn-ghost" onclick="cleanupMemory()">清理过期</button></div>';
|
|
1672
|
-
if(lt&<.length){
|
|
1673
|
-
const isSession=_memCategory==='session';
|
|
1674
|
-
html+='<div class="mem-list">';
|
|
1675
|
-
for(const e of lt){
|
|
1676
|
-
const content=(e.content||e.summary||'')||(e.role==='user'?'[用户消息]':e.role==='assistant'?'[助手回复]':'[系统]');
|
|
1677
|
-
let contentPreview=escHtml(content.slice(0,300));
|
|
1678
|
-
if(content.length>300)contentPreview+=`<span style="color:var(--text3)">... (${content.length}字)</span>`;
|
|
1679
|
-
html+=`<div class="mem-card">
|
|
1680
|
-
<div class="mem-card-header">
|
|
1681
|
-
<span class="mem-key">${escHtml(e.key||e.role||'-')}</span>
|
|
1682
|
-
<div class="mem-meta">
|
|
1683
|
-
<span class="tag">${escHtml(e.role||'')}</span>
|
|
1684
|
-
${!isSession&&e.importance!=null?'<span class="tag tag-imp">'+e.importance.toFixed(2)+'</span>':''}
|
|
1685
|
-
${e.session_id?'<span class="mem-session" title="'+escHtml(e.session_id)+'">'+escHtml((e.session_id||'').split('_web_')[0])+'</span>':''}
|
|
1686
|
-
</div>
|
|
1687
|
-
</div>
|
|
1688
|
-
<div class="mem-content">${contentPreview}</div>
|
|
1689
|
-
<div class="mem-actions"><button class="btn btn-sm btn-danger" onclick="deleteMemory('${e.id}')">删除</button></div>
|
|
1690
|
-
</div>`;
|
|
1691
|
-
}
|
|
1692
|
-
html+='</div>';
|
|
1693
|
-
}else{
|
|
1694
|
-
html+='<div class="empty">暂无'+(_memCategory==='session'?'会话':'全局')+'记忆</div>';
|
|
1695
|
-
}
|
|
1696
|
-
$('content').innerHTML=html;
|
|
1697
|
-
}
|
|
1698
|
-
async function searchMemory(){
|
|
1699
|
-
const q=$('memSearch').value;if(!q)return;
|
|
1700
|
-
const r=await api('/api/memory/search?q='+encodeURIComponent(q));
|
|
1701
|
-
let html='<h3>搜索结果: '+(r.length||0)+' 条</h3>';
|
|
1702
|
-
if(r&&r.length){
|
|
1703
|
-
html+='<div class="mem-list">';
|
|
1704
|
-
for(const e of r){
|
|
1705
|
-
const content=(e.content||'').slice(0,300);
|
|
1706
|
-
html+=`<div class="mem-card">
|
|
1707
|
-
<div class="mem-card-header">
|
|
1708
|
-
<span class="mem-key">${escHtml(e.key||'')}</span>
|
|
1709
|
-
<div class="mem-meta">
|
|
1710
|
-
<span class="tag">${e.category||''}</span>
|
|
1711
|
-
<span class="tag">${escHtml(e.role||'')}</span>
|
|
1712
|
-
<span class="mem-session">${escHtml((e.session_id||'').split('_web_')[0])}</span>
|
|
1713
|
-
</div>
|
|
1714
|
-
</div>
|
|
1715
|
-
<div class="mem-content">${escHtml(content)}</div>
|
|
1716
|
-
</div>`;
|
|
1717
|
-
}
|
|
1718
|
-
html+='</div>';
|
|
1719
|
-
}else{
|
|
1720
|
-
html+='<div class="empty">未找到匹配的记忆</div>';
|
|
1721
|
-
}
|
|
1722
|
-
html+='<button class="btn btn-ghost mt-8" onclick="goBack()">返回</button>';$('content').innerHTML=html;
|
|
1723
|
-
}
|
|
1724
|
-
async function deleteMemory(id){await api(`/api/memory/${id}`,{method:'DELETE'});renderMemory()}
|
|
1725
|
-
async function cleanupMemory(){const r=await api('/api/memory/cleanup',{method:'POST'});showToast('清理了 '+(r.cleaned||0)+' 条','success');renderMemory()}
|
|
1726
|
-
|
|
1727
|
-
// ========== 权限管理 ==========
|
|
1728
|
-
let _permsCache=null;
|
|
1729
|
-
async function renderPermissions(){
|
|
1730
|
-
const r=await api('/api/permissions');
|
|
1731
|
-
if(r.error){$('content').innerHTML='<div class="empty" style="color:var(--danger)">加载失败: '+escHtml(r.error)+'</div>';return}
|
|
1732
|
-
_permsCache=r;
|
|
1733
|
-
const labels=r.labels||{};
|
|
1734
|
-
const perms=r.all_permissions||[];
|
|
1735
|
-
const defaults=r.defaults||{};
|
|
1736
|
-
const agents=r.agents||{};
|
|
1737
|
-
|
|
1738
|
-
// 全局默认权限
|
|
1739
|
-
let html='<div class="card"><h3>全局默认权限</h3><p style="color:var(--text2);font-size:13px;margin-bottom:12px">新 Agent 将继承这些默认权限设置</p>';
|
|
1740
|
-
html+='<div class="grid grid-3" style="gap:12px">';
|
|
1741
|
-
for(const p of perms){
|
|
1742
|
-
const label=labels[p]||p;
|
|
1743
|
-
const val=defaults[p]!==false?'checked':'';
|
|
1744
|
-
html+=`<div class="form-group" style="display:flex;align-items:center;gap:10px"><label style="flex:1;font-weight:500">${label} <span style="color:var(--text3);font-size:11px">(${p})</span></label><input type="checkbox" id="perm_def_${p}" ${val} style="width:18px;height:18px;cursor:pointer"></div>`;
|
|
1745
|
-
}
|
|
1746
|
-
html+='</div><div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveDefaultPerms()">保存默认权限</button></div></div>';
|
|
1747
|
-
|
|
1748
|
-
// Agent 权限覆盖
|
|
1749
|
-
const agentKeys=Object.keys(agents);
|
|
1750
|
-
if(agentKeys.length>0){
|
|
1751
|
-
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>';
|
|
1752
|
-
for(const p of perms){html+=`<th>${labels[p]||p}</th>`;}
|
|
1753
|
-
html+='<th>操作</th></tr>';
|
|
1754
|
-
for(const name of agentKeys){
|
|
1755
|
-
const ap=agents[name]||{};
|
|
1756
|
-
html+=`<tr><td>${escHtml(name)}</td>`;
|
|
1757
|
-
for(const p of perms){
|
|
1758
|
-
const v=ap[p]!==false;
|
|
1759
|
-
html+=`<td style="text-align:center">${v?'<span style="color:var(--success)">✓</span>':'<span style="color:var(--danger)">✗</span>'}</td>`;
|
|
1760
|
-
}
|
|
1761
|
-
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>`;
|
|
1762
|
-
}
|
|
1763
|
-
html+='</table></div></div>';
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
$('content').innerHTML=html;
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
async function saveDefaultPerms(){
|
|
1770
|
-
if(!_permsCache)return;
|
|
1771
|
-
const perms=_permsCache.all_permissions||[];
|
|
1772
|
-
const data={};
|
|
1773
|
-
for(const p of perms){
|
|
1774
|
-
const el=document.getElementById('perm_def_'+p);
|
|
1775
|
-
if(el)data[p]=el.checked;
|
|
1776
|
-
}
|
|
1777
|
-
const r=await api('/api/permissions/defaults',{method:'PUT',body:JSON.stringify(data)});
|
|
1778
|
-
if(r.error){showToast(r.error,'danger');return}
|
|
1779
|
-
showToast('默认权限已保存','success');
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
async function editAgentPerms(name){
|
|
1783
|
-
if(!_permsCache)return;
|
|
1784
|
-
const labels=_permsCache.labels||{};
|
|
1785
|
-
const perms=_permsCache.all_permissions||[];
|
|
1786
|
-
const defaults=_permsCache.defaults||{};
|
|
1787
|
-
// 获取当前 agent 权限和 agent 信息(含执行模式)
|
|
1788
|
-
let agentPerms={};
|
|
1789
|
-
let agentExecMode='sandbox';
|
|
1790
|
-
try{
|
|
1791
|
-
const [r,ai]=await Promise.all([
|
|
1792
|
-
api('/api/permissions/'+encodeURIComponent(name)),
|
|
1793
|
-
api('/api/agents/'+encodeURIComponent(name))
|
|
1794
|
-
]);
|
|
1795
|
-
agentPerms=r.permissions||{};
|
|
1796
|
-
agentExecMode=ai.execution_mode||'sandbox';
|
|
1797
|
-
}catch(e){agentPerms={...defaults}}
|
|
1798
|
-
|
|
1799
|
-
let html='<h3>编辑 Agent 权限: '+escHtml(name)+'</h3>';
|
|
1800
|
-
|
|
1801
|
-
// 执行模式选择
|
|
1802
|
-
html+='<div style="margin-bottom:16px"><div style="font-size:13px;color:var(--text2);margin-bottom:8px;font-weight:600">⚡ 执行环境</div>';
|
|
1803
|
-
html+='<div style="display:flex;gap:12px">';
|
|
1804
|
-
html+=`<label id="modalExecSandboxLabel" style="flex:1;display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:var(--radius);border:2px solid ${agentExecMode==='sandbox'?'var(--primary)':'var(--border)'};cursor:pointer;background:${agentExecMode==='sandbox'?'var(--accent-light)':'transparent'};transition:all .15s" onclick="document.getElementById('modalExecSandbox').checked=true;updateModalExecUI()">`;
|
|
1805
|
-
html+=`<input type="radio" name="modalExecMode" id="modalExecSandbox" value="sandbox" ${agentExecMode==='sandbox'?'checked':''} style="display:none">`;
|
|
1806
|
-
html+=`<span style="font-size:24px">🐳</span><div><div style="font-weight:600;font-size:13px">沙盒模式</div><div style="font-size:11px;color:var(--text2)">Docker 容器隔离</div></div></label>`;
|
|
1807
|
-
html+=`<label id="modalExecLocalLabel" style="flex:1;display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:var(--radius);border:2px solid ${agentExecMode==='local'?'var(--primary)':'var(--border)'};cursor:pointer;background:${agentExecMode==='local'?'var(--accent-light)':'transparent'};transition:all .15s" onclick="document.getElementById('modalExecLocal').checked=true;updateModalExecUI()">`;
|
|
1808
|
-
html+=`<input type="radio" name="modalExecMode" id="modalExecLocal" value="local" ${agentExecMode==='local'?'checked':''} style="display:none">`;
|
|
1809
|
-
html+=`<span style="font-size:24px">💻</span><div><div style="font-weight:600;font-size:13px">本机模式</div><div style="font-size:11px;color:var(--text2)">完整本地权限</div></div></label>`;
|
|
1810
|
-
html+='</div></div>';
|
|
1811
|
-
|
|
1812
|
-
// 功能权限
|
|
1813
|
-
html+='<div style="font-size:13px;color:var(--text2);margin-bottom:8px;font-weight:600">🔑 功能权限</div>';
|
|
1814
|
-
html+='<div class="grid grid-3" style="gap:12px">';
|
|
1815
|
-
for(const p of perms){
|
|
1816
|
-
const label=labels[p]||p;
|
|
1817
|
-
const defVal=defaults[p]!==false;
|
|
1818
|
-
const curVal=agentPerms[p]!==undefined?agentPerms[p]:defVal;
|
|
1819
|
-
const checked=curVal?'checked':'';
|
|
1820
|
-
html+=`<div class="form-group" style="display:flex;align-items:center;gap:10px"><label style="flex:1;font-weight:500">${label}</label><input type="checkbox" id="perm_agent_${p}" ${checked} style="width:18px;height:18px;cursor:pointer"></div>`;
|
|
1821
|
-
}
|
|
1822
|
-
html+='</div><div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveAgentPerms(\''+escHtml(name)+'\')">💾 保存</button><button class="btn btn-ghost" onclick="renderPermissions()">取消</button></div>';
|
|
1823
|
-
$('modalContainer').innerHTML='<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()">'+html+'</div></div>';
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
function updateModalExecUI(){
|
|
1827
|
-
const sandbox=document.getElementById('modalExecSandbox');
|
|
1828
|
-
const local=document.getElementById('modalExecLocal');
|
|
1829
|
-
if(!sandbox||!local)return;
|
|
1830
|
-
[sandbox.parentElement,local.parentElement].forEach(el=>{
|
|
1831
|
-
const radio=el.querySelector('input[type=radio]');
|
|
1832
|
-
if(radio.checked){el.style.borderColor='var(--primary)';el.style.background='var(--accent-light)'}
|
|
1833
|
-
else{el.style.borderColor='var(--border)';el.style.background='transparent'}
|
|
1834
|
-
});
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
async function saveAgentPerms(name){
|
|
1838
|
-
if(!_permsCache)return;
|
|
1839
|
-
// 1. 保存执行模式
|
|
1840
|
-
const modalSandbox=document.getElementById('modalExecSandbox');
|
|
1841
|
-
const modalLocal=document.getElementById('modalExecLocal');
|
|
1842
|
-
if(modalSandbox||modalLocal){
|
|
1843
|
-
const newMode=(modalSandbox&&modalSandbox.checked)?'sandbox':((modalLocal&&modalLocal.checked)?'local':null);
|
|
1844
|
-
if(newMode){
|
|
1845
|
-
await api('/api/agents/'+encodeURIComponent(name),{method:'PUT',body:JSON.stringify({execution_mode:newMode})});
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
// 2. 保存功能权限
|
|
1849
|
-
const perms=_permsCache.all_permissions||[];
|
|
1850
|
-
const data={};
|
|
1851
|
-
for(const p of perms){
|
|
1852
|
-
const el=document.getElementById('perm_agent_'+p);
|
|
1853
|
-
if(el)data[p]=el.checked;
|
|
1854
|
-
}
|
|
1855
|
-
const r=await api('/api/permissions/'+encodeURIComponent(name),{method:'PUT',body:JSON.stringify(data)});
|
|
1856
|
-
if(r.error){showToast(r.error,'danger');return}
|
|
1857
|
-
closeModal();showToast('Agent 权限已更新','success');renderPermissions();
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
async function resetAgentPerms(name){
|
|
1861
|
-
if(!confirm('确认重置 "'+name+'" 的权限为默认值?'))return;
|
|
1862
|
-
const r=await api('/api/permissions/'+encodeURIComponent(name),{method:'DELETE'});
|
|
1863
|
-
if(r.error){showToast(r.error,'danger');return}
|
|
1864
|
-
showToast('已重置为默认权限','success');renderPermissions();
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
// ========== LLM ==========
|
|
1868
|
-
async function renderLLM(){
|
|
1869
|
-
const [u,models]=await Promise.all([api('/api/llm/usage'),api('/api/models')]);
|
|
1870
|
-
// 更新模型缓存
|
|
1871
|
-
allModelsCache=Array.isArray(models)?models:[];
|
|
1872
|
-
let html='';
|
|
1873
|
-
// 用量统计
|
|
1874
|
-
html+=`<div class="card"><h3>用量统计</h3><div class="grid grid-4">
|
|
1875
|
-
<div class="stat"><div class="label">调用次数</div><div class="value">${u.call_count||0}</div></div>
|
|
1876
|
-
<div class="stat"><div class="label">Prompt Tokens</div><div class="value">${u.total_prompt_tokens||0}</div></div>
|
|
1877
|
-
<div class="stat"><div class="label">Completion Tokens</div><div class="value">${u.total_completion_tokens||0}</div></div>
|
|
1878
|
-
<div class="stat"><div class="label">估算费用</div><div class="value">$${(u.total_cost_usd||0).toFixed(4)}</div></div></div></div>`;
|
|
1879
|
-
// 模型库列表
|
|
1880
|
-
const modelList=models||[];
|
|
1881
|
-
const enabledModels=modelList.filter(m=>m.enabled);
|
|
1882
|
-
html+=`<div class="card"><div class="flex justify-between items-center mb-16">
|
|
1883
|
-
<h3 style="margin:0">模型库 (${modelList.length})</h3>
|
|
1884
|
-
<div class="flex gap-8">
|
|
1885
|
-
<span class="badge badge-green">${enabledModels.length} 已启用</span>
|
|
1886
|
-
<button class="btn btn-sm btn-primary" onclick="showAddModelModal()">+ 添加模型</button>
|
|
1887
|
-
</div></div>`;
|
|
1888
|
-
if(!modelList.length){
|
|
1889
|
-
html+='<div class="empty">暂无自定义模型,点击上方按钮添加。</div>';
|
|
1890
|
-
}else{
|
|
1891
|
-
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>';
|
|
1892
|
-
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'};
|
|
1893
|
-
for(const m of modelList){
|
|
1894
|
-
const badgeClass=providerColors[m.provider]||'badge-green';
|
|
1895
|
-
const inputModes=m.input_modes?m.input_modes.join(','):'text';
|
|
1896
|
-
const isFallback=m.is_global_fallback!==false;
|
|
1897
|
-
html+=`<tr>
|
|
1898
|
-
<td><code>${escHtml(m.id)}</code></td>
|
|
1899
|
-
<td>${escHtml(m.name||'')}</td>
|
|
1900
|
-
<td><span class="badge ${badgeClass}">${escHtml(m.provider||'')}</span></td>
|
|
1901
|
-
<td style="max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtml(m.model||'')}">${escHtml(m.model||'')}</td>
|
|
1902
|
-
<td>${(m.context_window||128000).toLocaleString()}</td>
|
|
1903
|
-
<td>${escHtml(inputModes)}</td>
|
|
1904
|
-
<td>${m.reasoning?'<span class="badge badge-purple">是</span>':'-'}</td>
|
|
1905
|
-
<td><span style="cursor:help" title="作为保障系统运行的最终兜底模型">${isFallback?'<span class="badge badge-green">☑</span>':'<span class="badge badge-red">☐</span>'}</span></td>
|
|
1906
|
-
<td>${m.enabled?'<span class="badge badge-green">启用</span>':'<span class="badge badge-red">禁用</span>'}</td>
|
|
1907
|
-
<td><button class="btn btn-sm btn-ghost" onclick="showEditModelModal(encodeURIComponent('${escHtml(m.id)}'))">编辑</button>
|
|
1908
|
-
<button class="btn btn-sm btn-success" onclick="testModel(encodeURIComponent('${escHtml(m.id)}'))">测试</button>
|
|
1909
|
-
<button class="btn btn-sm btn-danger" onclick="deleteModel('${escHtml(m.id)}','${escHtml(String(m.name||'').replace(/'/g,"\\'"))}')">删除</button></td></tr>`;
|
|
1910
|
-
}
|
|
1911
|
-
html+='</table></div>';
|
|
1912
|
-
}
|
|
1913
|
-
html+='</div>';
|
|
1914
|
-
$('content').innerHTML=html;
|
|
1915
|
-
}
|
|
1916
|
-
// saveLLM / testLLM / showTestCode / hideTestCode 已移除(全局模型配置卡片已删除)
|
|
1917
|
-
async function testModel(modelId){
|
|
1918
|
-
modelId=decodeURIComponent(modelId);
|
|
1919
|
-
const model=allModelsCache.find(m=>m.id===modelId);
|
|
1920
|
-
if(!model){showToast('模型不存在','danger');return;}
|
|
1921
|
-
const btn=event.target;btn.textContent='测试中...';btn.disabled=true;
|
|
1922
|
-
// 直接发送模型自身配置,使用独立的测试接口,不经过任何兜底/fallback逻辑
|
|
1923
|
-
// [v1.16.18] 传递 input_modes,让后端测试对应的多模态能力
|
|
1924
|
-
const payload={model:model.model,base_url:model.base_url,provider:model.provider,temperature:model.temperature||0.1,api_key:model.api_key||''};
|
|
1925
|
-
if(model.api_type)payload.api_type=model.api_type;
|
|
1926
|
-
if(model.input_modes&&model.input_modes.length)payload.input_modes=model.input_modes;
|
|
1927
|
-
const r=await api('/api/llm/test',{method:'POST',body:JSON.stringify(payload)});
|
|
1928
|
-
btn.textContent='测试';btn.disabled=false;
|
|
1929
|
-
// [v1.16.18] 显示详细的测试结果
|
|
1930
|
-
if(r.ok){
|
|
1931
|
-
if(r.tests){
|
|
1932
|
-
// 有多模态测试结果
|
|
1933
|
-
const parts=['文本连接: OK'+(r.text_response?' ('+r.text_response+')':'')];
|
|
1934
|
-
if(r.tests.image===true)parts.push('图片识别: OK'+(r.tests.image_response?' ('+r.tests.image_response+')':''));
|
|
1935
|
-
else if(r.tests.image===false)parts.push('图片识别: FAIL'+(r.tests.image_error?' - '+r.tests.image_error:''));
|
|
1936
|
-
if(r.tests.video===null)parts.push('视频: 跳过(需要真实文件)');
|
|
1937
|
-
if(r.tests.audio===null)parts.push('音频: 跳过(需要真实文件)');
|
|
1938
|
-
showToast(parts.join('\n'),'success',8000);
|
|
1939
|
-
}else{
|
|
1940
|
-
showToast('连接成功: '+(r.text_response||r.response),'success');
|
|
1941
|
-
}
|
|
1942
|
-
}else{
|
|
1943
|
-
const msg=r.error||'连接失败';
|
|
1944
|
-
if(r.detail)showToast(msg+'\n'+r.detail,'danger',8000);
|
|
1945
|
-
else showToast(msg,'danger');
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
// ── Model Library Modal ──
|
|
1949
|
-
function showAddModelModal(editId){
|
|
1950
|
-
const model=allModelsCache.find(m=>m.id===editId);
|
|
1951
|
-
const isEdit=!!model;
|
|
1952
|
-
const id=model?model.id:'';
|
|
1953
|
-
const name=model?model.name:'';
|
|
1954
|
-
const provider=model?model.provider:'custom';
|
|
1955
|
-
const apiType=model?model.api_type:'openai-completions';
|
|
1956
|
-
const baseUrl=model?model.base_url:'';
|
|
1957
|
-
const apiKey=model&&model.api_key?model.api_key:'';
|
|
1958
|
-
const contextWindow=model?model.context_window:128000;
|
|
1959
|
-
const temperature=model?model.temperature:0.1;
|
|
1960
|
-
const inputModesArr=model&&model.input_modes?model.input_modes:['text'];
|
|
1961
|
-
const reasoning=model?model.reasoning:false;
|
|
1962
|
-
const maxTokens=model&&model.max_tokens?model.max_tokens:4096;
|
|
1963
|
-
const isFallback=model?model.is_global_fallback!==false:true;
|
|
1964
|
-
const provOpts=['custom','openai','anthropic','ollama','zhipu','deepseek','moonshot','qwen','modelscope'].map(p=>`<option value="${p}" ${provider===p?'selected':''}>${p}</option>`).join('');
|
|
1965
|
-
const apiTypeOpts=['openai-completions','openai-chat','anthropic','ollama'].map(t=>`<option value="${t}" ${apiType===t?'selected':''}>${t}</option>`).join('');
|
|
1966
|
-
const isCustomApiType=apiType==='custom'||!['openai-completions','openai-chat','anthropic','ollama'].includes(apiType);
|
|
1967
|
-
const inputModesChecked=['text','image','video','audio'].map(m=>`<label style="display:inline-flex;align-items:center;gap:4px;margin-right:12px"><input type="checkbox" class="mlInputMode" value="${m}" ${inputModesArr.indexOf(m)>-1?'checked':''}>${m}</label>`).join('');
|
|
1968
|
-
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()">
|
|
1969
|
-
<h3>${isEdit?'编辑模型':'添加模型'}</h3>
|
|
1970
|
-
<div class="form-row">
|
|
1971
|
-
<div class="form-group"><label>模型 ID ${isEdit?'(只读)':''}</label><input id="mlId" value="${escHtml(id)}" placeholder="如 gpt-4o" ${isEdit?'disabled style="opacity:.6"':''}></div>
|
|
1972
|
-
<div class="form-group"><label>显示名称</label><input id="mlName" value="${escHtml(name)}" placeholder="如 我的 GPT-4o"></div>
|
|
1973
|
-
</div>
|
|
1974
|
-
<div class="form-row">
|
|
1975
|
-
<div class="form-group"><label>Provider</label><select id="mlProvider">${provOpts}</select></div>
|
|
1976
|
-
<div class="form-group">
|
|
1977
|
-
<label>API 类型</label>
|
|
1978
|
-
<select id="mlApiTypeSelect" onchange="toggleApiTypeInput('ml')">
|
|
1979
|
-
${apiTypeOpts}
|
|
1980
|
-
<option value="custom" ${isCustomApiType?'selected':''}>custom (自定义)</option>
|
|
1981
|
-
</select>
|
|
1982
|
-
</div>
|
|
1983
|
-
</div>
|
|
1984
|
-
<div id="mlApiTypeCustomWrap" style="${isCustomApiType?'':'display:none'}" class="form-group">
|
|
1985
|
-
<label>自定义 API 类型</label>
|
|
1986
|
-
<input id="mlApiTypeCustom" value="${isCustomApiType?escHtml(apiType):''}" placeholder="输入自定义 API 类型">
|
|
1987
|
-
</div>
|
|
1988
|
-
<div class="form-group"><label>Base URL</label><input id="mlBaseUrl" value="${escHtml(baseUrl)}" placeholder="https://api.example.com/v1"></div>
|
|
1989
|
-
<div class="form-group"><label>API Key</label><input id="mlApiKey" type="text" value="${escHtml(apiKey)}" placeholder="sk-..." style="width:100%"></div>
|
|
1990
|
-
<div class="form-row">
|
|
1991
|
-
<div class="form-group"><label>上下文窗口</label><input id="mlContextWindow" type="number" value="${contextWindow}" placeholder="128000"></div>
|
|
1992
|
-
<div class="form-group"><label>最大输出</label><input id="mlMaxTokens" type="number" value="${maxTokens}" placeholder="4096"></div>
|
|
1993
|
-
</div>
|
|
1994
|
-
<div class="form-row">
|
|
1995
|
-
<div class="form-group"><label>Temperature</label><input id="mlTemp" type="number" step="0.1" value="${temperature}" placeholder="0.1"></div>
|
|
1996
|
-
<div class="form-group" style="display:flex;align-items:center">
|
|
1997
|
-
<input id="mlReasoning" type="checkbox" ${reasoning?'checked':''} style="margin-right:6px"><label for="mlReasoning" style="margin:0">支持推理</label>
|
|
1998
|
-
</div>
|
|
1999
|
-
</div>
|
|
2000
|
-
<div class="form-group"><label>输入模式</label><div style="padding:8px 0">${inputModesChecked}</div></div>
|
|
2001
|
-
<div class="form-group" style="display:flex;align-items:center">
|
|
2002
|
-
<input id="mlGlobalFallback" type="checkbox" ${isFallback?'checked':''} style="margin-right:6px"><label for="mlGlobalFallback" style="margin:0;cursor:help" title="作为保障系统运行的最终兜底模型">全局兜底</label>
|
|
2003
|
-
</div>
|
|
2004
|
-
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="doSaveModel('${escHtml(editId)}')">${isEdit?'保存':'添加'}</button><button class="btn btn-ghost" onclick="closeModal()">取消</button></div>
|
|
2005
|
-
</div></div>`;
|
|
2006
|
-
}
|
|
2007
|
-
function toggleApiTypeInput(prefix){
|
|
2008
|
-
const select=document.getElementById(prefix+'ApiTypeSelect');
|
|
2009
|
-
const customWrap=document.getElementById(prefix+'ApiTypeCustomWrap');
|
|
2010
|
-
if(customWrap){
|
|
2011
|
-
customWrap.style.display=select.value==='custom'?'block':'none';
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
function showEditModelModal(id){showAddModelModal(decodeURIComponent(id))}
|
|
2015
|
-
async function doSaveModel(editId){
|
|
2016
|
-
const inputModes=Array.from(document.querySelectorAll('.mlInputMode:checked')).map(c=>c.value);
|
|
2017
|
-
const apiTypeSelect=$('mlApiTypeSelect').value;
|
|
2018
|
-
const apiType=apiTypeSelect==='custom'?($('mlApiTypeCustom').value||'custom'):apiTypeSelect;
|
|
2019
|
-
const payload={
|
|
2020
|
-
name:$('mlName').value||$('mlId').value,
|
|
2021
|
-
provider:$('mlProvider').value,
|
|
2022
|
-
api_type:apiType,
|
|
2023
|
-
model:$('mlId').value,
|
|
2024
|
-
base_url:$('mlBaseUrl').value,
|
|
2025
|
-
context_window:parseInt($('mlContextWindow').value)||128000,
|
|
2026
|
-
max_tokens:parseInt($('mlMaxTokens').value)||4096,
|
|
2027
|
-
temperature:parseFloat($('mlTemp').value)||0.1,
|
|
2028
|
-
input_modes:inputModes,
|
|
2029
|
-
reasoning:$('mlReasoning').checked,
|
|
2030
|
-
is_global_fallback:$('mlGlobalFallback').checked
|
|
2031
|
-
};
|
|
2032
|
-
// api_key 始终发送,确保保存生效
|
|
2033
|
-
const apiKeyEl=$('mlApiKey');
|
|
2034
|
-
if(apiKeyEl)payload.api_key=apiKeyEl.value;
|
|
2035
|
-
let r;
|
|
2036
|
-
if(editId){
|
|
2037
|
-
r=await api(`/api/models/${encodeURIComponent(editId)}`,{method:'PUT',body:JSON.stringify(payload)});
|
|
2038
|
-
}else{
|
|
2039
|
-
payload.id=$('mlId').value;if(!payload.id){showToast('模型ID不能为空','danger');return}
|
|
2040
|
-
r=await api('/api/models',{method:'POST',body:JSON.stringify(payload)});
|
|
2041
|
-
}
|
|
2042
|
-
if(r.error){showToast(r.error,'danger');return}closeModal();showToast(editId?'已保存':'已添加','success');renderLLM();
|
|
2043
|
-
}
|
|
2044
|
-
async function deleteModel(id,name){
|
|
2045
|
-
showConfirm('删除模型','确认删除模型 "'+escHtml(name)+'" 吗?',async()=>{
|
|
2046
|
-
const r=await api(`/api/models/${id}`,{method:'DELETE'});if(r.error){showToast(r.error,'danger');closeModal();return}closeModal();showToast('已删除','success');renderLLM();
|
|
2047
|
-
});
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
// ========== Executor ==========
|
|
2051
|
-
async function renderExecutor(){
|
|
2052
|
-
const e=await api('/api/executor');if(!e)return;
|
|
2053
|
-
const isSandbox=e.mode==='sandbox';const dockerOk=e.docker_available;
|
|
2054
|
-
const lock=e.lock||{};
|
|
2055
|
-
const sandboxType=e.sandbox_type||'';
|
|
2056
|
-
const sandboxDesc=e.sandbox_desc||'';
|
|
2057
|
-
let html=`<div class="card"><h3>执行模式</h3>
|
|
2058
|
-
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
|
|
2059
|
-
<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">
|
|
2060
|
-
<input type="radio" name="execMode" value="local" ${!isSandbox?'checked':''} onchange="switchMode('local')">
|
|
2061
|
-
<div><strong>🖥️ 本机执行</strong><br><span style="font-size:12px;color:var(--text2)">直接在本机运行代码,功能完整,速度最快</span></div></label>
|
|
2062
|
-
<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">
|
|
2063
|
-
<input type="radio" name="execMode" value="sandbox" ${isSandbox?'checked':''} onchange="switchMode('sandbox')" ${!dockerOk?'disabled':''}>
|
|
2064
|
-
<div><strong>📦 沙盒执行</strong><br><span style="font-size:12px;color:var(--text2)">${dockerOk?'Docker 隔离容器':'轻量级进程沙盒'}${!dockerOk?' (Docker 不可用)':''}</span></div></label></div>
|
|
2065
|
-
<div style="font-size:13px;color:var(--text2);display:flex;flex-wrap:wrap;gap:8px">
|
|
2066
|
-
<span>当前模式: <span class="badge ${isSandbox?'badge-yellow':'badge-green'}">${isSandbox?'沙盒':'本机'}</span></span>
|
|
2067
|
-
${isSandbox?`<span>沙盒类型: <span class="tag">${sandboxType==='docker'?'Docker':'轻量级进程'}</span></span>`:''}
|
|
2068
|
-
<span>Docker: <span class="badge ${dockerOk?'badge-green':'badge-red'}">${dockerOk?'可用':'不可用'}</span></span>
|
|
2069
|
-
<span>累计执行: <span class="tag">${e.execution_count||0} 次</span></span>
|
|
2070
|
-
</div>
|
|
2071
|
-
<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>
|
|
2072
|
-
</div>`;
|
|
2073
|
-
// 执行锁状态
|
|
2074
|
-
html+=`<div class="card"><h3>🔒 全局执行锁</h3>
|
|
2075
|
-
<div style="font-size:13px">
|
|
2076
|
-
${lock.locked?
|
|
2077
|
-
`<span class="badge badge-red">已锁定</span> <strong>${escHtml(lock.locked_by||'')}</strong> — 锁定于 ${escHtml(lock.locked_at||'')}
|
|
2078
|
-
<div style="margin-top:8px"><button class="btn btn-sm btn-danger" onclick="releaseLock()">🔓 释放锁</button></div>`:
|
|
2079
|
-
`<span class="badge badge-green">未锁定</span> — 所有 Agent 可自由运行`
|
|
2080
|
-
}
|
|
2081
|
-
<div style="margin-top:6px;font-size:12px;color:var(--text3)">${isSandbox?'沙盒模式不走全局锁,支持多 Agent 并发执行':'本机模式下多个 Agent 共享此锁,同一时间只有一个 Agent 可以执行代码'}</div>
|
|
2082
|
-
</div></div>`;
|
|
2083
|
-
// 沙盒设置
|
|
2084
|
-
html+=`<div class="card"><h3>沙盒设置</h3><div class="form-row">
|
|
2085
|
-
<div class="form-group"><label>Docker 镜像</label><input id="sbImage" value="${escHtml(e.sandbox_image||'python:3.12-slim')}"></div>
|
|
2086
|
-
<div class="form-group"><label>内存限制</label><input id="sbMemory" value="${escHtml(e.sandbox_memory||'512m')}" placeholder="512m"></div>
|
|
2087
|
-
<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>
|
|
2088
|
-
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveExecutor()">保存设置</button></div></div>`;
|
|
2089
|
-
// 执行参数
|
|
2090
|
-
html+=`<div class="card"><h3>执行参数</h3><div class="form-row">
|
|
2091
|
-
<div class="form-group"><label>超时时间 (秒)</label><input id="exTimeout" type="number" value="${e.timeout||300}"></div>
|
|
2092
|
-
<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>
|
|
2093
|
-
<div class="form-group"><label>最大输出长度</label><input id="exMaxOutput" type="number" value="${e.max_output_length||50000}"></div></div>
|
|
2094
|
-
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveExecutor()">保存参数</button></div></div>`;
|
|
2095
|
-
$('content').innerHTML=html;
|
|
2096
|
-
}
|
|
2097
|
-
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();}}
|
|
2098
|
-
async function saveExecutor(){
|
|
2099
|
-
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)};
|
|
2100
|
-
const r=await api('/api/executor',{method:'PUT',body:JSON.stringify(body)});
|
|
2101
|
-
if(r.ok)showToast('已保存','success');else showToast('保存失败: '+(r.error||''),'danger');
|
|
2102
|
-
renderExecutor();
|
|
2103
|
-
}
|
|
2104
|
-
async function releaseLock(){
|
|
2105
|
-
const r=await api('/api/execution-lock',{method:'POST',body:JSON.stringify({action:'release'})});
|
|
2106
|
-
if(r.ok)showToast('锁已释放','success');else showToast('释放失败: '+(r.error||''),'danger');
|
|
2107
|
-
renderExecutor();
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
// ========== Skills ==========
|
|
2111
|
-
// [v1.23.0] CLI 命令分类配置(与 registry.py CLI_CATEGORY_LABELS/ICONS 同步)
|
|
2112
|
-
const CLI_CAT_ICONS={perception:'\u{1F441}',search:'\u{1F50D}',file:'\u{1F4C1}',document:'\u{1F4C4}',system:'\u{1F4BB}',browser:'\u{1F310}',gui:'\u{1F5A5}',memory:'\u{1F9E0}'};
|
|
2113
|
-
const CLI_CAT_LABELS={perception:'感知',search:'搜索',file:'文件操作',document:'文档生成',system:'系统',browser:'浏览器',gui:'GUI 桌面',memory:'记忆'};
|
|
2114
|
-
|
|
2115
|
-
async function renderSkills(){
|
|
2116
|
-
const cats=await api('/api/skills?view=categorized');
|
|
2117
|
-
const bt=cats.builtin_tools||[];
|
|
2118
|
-
const cc=cats.cli_commands||[];
|
|
2119
|
-
const ps=cats.python_skills||[];
|
|
2120
|
-
const sg=cats.skill_guides||[];
|
|
2121
|
-
const total=bt.length+cc.length+ps.length+sg.length;
|
|
2122
|
-
const disabledAll=[...ps,...sg].filter(s=>s.disabled).length;
|
|
2123
|
-
|
|
2124
|
-
let html=`<div class="flex justify-between items-center mb-16 flex-wrap gap-8">
|
|
2125
|
-
<div style="color:var(--text2);font-size:13px">
|
|
2126
|
-
共 <strong>${total}</strong> 个工具/技能 (内置${bt.length} · CLI${cc.length} · Python${ps.length} · 指南${sg.length}) · ${disabledAll} 已禁用
|
|
2127
|
-
</div>
|
|
2128
|
-
<div class="flex gap-8"><input id="skillSearch" placeholder="搜索工具/技能/CLI命令..." style="width:220px" oninput="filterSkills()">
|
|
2129
|
-
</div></div>`;
|
|
2130
|
-
|
|
2131
|
-
// ── 第一区: 内置平台工具 (LLM 直接调用) ──
|
|
2132
|
-
if(bt.length){
|
|
2133
|
-
html+=`<div class="card" style="padding:0;overflow:hidden;margin-bottom:16px">
|
|
2134
|
-
<h3 style="padding:12px 16px;margin:0;display:flex;align-items:center;gap:8px">
|
|
2135
|
-
<span>⚙️</span> 内置平台工具
|
|
2136
|
-
<span class="badge badge-blue">${bt.length}</span>
|
|
2137
|
-
<span style="font-size:11px;color:var(--text2);font-weight:normal;margin-left:auto">LLM 直接调用 · 不可禁用</span>
|
|
2138
|
-
</h3>`;
|
|
2139
|
-
for(const s of bt){html+=builtinRowHtml(s);}
|
|
2140
|
-
html+='</div>';
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
// ── 第二区: CLI 命令 (通过 command 工具间接调用) ──
|
|
2144
|
-
if(cc.length){
|
|
2145
|
-
// 按分类分组显示
|
|
2146
|
-
const groups={};
|
|
2147
|
-
for(const c of cc){
|
|
2148
|
-
const cat=c.category||'other';
|
|
2149
|
-
if(!groups[cat])groups[cat]=[];
|
|
2150
|
-
groups[cat].push(c);
|
|
2151
|
-
}
|
|
2152
|
-
html+=`<div class="card" style="padding:0;overflow:hidden;margin-bottom:16px">
|
|
2153
|
-
<h3 style="padding:12px 16px;margin:0;display:flex;align-items:center;gap:8px">
|
|
2154
|
-
<span>⚡</span> CLI 命令
|
|
2155
|
-
<span class="badge badge-cyan">${cc.length}</span>
|
|
2156
|
-
<span style="font-size:11px;color:var(--text2);font-weight:normal;margin-left:auto">通过 command 工具调用 · 不可禁用</span>
|
|
2157
|
-
</h3>`;
|
|
2158
|
-
for(const [cat,cmds] of Object.entries(groups)){
|
|
2159
|
-
const icon=CLI_CAT_ICONS[cat]||'🔧';
|
|
2160
|
-
const label=CLI_CAT_LABELS[cat]||cat;
|
|
2161
|
-
html+=`<div style="padding:8px 16px 4px;font-size:12px;color:var(--text2);font-weight:600">${icon} ${label} (${cmds.length})</div>`;
|
|
2162
|
-
for(const s of cmds){html+=cliCmdRowHtml(s);}
|
|
2163
|
-
}
|
|
2164
|
-
html+='</div>';
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
// ── 第三区: Python 可执行技能 ──
|
|
2168
|
-
if(ps.length){
|
|
2169
|
-
html+=`<div class="card" style="padding:0;overflow:hidden;margin-bottom:16px">
|
|
2170
|
-
<h3 style="padding:12px 16px;margin:0;display:flex;align-items:center;gap:8px">
|
|
2171
|
-
<span>🔧</span> Python 技能
|
|
2172
|
-
<span class="badge badge-green">${ps.length}</span>
|
|
2173
|
-
<span style="font-size:11px;color:var(--text2);font-weight:normal;margin-left:auto">CLI 底层实现 · SkillRegistry 管理</span>
|
|
2174
|
-
</h3>`;
|
|
2175
|
-
for(const s of ps){html+=skillRowHtml(s);}
|
|
2176
|
-
html+='</div>';
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
|
-
// ── 第四区: 技能指南 (RAG) ──
|
|
2180
|
-
if(sg.length){
|
|
2181
|
-
html+=`<div class="card" style="padding:0;overflow:hidden;margin-bottom:16px">
|
|
2182
|
-
<h3 style="padding:12px 16px;margin:0;display:flex;align-items:center;gap:8px">
|
|
2183
|
-
<span>📝</span> 技能指南
|
|
2184
|
-
<span class="badge badge-purple">${sg.length}</span>
|
|
2185
|
-
<span style="font-size:11px;color:var(--text2);font-weight:normal;margin-left:auto">Markdown Prompt · RAG 按需检索</span>
|
|
2186
|
-
</h3>`;
|
|
2187
|
-
for(const s of sg){html+=guideRowHtml(s);}
|
|
2188
|
-
html+='</div>';
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
if(!total)html+='<div class="empty">暂无工具/技能</div>';
|
|
2192
|
-
$('content').innerHTML=html;
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
/** 内置平台工具行 (只读,无 toggle) */
|
|
2196
|
-
function builtinRowHtml(s){
|
|
2197
|
-
const paramCount=(s.parameters||[]).length;
|
|
2198
|
-
const paramTag=paramCount>0?`<span class="tag">${paramCount} 参数</span>`:'';
|
|
2199
|
-
const catBadge=s.category?`<span class="badge badge-blue">${escHtml(s.category)}</span>`:'';
|
|
2200
|
-
return `<div class="skill-row" data-name="${escHtml(s.name||'')}" data-desc="${escHtml(s.description||'')}">
|
|
2201
|
-
<div style="flex-shrink:0;width:36px;text-align:center;font-size:18px">⚙️</div>
|
|
2202
|
-
<div class="skill-info">
|
|
2203
|
-
<div class="skill-name">${escHtml(s.name)} <span class="badge badge-blue">内置</span></div>
|
|
2204
|
-
<div class="skill-desc">${escHtml(s.description||'暂无描述')}</div>
|
|
2205
|
-
</div>
|
|
2206
|
-
<div class="flex gap-8 items-center" style="flex-shrink:0">
|
|
2207
|
-
${catBadge}${paramTag}
|
|
2208
|
-
<button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
|
|
2209
|
-
</div>
|
|
2210
|
-
</div>`;
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
/** CLI 命令行 (只读,无 toggle,紧凑显示) */
|
|
2214
|
-
function cliCmdRowHtml(s){
|
|
2215
|
-
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>`:'';
|
|
2216
|
-
const aliasTag=(s.aliases||[]).length>0?`<span class="tag">${s.aliases.map(a=>'alias: '+a).join(', ')}</span>`:'';
|
|
2217
|
-
return `<div class="skill-row" data-name="${escHtml(s.name||'')}" data-desc="${escHtml(s.description||'')}" style="padding:8px 16px 8px 48px">
|
|
2218
|
-
<div class="skill-info">
|
|
2219
|
-
<div class="skill-name" style="font-size:13px">${escHtml(s.name)} <span class="badge badge-cyan" style="font-size:10px">CLI</span> ${aliasTag}</div>
|
|
2220
|
-
<div class="skill-desc" style="font-size:12px">${escHtml(s.description||'')} ${cliText}</div>
|
|
2221
|
-
</div>
|
|
2222
|
-
<div class="flex gap-8 items-center" style="flex-shrink:0">
|
|
2223
|
-
<button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
|
|
2224
|
-
</div>
|
|
2225
|
-
</div>`;
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
/** Python 可执行技能行 (可 toggle) */
|
|
2229
|
-
function skillRowHtml(s){
|
|
2230
|
-
const isDisabled=s.disabled;
|
|
2231
|
-
const paramCount=(s.parameters||[]).length;
|
|
2232
|
-
const paramTag=paramCount>0?`<span class="tag">${paramCount} 参数</span>`:'';
|
|
2233
|
-
const catBadge=s.category?`<span class="badge badge-blue">${escHtml(s.category)}</span>`:'';
|
|
2234
|
-
return `<div class="skill-row" data-name="${escHtml(s.name||'')}" data-desc="${escHtml(s.description||'')}">
|
|
2235
|
-
<div style="flex-shrink:0;width:36px;text-align:center;font-size:18px">🔧</div>
|
|
2236
|
-
<div class="skill-info">
|
|
2237
|
-
<div class="skill-name">${escHtml(s.name)} ${isDisabled?'<span class="badge badge-red">已禁用</span>':''} ${s.dangerous?'<span class="badge badge-red">⚠ 危险</span>':''}</div>
|
|
2238
|
-
<div class="skill-desc">${escHtml(s.description||'暂无描述')}</div>
|
|
2239
|
-
</div>
|
|
2240
|
-
<div class="flex gap-8 items-center" style="flex-shrink:0">
|
|
2241
|
-
${catBadge}${paramTag}
|
|
2242
|
-
<label class="toggle" title="${isDisabled?'启用':'禁用'}"><input type="checkbox" ${!isDisabled?'checked':''} onchange="toggleSkill('${escHtml(s.name)}',this.checked)"><span class="slider"></span></label>
|
|
2243
|
-
<button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
|
|
2244
|
-
</div>
|
|
2245
|
-
</div>`;
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
/** 技能指南行 (可 toggle + 参考资料) */
|
|
2249
|
-
function guideRowHtml(s){
|
|
2250
|
-
const isDisabled=s.disabled;
|
|
2251
|
-
const refTag=(s.references||[]).length>0?`<span class="tag">${s.references.length} 参考资料</span>`:'';
|
|
2252
|
-
const tplTag=s.has_templates?'<span class="tag">含模板</span>':'';
|
|
2253
|
-
const scriptTag=s.has_scripts?'<span class="tag">含脚本</span>`:'';
|
|
2254
|
-
const catBadge=s.category?`<span class="badge badge-purple">${escHtml(s.category)}</span>`:'';
|
|
2255
|
-
return `<div class="skill-row" data-name="${escHtml(s.name||'')}" data-desc="${escHtml(s.description||'')}">
|
|
2256
|
-
<div style="flex-shrink:0;width:36px;text-align:center;font-size:18px">📝</div>
|
|
2257
|
-
<div class="skill-info">
|
|
2258
|
-
<div class="skill-name">${escHtml(s.name)} ${isDisabled?'<span class="badge badge-red">已禁用</span>':''}</div>
|
|
2259
|
-
<div class="skill-desc">${escHtml(s.description||'暂无描述')}</div>
|
|
2260
|
-
</div>
|
|
2261
|
-
<div class="flex gap-8 items-center" style="flex-shrink:0">
|
|
2262
|
-
${catBadge}${refTag}${tplTag}${scriptTag}
|
|
2263
|
-
<label class="toggle" title="${isDisabled?'启用':'禁用'}"><input type="checkbox" ${!isDisabled?'checked':''} onchange="toggleSkill('${escHtml(s.name)}',this.checked)"><span class="slider"></span></label>
|
|
2264
|
-
<button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
|
|
2265
|
-
</div>
|
|
2266
|
-
</div>`;
|
|
2267
|
-
}
|
|
2268
|
-
|
|
2269
|
-
function filterSkills(){
|
|
2270
|
-
const q=($('skillSearch')?.value||'').toLowerCase();
|
|
2271
|
-
document.querySelectorAll('.skill-row').forEach(el=>{
|
|
2272
|
-
const n=(el.dataset.name||'').toLowerCase();const d=(el.dataset.desc||'').toLowerCase();
|
|
2273
|
-
el.style.display=(n.includes(q)||d.includes(q))?'':'none';
|
|
2274
|
-
});
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
async function toggleSkill(name,enabled){
|
|
2278
|
-
const r=await api(`/api/skills/${encodeURIComponent(name)}/toggle`,{method:'POST',body:JSON.stringify({enabled})});
|
|
2279
|
-
if(r.error){showToast(r.error,'danger');return}
|
|
2280
|
-
showToast(enabled?'已启用':'已禁用','success');renderSkills();
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
async function viewSkillDetail(name){
|
|
2284
|
-
const s=await api(`/api/skills/${encodeURIComponent(name)}`);
|
|
2285
|
-
if(s.error){showToast(s.error,'danger');return}
|
|
2286
|
-
|
|
2287
|
-
// 判断类型标签
|
|
2288
|
-
const st=s.skill_type||'';
|
|
2289
|
-
let typeLabel,typeBadge;
|
|
2290
|
-
if(st==='builtin_platform'||st==='builtin_agent_tool'){
|
|
2291
|
-
typeLabel='内置平台工具';typeBadge='<span class="badge badge-blue">内置平台</span>';
|
|
2292
|
-
}else if(st==='cli_command'){
|
|
2293
|
-
typeLabel='CLI 命令';typeBadge='<span class="badge badge-cyan">CLI 命令</span>';
|
|
2294
|
-
}else if(st==='markdown'){
|
|
2295
|
-
typeLabel='技能指南 (RAG)';typeBadge='<span class="badge badge-purple">技能指南</span>';
|
|
2296
|
-
}else{
|
|
2297
|
-
typeLabel='Python 可执行技能';typeBadge='<span class="badge badge-green">Python 技能</span>';
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
// CLI 命令显示调用方式
|
|
2301
|
-
const cliInfo=s.cli?`<div class="form-group"><label>调用方式</label><div><code style="font-size:13px;color:var(--accent);background:var(--surface2);padding:4px 8px;border-radius:4px">${escHtml(s.cli)}</code></div></div>`:'';
|
|
2302
|
-
const aliasInfo=(s.aliases||[]).length>0?`<div class="form-group"><label>别名</label><div>${s.aliases.map(a=>`<code style="font-size:12px;background:var(--surface2);padding:2px 6px;border-radius:3px">${escHtml(a)}</code>`).join(' ')}</div></div>`:'';
|
|
2303
|
-
|
|
2304
|
-
let html=`<h3>${escHtml(s.name)}</h3>
|
|
2305
|
-
<div class="form-group"><label>描述</label><div style="font-size:13px;color:var(--text)">${escHtml(s.description||'无')}</div></div>
|
|
2306
|
-
<div class="form-row">
|
|
2307
|
-
<div class="form-group"><label>类型</label><div>${typeBadge}</div></div>
|
|
2308
|
-
<div class="form-group"><label>类别</label><div><span class="badge badge-blue">${escHtml(s.category||'general')}</span></div></div>
|
|
2309
|
-
</div>
|
|
2310
|
-
${s.note?`<div class="form-group"><label>备注</label><div style="font-size:13px;color:var(--text2)">${escHtml(s.note)}</div></div>`:''}
|
|
2311
|
-
${cliInfo}${aliasInfo}
|
|
2312
|
-
<div class="form-group"><label>危险操作</label><div>${s.dangerous?'<span class="badge badge-red">是</span>':'<span class="badge badge-green">否</span>'}</div></div>
|
|
2313
|
-
${(s.parameters||[]).length>0?`<div class="form-group"><label>参数 (${s.parameters.length})</label>
|
|
2314
|
-
<div class="table-wrap"><table><tr><th>名称</th><th>类型</th><th>必需</th><th>描述</th></tr>
|
|
2315
|
-
${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('')}
|
|
2316
|
-
</table></div></div>`:''}`;
|
|
2317
|
-
$('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>`;
|
|
2318
|
-
}
|
|
2319
|
-
|
|
2320
|
-
// ========== Files ==========
|
|
2321
|
-
var _workdirAgent=''; // 当前选中的 agent(空=全局工作目录)
|
|
2322
|
-
async function renderFiles(){
|
|
2323
|
-
try{
|
|
2324
|
-
const [wd,agents]=await Promise.all([api('/api/workdir').catch(()=>({})),api('/api/agents').catch(()=>[])]);
|
|
2325
|
-
const agentList=Array.isArray(agents)?agents.filter(a=>!a.system&&a.path!=='default'):[];
|
|
2326
|
-
const wdPath=wd&&wd.path?wd.path:'';
|
|
2327
|
-
let html=`<div class="flex items-center gap-8 mb-16 flex-wrap">
|
|
2328
|
-
<span style="font-size:14px;color:var(--text2)">📁 工作目录: <strong>${escHtml(wdPath)}</strong></span>
|
|
2329
|
-
<select id="workdirAgentSelect" onchange="onWorkdirAgentChange()" style="width:auto">
|
|
2330
|
-
<option value="">全局工作目录</option>
|
|
2331
|
-
${agentList.map(a=>`<option value="${escHtml(a.path)}" ${_workdirAgent===a.path?'selected':''}>${escHtml((a.avatar_emoji||'🤖')+' '+a.name)} (${escHtml(a.path)})</option>`).join('')}
|
|
2332
|
-
</select>
|
|
2333
|
-
<button class="btn btn-sm btn-ghost" onclick="changeWorkdir()">更改全局</button>
|
|
2334
|
-
<button class="btn btn-sm btn-ghost" onclick="renderFiles()">🔄 刷新</button></div>
|
|
2335
|
-
<div id="workdirContent">加载中...</div>`;
|
|
2336
|
-
$('content').innerHTML=html;
|
|
2337
|
-
loadWorkdirContent('');
|
|
2338
|
-
}catch(e){console.error('renderFiles error:',e);$('content').innerHTML='<div class="empty" style="color:var(--danger)">加载失败: '+escHtml(e.message||'未知错误')+'</div>';}
|
|
2339
|
-
}
|
|
2340
|
-
function onWorkdirAgentChange(){
|
|
2341
|
-
_workdirAgent=$('workdirAgentSelect').value;
|
|
2342
|
-
loadWorkdirContent('');
|
|
2343
|
-
}
|
|
2344
|
-
async function loadWorkdirContent(subPath){
|
|
2345
|
-
const params=new URLSearchParams();
|
|
2346
|
-
if(subPath)params.set('path',subPath);
|
|
2347
|
-
const files=await api('/api/workdir/files?'+params.toString());
|
|
2348
|
-
const el=document.getElementById('workdirContent');
|
|
2349
|
-
if(!el)return;
|
|
2350
|
-
// 面包屑
|
|
2351
|
-
var bcHtml='';
|
|
2352
|
-
if(subPath){
|
|
2353
|
-
var parts=subPath.split('/');var crumbs=['<span style="cursor:pointer;color:var(--primary)" onclick="loadWorkdirContent(\'\')">根目录</span>'];var acc='';
|
|
2354
|
-
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>');}
|
|
2355
|
-
bcHtml=crumbs.join('');
|
|
2356
|
-
}else{bcHtml='<span style="color:var(--text3)">根目录</span>';}
|
|
2357
|
-
// Agent 工作目录信息
|
|
2358
|
-
if(_workdirAgent){
|
|
2359
|
-
var agentCfg=await api('/api/agents/'+encodeURIComponent(_workdirAgent)).catch(()=>null);
|
|
2360
|
-
if(agentCfg&&agentCfg.work_dir){
|
|
2361
|
-
bcHtml+=` <span style="margin-left:12px;font-size:11px;color:var(--text3)">Agent 工作目录: ${escHtml(agentCfg.work_dir)}</span>`;
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
var html=`<div style="font-size:12px;margin-bottom:8px">${bcHtml}</div>`;
|
|
2365
|
-
html+='<div class="table-wrap"><table><tr><th>名称</th><th>大小</th><th></th></tr>';
|
|
2366
|
-
if(!files||!files.length){
|
|
2367
|
-
html+='<tr><td colspan="3" class="empty">目录为空</td></tr>';
|
|
2368
|
-
}else{
|
|
2369
|
-
// 排序:目录在前
|
|
2370
|
-
var dirs=files.filter(f=>f.type==='dir').sort((a,b)=>a.name.localeCompare(b.name));
|
|
2371
|
-
var fils=files.filter(f=>f.type==='file').sort((a,b)=>a.name.localeCompare(b.name));
|
|
2372
|
-
for(const d of dirs){
|
|
2373
|
-
var dp=d.path||d.name;
|
|
2374
|
-
html+=`<tr style="cursor:pointer" onclick="loadWorkdirContent('${escHtml(dp)}')"><td>📂 ${escHtml(d.name)}</td><td>-</td><td></td></tr>`;
|
|
2375
|
-
}
|
|
2376
|
-
for(const f of fils){
|
|
2377
|
-
var fp=f.path||f.name;
|
|
2378
|
-
var sizeStr=f.size>1048576?(f.size/1048576).toFixed(1)+' MB':f.size>1024?(f.size/1024).toFixed(1)+' KB':f.size+' B';
|
|
2379
|
-
html+=`<tr><td>📄 ${escHtml(f.name)}</td><td>${sizeStr}</td>
|
|
2380
|
-
<td><button class="btn btn-sm btn-ghost" onclick="downloadWorkdirFileGlobal('${escHtml(fp)}')">下载</button></td></tr>`;
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
html+='</table></div>';
|
|
2384
|
-
el.innerHTML=html;
|
|
2385
|
-
}
|
|
2386
|
-
function downloadWorkdirFileGlobal(relPath){
|
|
2387
|
-
var link=document.createElement('a');
|
|
2388
|
-
link.href=API+'/api/workdir/download/'+encodeURIComponent(relPath);
|
|
2389
|
-
link.download='';
|
|
2390
|
-
document.body.appendChild(link);link.click();document.body.removeChild(link);
|
|
2391
|
-
}
|
|
2392
|
-
async function changeWorkdir(){const p=prompt('新路径:');if(!p)return;await api('/api/workdir',{method:'PUT',body:JSON.stringify({path:p})});showToast('已更新','success');renderFiles();}
|
|
2393
|
-
|
|
2394
|
-
// ========== Logs ==========
|
|
2395
|
-
async function renderLogs(){
|
|
2396
|
-
let html=`<div class="flex gap-8 mb-16 flex-wrap">
|
|
2397
|
-
<select id="logLines" style="width:auto"><option value="100">100 行</option><option value="500" selected>500 行</option><option value="1000">1000 行</option><option value="2000">2000 行</option></select>
|
|
2398
|
-
<select id="logLevel" style="width:auto"><option value="">全部级别</option><option value="ERROR">ERROR</option><option value="WARNING">WARNING</option><option value="INFO">INFO</option><option value="DEBUG">DEBUG</option></select>
|
|
2399
|
-
<button class="btn btn-primary" onclick="loadLogs()">刷新</button>
|
|
2400
|
-
<button class="btn btn-ghost" onclick="toggleLogStream()" id="streamBtn">▶ 实时日志</button>
|
|
2401
|
-
<button class="btn btn-sm btn-ghost" onclick="clearLogViewer()" title="清空显示">🗑️ 清屏</button></div>
|
|
2402
|
-
<div class="log-viewer" id="logViewer" style="height:calc(100vh - 240px);font-family:monospace;font-size:12px;line-height:1.6;overflow-y:auto;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:8px 12px">加载中...</div>`;
|
|
2403
|
-
$('content').innerHTML=html;loadLogs();
|
|
2404
|
-
}
|
|
2405
|
-
async function loadLogs(){
|
|
2406
|
-
const lines=$('logLines').value;const level=$('logLevel').value;
|
|
2407
|
-
const logs=await api(`/api/logs?lines=${lines}${level?'&level='+level:''}`);
|
|
2408
|
-
const viewer=$('logViewer');
|
|
2409
|
-
viewer.innerHTML='';
|
|
2410
|
-
if(!logs||!logs.length){viewer.innerHTML='<div style="color:var(--text3)">暂无日志</div>';return;}
|
|
2411
|
-
for(const l of logs){appendLogLine(viewer,l);}
|
|
2412
|
-
viewer.scrollTop=viewer.scrollHeight;
|
|
2413
|
-
}
|
|
2414
|
-
function appendLogLine(viewer,line){
|
|
2415
|
-
var div=document.createElement('div');
|
|
2416
|
-
div.style.cssText='white-space:pre-wrap;word-break:break-all;padding:1px 0;border-bottom:1px solid var(--border)';
|
|
2417
|
-
// 高亮 ERROR/WARNING
|
|
2418
|
-
if(line.includes('ERROR')||line.includes('CRITICAL')){div.style.color='var(--danger)';div.style.fontWeight='600';}
|
|
2419
|
-
else if(line.includes('WARNING')){div.style.color='#f59e0b';}
|
|
2420
|
-
else if(line.includes('DEBUG')){div.style.color='var(--text3)';}
|
|
2421
|
-
div.textContent=line;
|
|
2422
|
-
viewer.appendChild(div);
|
|
2423
|
-
}
|
|
2424
|
-
function clearLogViewer(){if($('logViewer'))$('logViewer').innerHTML='';}
|
|
2425
|
-
let logStreamActive=false;let _logES=null;
|
|
2426
|
-
async function toggleLogStream(){
|
|
2427
|
-
if(logStreamActive){
|
|
2428
|
-
logStreamActive=false;
|
|
2429
|
-
if(_logES){_logES.close();_logES=null;}
|
|
2430
|
-
$('streamBtn').textContent='▶ 实时日志';
|
|
2431
|
-
$('streamBtn').style.background='';return;
|
|
2432
|
-
}
|
|
2433
|
-
logStreamActive=true;$('streamBtn').textContent='⏹ 停止实时';$('streamBtn').style.background='var(--danger)22';
|
|
2434
|
-
_logES=new EventSource(API+'/api/logs/stream');
|
|
2435
|
-
_logES.onmessage=function(e){
|
|
2436
|
-
try{
|
|
2437
|
-
var lines=JSON.parse(e.data).split('\n');
|
|
2438
|
-
var viewer=$('logViewer');if(!viewer)return;
|
|
2439
|
-
for(const l of lines){if(l.trim())appendLogLine(viewer,l);}
|
|
2440
|
-
viewer.scrollTop=viewer.scrollHeight;
|
|
2441
|
-
while(viewer.children.length>3000)viewer.removeChild(viewer.firstChild);
|
|
2442
|
-
}catch(ex){}
|
|
2443
|
-
};
|
|
2444
|
-
// [v1.18.7] 断线自动重连
|
|
2445
|
-
_logES.onerror=function(){
|
|
2446
|
-
if(_logES){_logES.close();_logES=null;}
|
|
2447
|
-
if(logStreamActive){
|
|
2448
|
-
$('streamBtn').textContent='⟳ 重连中...';
|
|
2449
|
-
setTimeout(function(){
|
|
2450
|
-
if(logStreamActive){toggleLogStream();}// 重新连接
|
|
2451
|
-
},3000);
|
|
2452
|
-
}
|
|
2453
|
-
};
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
// ========== Tasks ==========
|
|
2457
|
-
async function renderTasks(){
|
|
2458
|
-
// [v1.18.7] 同时获取实时 task plan 和持久化任务
|
|
2459
|
-
const [planR,persistR]=await Promise.all([api('/api/task-plan/all').catch(()=>({plans:[],total_keys:0})),api('/api/tasks').catch(()=>({tasks:[],total:0}))]);
|
|
2460
|
-
const plans=planR.plans||[];
|
|
2461
|
-
const persistTasks=persistR.tasks||[];
|
|
2462
|
-
// 统计
|
|
2463
|
-
var totalPlans=0,totalDone=0,totalPending=0,totalRunning=0;
|
|
2464
|
-
for(const p of plans){totalPlans+=p.total;totalDone+=p.done;totalPending+=p.pending;totalRunning+=p.running;}
|
|
2465
|
-
// 持久化任务统计
|
|
2466
|
-
var ptPending=0,ptRunning=0,ptCompleted=0,ptFailed=0;
|
|
2467
|
-
for(const t of persistTasks){if(t.status==='pending')ptPending++;else if(t.status==='running')ptRunning++;else if(t.status==='completed')ptCompleted++;else if(t.status==='failed')ptFailed++;}
|
|
2468
|
-
let html=`<div class="card" style="margin-bottom:16px"><h3>📊 概览</h3>
|
|
2469
|
-
<div class="grid grid-4" style="margin-top:12px">
|
|
2470
|
-
<div class="stat"><div class="label">活跃 Task Plan</div><div class="value">${plans.length}</div><div style="font-size:11px;color:var(--text3)">${totalPlans} 项任务</div></div>
|
|
2471
|
-
<div class="stat"><div class="label">待执行</div><div class="value">${totalPending}</div><div style="font-size:11px;color:var(--text3)">pending</div></div>
|
|
2472
|
-
<div class="stat"><div class="label">已完成</div><div class="value" style="color:var(--success)">${totalDone}</div><div style="font-size:11px;color:var(--text3)">done</div></div>
|
|
2473
|
-
<div class="stat"><div class="label">执行中</div><div class="value" style="color:var(--info)">${totalRunning}</div><div style="font-size:11px;color:var(--text3)">running</div></div>
|
|
2474
|
-
</div>
|
|
2475
|
-
${persistTasks.length?`<div style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border);font-size:13px;color:var(--text2)">
|
|
2476
|
-
持久化任务: ${ptPending} 待处理 · ${ptRunning} 运行中 · <span style="color:var(--success)">${ptCompleted} 已完成</span> · <span style="color:var(--danger)">${ptFailed} 失败</span>
|
|
2477
|
-
</div>`:''}
|
|
2478
|
-
</div>`;
|
|
2479
|
-
html+=`<div class="flex gap-8 mb-16 flex-wrap">
|
|
2480
|
-
<button class="btn btn-sm btn-ghost" onclick="renderTasks()">🔄 刷新</button></div>`;
|
|
2481
|
-
// 实时 Task Plan 列表
|
|
2482
|
-
if(plans.length){
|
|
2483
|
-
html+='<h3 style="margin-bottom:8px">📋 实时任务计划 ('+plans.length+' 个活跃)</h3>';
|
|
2484
|
-
for(const p of plans){
|
|
2485
|
-
html+=`<div class="card" style="margin-bottom:12px;padding:12px 16px">
|
|
2486
|
-
<div class="flex justify-between items-center mb-8" style="flex-wrap:wrap;gap:8px">
|
|
2487
|
-
<div><strong>${escHtml(p.label)}</strong> <span class="tag">${p.type==='session'?'会话':'Agent'}</span></div>
|
|
2488
|
-
<div class="flex gap-4" style="font-size:12px">
|
|
2489
|
-
<span class="badge badge-yellow">${p.pending} 待执行</span>
|
|
2490
|
-
<span class="badge badge-blue">${p.running} 执行中</span>
|
|
2491
|
-
<span class="badge badge-green">${p.done} 已完成</span>
|
|
2492
|
-
</div>
|
|
2493
|
-
</div>
|
|
2494
|
-
<div style="margin-top:4px">`;
|
|
2495
|
-
for(var i=0;i<p.tasks.length;i++){
|
|
2496
|
-
var t=p.tasks[i];
|
|
2497
|
-
var st=t.status||'pending';
|
|
2498
|
-
var stBadge=st==='done'?'<span class="badge badge-green" style="font-size:10px">✓ done</span>':
|
|
2499
|
-
st==='running'?'<span class="badge badge-blue" style="font-size:10px">⟳ running</span>':
|
|
2500
|
-
'<span class="badge badge-yellow" style="font-size:10px">○ pending</span>';
|
|
2501
|
-
var checked=st==='done'?'checked':'';
|
|
2502
|
-
html+=`<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:13px;${st==='done'?'text-decoration:line-through;color:var(--text3)':''}">
|
|
2503
|
-
<input type="checkbox" ${checked} style="flex-shrink:0" onclick="toggleTaskPlanItem('${escHtml(p.key)}',${i},this.checked)">
|
|
2504
|
-
<span style="flex:1">${escHtml(t.text||'')}</span>${stBadge}
|
|
2505
|
-
</div>`;
|
|
2506
|
-
}
|
|
2507
|
-
html+=`</div></div>`;
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
if(!plans.length){
|
|
2511
|
-
html+='<div class="empty" style="margin-bottom:16px">暂无活跃的实时任务计划</div>';
|
|
2512
|
-
}
|
|
2513
|
-
// 持久化任务列表
|
|
2514
|
-
if(persistTasks.length){
|
|
2515
|
-
html+=`<h3 style="margin-bottom:8px;margin-top:16px">💾 持久化任务 (${persistTasks.length} 条)</h3>`;
|
|
2516
|
-
html+='<div class="table-wrap"><table><tr><th>任务ID</th><th>描述</th><th>来源</th><th>状态</th><th>时间</th><th>操作</th></tr>';
|
|
2517
|
-
for(const t of persistTasks){
|
|
2518
|
-
var statusBadge=t.status==='completed'?'<span class="badge badge-green">已完成</span>':
|
|
2519
|
-
t.status==='running'?'<span class="badge badge-blue">运行中</span>':
|
|
2520
|
-
t.status==='failed'?'<span class="badge badge-red">失败</span>':
|
|
2521
|
-
'<span class="badge badge-yellow">待处理</span>';
|
|
2522
|
-
var meta=t.metadata||{};
|
|
2523
|
-
var source=meta.source==='group_chat'?'群聊':meta.source||'';
|
|
2524
|
-
html+=`<tr data-status="${t.status}">
|
|
2525
|
-
<td style="font-family:monospace;font-size:11px;max-width:120px;overflow:hidden;text-overflow:ellipsis" title="${escHtml(t.task_id)}">${escHtml(t.task_id)}</td>
|
|
2526
|
-
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtml(t.description)}">${escHtml((t.description||'').slice(0,100))}</td>
|
|
2527
|
-
<td>${source}${meta.group_name?' / '+escHtml(meta.group_name):''}</td>
|
|
2528
|
-
<td>${statusBadge}</td>
|
|
2529
|
-
<td style="font-size:12px;white-space:nowrap">${fmtTimeAgo(t.updated_at)}</td>
|
|
2530
|
-
<td class="flex gap-8">
|
|
2531
|
-
${t.status==='failed'||t.status==='pending'?`<button class="btn btn-sm btn-primary" onclick="retryTask('${escHtml(t.task_id)}')">重试</button>`:''}
|
|
2532
|
-
<button class="btn btn-sm btn-danger" onclick="deleteTask('${escHtml(t.task_id)}')">删除</button>
|
|
2533
|
-
</td></tr>`;
|
|
2534
|
-
}
|
|
2535
|
-
html+='</table></div>';
|
|
2536
|
-
}
|
|
2537
|
-
if(!plans.length&&!persistTasks.length){
|
|
2538
|
-
html+='<div class="empty">暂无任务记录</div>';
|
|
2539
|
-
}
|
|
2540
|
-
$('content').innerHTML=html;
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
async function toggleTaskPlanItem(key,idx,checked){
|
|
2544
|
-
await api('/api/task-plan/all');
|
|
2545
|
-
// 直接调用 task-plan API 修改状态
|
|
2546
|
-
const r=await api('/api/task-plan?agent='+encodeURIComponent(key));
|
|
2547
|
-
if(!r||!r.tasks)return;
|
|
2548
|
-
var tasks=r.tasks;
|
|
2549
|
-
if(idx>=0&&idx<tasks.length){
|
|
2550
|
-
tasks[idx].status=checked?'done':'pending';
|
|
2551
|
-
await api('/api/task-plan',{method:'PUT',body:JSON.stringify({agent:key,session:r.session,tasks:tasks})});
|
|
2552
|
-
renderTasks();
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
|
-
function filterTasks(){
|
|
2557
|
-
const s=$('taskStatusFilter')?.value||'';
|
|
2558
|
-
document.querySelectorAll('#taskList tr').forEach(tr=>{
|
|
2559
|
-
if(!tr.dataset.status)return;
|
|
2560
|
-
tr.style.display=(!s||tr.dataset.status===s)?'':'none';
|
|
2561
|
-
});
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
async function retryTask(taskId){
|
|
2565
|
-
showToast('正在重试...','info');
|
|
2566
|
-
const r=await api(`/api/tasks/${encodeURIComponent(taskId)}/retry`,{method:'POST'});
|
|
2567
|
-
if(r.error){showToast('重试失败: '+r.error,'danger');return;}
|
|
2568
|
-
showToast('任务已重新提交','success');renderTasks();
|
|
2569
|
-
}
|
|
2570
|
-
|
|
2571
|
-
async function deleteTask(taskId){
|
|
2572
|
-
showConfirm('删除任务','确认删除此任务记录?此操作不可恢复。',async()=>{
|
|
2573
|
-
const r=await api(`/api/tasks/${encodeURIComponent(taskId)}`,{method:'DELETE'});
|
|
2574
|
-
if(r.error){showToast(r.error,'danger');closeModal();return;}
|
|
2575
|
-
closeModal();showToast('已删除','success');renderTasks();
|
|
2576
|
-
});
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
|
-
let _deptTreeNeedsRefresh=false;
|
|
2580
|
-
function closeModal(){$('modalContainer').innerHTML='';if(_deptTreeNeedsRefresh){_deptTreeNeedsRefresh=false;renderDepartments()}}
|
|
2581
|
-
|
|
2582
|
-
// ========== Organization ==========
|
|
2583
|
-
async function renderOrganization(){
|
|
2584
|
-
const [org,info]=await Promise.all([api('/api/organization'),api('/api/organization/info')]);
|
|
2585
|
-
if(org.error){$('content').innerHTML='<div class="empty" style="color:var(--danger)">加载失败: '+escHtml(org.error)+'</div>';return}
|
|
2586
|
-
const cfg=org.organization||org;
|
|
2587
|
-
const inf=info.info||info||{};
|
|
2588
|
-
let html=`<div class="card"><h3>组织配置</h3>
|
|
2589
|
-
<div class="form-row">
|
|
2590
|
-
<div class="form-group"><label>启用组织管理</label><label class="toggle"><input type="checkbox" id="orgEnabled" ${cfg.enabled?'checked':''}><span class="slider"></span></label></div>
|
|
2591
|
-
<div class="form-group"><label>知识库管理员</label><input id="orgAdmin" value="${escHtml(cfg.knowledge_admin||'')}" placeholder="管理员用户名"></div>
|
|
2592
|
-
</div>
|
|
2593
|
-
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveOrgConfig()">保存配置</button></div></div>`;
|
|
2594
|
-
html+=`<div class="card"><h3>组织信息</h3>
|
|
2595
|
-
<div class="form-row">
|
|
2596
|
-
<div class="form-group"><label>组织名称</label><input id="orgName" value="${escHtml(inf.name||'')}" placeholder="我的组织"></div>
|
|
2597
|
-
<div class="form-group"><label>组织描述</label><input id="orgDesc" value="${escHtml(inf.description||'')}" placeholder="组织简介"></div>
|
|
2598
|
-
<div class="form-group"><label>联系方式</label><input id="orgContact" value="${escHtml(inf.contact||'')}" placeholder="联系邮箱或电话"></div>
|
|
2599
|
-
<div class="form-group"><label>网站</label><input id="orgWebsite" value="${escHtml(inf.website||'')}" placeholder="https://..."></div>
|
|
2600
|
-
</div>
|
|
2601
|
-
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveOrgInfo()">保存信息</button></div></div>`;
|
|
2602
|
-
// 知识库 — 未启用组织时隐藏
|
|
2603
|
-
const orgEnabled=cfg.enabled;
|
|
2604
|
-
html+=`<div class="card" id="orgKBCard"><div class="flex justify-between items-center mb-16">
|
|
2605
|
-
<h3 style="margin:0">组织知识库</h3>
|
|
2606
|
-
<div id="orgKBActions">${orgEnabled?'<button class="btn btn-sm btn-primary" onclick="uploadOrgKnowledge(false)">上传文件</button> <button class="btn btn-sm btn-secondary" onclick="uploadOrgKnowledge(true)">📁 上传文件夹</button>':''}</div></div>
|
|
2607
|
-
<div id="orgKBList"><div class="empty">${orgEnabled?'加载中...':'组织管理未启用,请先启用后再使用知识库'}</div></div></div>`;
|
|
2608
|
-
$('content').innerHTML=html;
|
|
2609
|
-
if(orgEnabled)loadOrgKnowledge();
|
|
2610
|
-
}
|
|
2611
|
-
async function saveOrgConfig(){
|
|
2612
|
-
const r=await api('/api/organization',{method:'PUT',body:JSON.stringify({enabled:$('orgEnabled').checked,knowledge_admin:$('orgAdmin').value})});
|
|
2613
|
-
if(r.error){showToast(r.error,'danger');return}showToast('已保存','success');renderOrganization();
|
|
2614
|
-
}
|
|
2615
|
-
async function saveOrgInfo(){
|
|
2616
|
-
const r=await api('/api/organization/info',{method:'PUT',body:JSON.stringify({name:$('orgName').value,description:$('orgDesc').value,contact:$('orgContact').value,website:$('orgWebsite').value})});
|
|
2617
|
-
if(r.error){showToast(r.error,'danger');return}showToast('已保存','success');
|
|
2618
|
-
}
|
|
2619
|
-
async function loadOrgKnowledge(){
|
|
2620
|
-
const files=await api('/api/organization/knowledge');
|
|
2621
|
-
const el=document.getElementById('orgKBList');if(!el)return;
|
|
2622
|
-
if(!files||!files.length){el.innerHTML='<div class="empty">暂无知识库文件</div>';return}
|
|
2623
|
-
let html='<div class="table-wrap"><table><tr><th>文件名</th><th>大小</th><th>操作</th></tr>';
|
|
2624
|
-
for(const f of files){
|
|
2625
|
-
html+=`<tr><td>${escHtml(f.name||f.path)}</td><td>${f.size?f.size>1024?(f.size/1024).toFixed(1)+' KB':f.size+' B':'-'}</td>
|
|
2626
|
-
<td><button class="btn btn-sm btn-ghost" onclick="viewOrgKBFile('${escHtml(f.path||f.name)}')">查看</button>
|
|
2627
|
-
<button class="btn btn-sm btn-ghost" onclick="downloadOrgKBFile('${escHtml(f.path||f.name)}')">下载</button>
|
|
2628
|
-
<button class="btn btn-sm btn-danger" onclick="deleteOrgKBFile('${escHtml(f.path||f.name)}')">删除</button></td></tr>`;
|
|
2629
|
-
}
|
|
2630
|
-
html+='</table></div>';el.innerHTML=html;
|
|
2631
|
-
}
|
|
2632
|
-
function uploadOrgKnowledge(folderMode){
|
|
2633
|
-
const input=document.createElement('input');input.type='file';input.multiple=true;
|
|
2634
|
-
if(folderMode){input.webkitdirectory=true;}
|
|
2635
|
-
input.onchange=async()=>{
|
|
2636
|
-
const fd=new FormData();
|
|
2637
|
-
for(const f of input.files){
|
|
2638
|
-
// 文件夹上传时保留相对路径
|
|
2639
|
-
if(f.webkitRelativePath){fd.append('files',f,{headers:{'X-File-Path':f.webkitRelativePath}});}
|
|
2640
|
-
else{fd.append('files',f);}
|
|
2641
|
-
}
|
|
2642
|
-
showToast('正在上传...','info');
|
|
2643
|
-
const r=await fetch(API+'/api/organization/knowledge/upload',{method:'POST',body:fd});
|
|
2644
|
-
const data=await r.json();
|
|
2645
|
-
if(data.error){showToast(data.error,'danger');return}
|
|
2646
|
-
const total=data.results?data.results.length:0;
|
|
2647
|
-
const okCount=data.results?data.results.filter(x=>x.ok).length:0;
|
|
2648
|
-
showToast(`上传完成: ${okCount}/${total} 文件成功`,okCount===total?'success':'warning');
|
|
2649
|
-
loadOrgKnowledge();
|
|
2650
|
-
};input.click();
|
|
2651
|
-
}
|
|
2652
|
-
async function viewOrgKBFile(path){
|
|
2653
|
-
const r=await fetch(API+'/api/organization/knowledge/file?path='+encodeURIComponent(path));
|
|
2654
|
-
const data=await r.json();
|
|
2655
|
-
if(data.error){showToast(data.error,'danger');return}
|
|
2656
|
-
const content=typeof data==='string'?data:(data.content||JSON.stringify(data,null,2));
|
|
2657
|
-
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:800px">
|
|
2658
|
-
<h3>${escHtml(path)}</h3>
|
|
2659
|
-
<div class="log-viewer" style="max-height:60vh;white-space:pre-wrap">${escHtml(content.slice(0,5000))}${content.length>5000?'\\n...(已截断)':''}</div>
|
|
2660
|
-
<div class="flex gap-8 mt-16"><button class="btn btn-ghost" onclick="closeModal()">关闭</button></div>
|
|
2661
|
-
</div></div>`;
|
|
2662
|
-
}
|
|
2663
|
-
async function downloadOrgKBFile(path){
|
|
2664
|
-
const a=document.createElement('a');
|
|
2665
|
-
a.href=API+'/api/organization/knowledge/file?path='+encodeURIComponent(path)+'&download=1';
|
|
2666
|
-
a.download=path.split('/').pop();
|
|
2667
|
-
a.click();
|
|
2668
|
-
}
|
|
2669
|
-
async function deleteOrgKBFile(path){
|
|
2670
|
-
showConfirm('删除文件','确认删除 "'+escHtml(path)+'" 吗?',async()=>{
|
|
2671
|
-
const r=await api('/api/organization/knowledge?path='+encodeURIComponent(path),{method:'DELETE'});
|
|
2672
|
-
if(r.error){showToast(r.error,'danger');closeModal();return}closeModal();showToast('已删除','success');loadOrgKnowledge();
|
|
2673
|
-
});
|
|
2674
|
-
}
|
|
2675
|
-
|
|
2676
|
-
// ========== Departments ==========
|
|
2677
|
-
async function renderDepartments(){
|
|
2678
|
-
const [tree,agents]=await Promise.all([api('/api/departments'),api('/api/agents')]);
|
|
2679
|
-
if(tree.error){$('content').innerHTML='<div class="empty" style="color:var(--danger)">加载失败: '+escHtml(tree.error)+'</div>';return}
|
|
2680
|
-
allAgentsCache=Array.isArray(agents)?agents:allAgentsCache;
|
|
2681
|
-
const depts=Array.isArray(tree)?tree:(tree.children||[]);
|
|
2682
|
-
let html=`<div class="flex justify-between items-center mb-16">
|
|
2683
|
-
<div style="color:var(--text2);font-size:13px">共 ${countDepts(depts)} 个部门</div>
|
|
2684
|
-
<button class="btn btn-primary" onclick="showCreateDeptModal()">+ 创建部门</button></div>`;
|
|
2685
|
-
if(!depts.length){html+='<div class="empty">暂无部门,点击上方按钮创建</div>';$('content').innerHTML=html;return}
|
|
2686
|
-
html+='<div id="deptTree">';
|
|
2687
|
-
html+=renderDeptTree(depts,0);
|
|
2688
|
-
html+='</div>';
|
|
2689
|
-
$('content').innerHTML=html;
|
|
2690
|
-
}
|
|
2691
|
-
function countDepts(list){let c=0;for(const d of list||[]){c++;c+=countDepts(d.children||[])}return c}
|
|
2692
|
-
function renderDeptTree(list,depth){
|
|
2693
|
-
let html='';
|
|
2694
|
-
for(const d of list||[]){
|
|
2695
|
-
const hasChildren=(d.children||[]).length>0;
|
|
2696
|
-
const indent=depth*24;
|
|
2697
|
-
html+=`<div style="margin-left:${indent}px;border:1px solid var(--border);border-radius:var(--radius);margin-bottom:8px;background:var(--surface);padding:12px 16px">
|
|
2698
|
-
<div class="flex justify-between items-center">
|
|
2699
|
-
<div class="flex items-center gap-8">
|
|
2700
|
-
<span style="font-size:20px">${escHtml(d.emoji||'📁')}</span>
|
|
2701
|
-
<div><div style="font-weight:600">${escHtml(d.name)} <span class="tag">${d.agent_count||0} 成员</span></div>
|
|
2702
|
-
<div style="font-size:12px;color:var(--text2)">${escHtml(d.description||'无描述')} ${hasChildren?'· '+d.children.length+' 个子部门':''}</div></div>
|
|
2703
|
-
</div>
|
|
2704
|
-
<div class="flex gap-8">
|
|
2705
|
-
<button class="btn btn-sm btn-ghost" onclick="showDeptDetail('${escHtml(d.path||d.name)}')">详情/管理成员</button>
|
|
2706
|
-
<button class="btn btn-sm btn-primary" onclick="showCreateDeptModal('${escHtml(d.path||d.name)}')">子部门</button>
|
|
2707
|
-
<button class="btn btn-sm btn-danger" onclick="deleteDept('${escHtml(d.path||d.name)}','${escHtml(d.name)}')">删除</button>
|
|
2708
|
-
</div>
|
|
2709
|
-
</div>
|
|
2710
|
-
${hasChildren?renderDeptTree(d.children,depth+1):''}
|
|
2711
|
-
</div>`;
|
|
2712
|
-
}
|
|
2713
|
-
return html;
|
|
2714
|
-
}
|
|
2715
|
-
function showCreateDeptModal(parent){
|
|
2716
|
-
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()">
|
|
2717
|
-
<h3>${parent?'创建子部门':'创建部门'}</h3>
|
|
2718
|
-
${parent?'<div class="form-group"><label>父部门</label><input value="'+escHtml(parent)+'" disabled></div>':''}
|
|
2719
|
-
<div class="form-row">
|
|
2720
|
-
<div class="form-group"><label>部门名称 *</label><input id="deptName" placeholder="部门名称"></div>
|
|
2721
|
-
<div class="form-group"><label>Emoji</label><input id="deptEmoji" placeholder="📁" maxlength="4"></div>
|
|
2722
|
-
</div>
|
|
2723
|
-
<div class="form-group"><label>描述</label><textarea id="deptDesc" rows="3" placeholder="部门描述..."></textarea></div>
|
|
2724
|
-
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="doCreateDept('${escHtml(parent||'')}')">创建</button><button class="btn btn-ghost" onclick="closeModal()">取消</button></div>
|
|
2725
|
-
</div></div>`;
|
|
2726
|
-
}
|
|
2727
|
-
async function doCreateDept(parent){
|
|
2728
|
-
const r=await api('/api/departments',{method:'POST',body:JSON.stringify({name:$('deptName').value,emoji:$('deptEmoji').value,description:$('deptDesc').value,parent})});
|
|
2729
|
-
if(r.error){showToast(r.error,'danger');return}closeModal();showToast('已创建','success');renderDepartments();
|
|
2730
|
-
}
|
|
2731
|
-
async function deleteDept(path,name){
|
|
2732
|
-
showConfirm('删除部门','确认删除部门 "'+escHtml(name)+'" 及其所有子部门吗?',async()=>{
|
|
2733
|
-
const r=await api(`/api/departments/${encodeURIComponent(path)}`,{method:'DELETE'});
|
|
2734
|
-
if(r.error){showToast(r.error,'danger');closeModal();return}closeModal();showToast('已删除','success');renderDepartments();
|
|
2735
|
-
});
|
|
2736
|
-
}
|
|
2737
|
-
async function showDeptDetail(path){
|
|
2738
|
-
try {
|
|
2739
|
-
const [dept,info,agents]=await Promise.all([api(`/api/departments/${encodeURIComponent(path)}`),api(`/api/departments/${encodeURIComponent(path)}/info`),api('/api/agents')]);
|
|
2740
|
-
if(dept.error){showToast(dept.error,'danger');return}
|
|
2741
|
-
// 刷新 allAgentsCache 确保最新
|
|
2742
|
-
allAgentsCache=Array.isArray(agents)?agents:allAgentsCache;
|
|
2743
|
-
// 描述和负责人从 dept 元数据读取(非 info API 的 dept.md 内容)
|
|
2744
|
-
const deptDesc=dept.description||'';
|
|
2745
|
-
const deptHead=dept.head||'';
|
|
2746
|
-
// 获取所有 agent 用于添加
|
|
2747
|
-
const allAgents=Array.isArray(allAgentsCache)?allAgentsCache:[];
|
|
2748
|
-
const currentAgents=Array.isArray(dept.agents)?dept.agents:[];
|
|
2749
|
-
// 构建可选 agent 列表(排除已添加的)
|
|
2750
|
-
const availAgents=allAgents.filter(a=>!currentAgents.includes(a.path||a.name));
|
|
2751
|
-
const agentOptions=availAgents.map(a=>`<option value="${escHtml(a.path||a.name)}">${escHtml((a.avatar_emoji||'🤖')+' '+(a.name||a.path))}</option>`).join('');
|
|
2752
|
-
// 已有 agent 列表 — 改为行列表形式,每行带删除按钮
|
|
2753
|
-
const agentListHtml=currentAgents.length?`<div style="display:flex;flex-direction:column;gap:6px">${currentAgents.map(aName=>{
|
|
2754
|
-
const aInfo=allAgents.find(a=>(a.path||a.name)===aName);
|
|
2755
|
-
const label=aInfo?((aInfo.avatar_emoji||'🤖')+' '+(aInfo.name||aName)):aName;
|
|
2756
|
-
const isHead=aName===deptHead;
|
|
2757
|
-
return `<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--surface2);border-radius:8px;border:1px solid var(--border)">
|
|
2758
|
-
<div style="display:flex;align-items:center;gap:8px">
|
|
2759
|
-
<span>${isHead?'👑':'🤖'}</span>
|
|
2760
|
-
<span style="font-weight:500">${escHtml(label)}</span>
|
|
2761
|
-
${isHead?'<span class="tag" style="font-size:11px">部长</span>':''}
|
|
2762
|
-
</div>
|
|
2763
|
-
<button class="btn btn-sm btn-danger" onclick="removeDeptAgent('${escHtml(path)}','${escHtml(aName)}')" style="padding:2px 10px;font-size:12px">删除</button>
|
|
2764
|
-
</div>`;
|
|
2765
|
-
}).join('')}</div>`:'<div style="padding:16px;text-align:center;color:var(--text3);font-size:13px;background:var(--surface2);border-radius:8px;border:1px dashed var(--border)">暂无成员</div>';
|
|
2766
|
-
|
|
2767
|
-
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal modal-wide" onclick="event.stopPropagation()">
|
|
2768
|
-
<h3>${escHtml(dept.emoji||'📁')} ${escHtml(dept.name)}</h3>
|
|
2769
|
-
<div class="form-row">
|
|
2770
|
-
<div class="form-group"><label>部门名称</label><input id="ddName" value="${escHtml(dept.name||'')}"></div>
|
|
2771
|
-
<div class="form-group"><label>Emoji</label><input id="ddEmoji" value="${escHtml(dept.emoji||'')}" maxlength="4"></div>
|
|
2772
|
-
</div>
|
|
2773
|
-
<div class="form-group"><label>描述</label><textarea id="ddDesc" rows="3">${escHtml(deptDesc)}</textarea></div>
|
|
2774
|
-
<div class="form-group"><label>负责人</label><select id="ddHead"><option value="">请选择...</option>${currentAgents.map(aName=>{const aInfo=allAgents.find(a=>(a.path||a.name)===aName);const label=aInfo?((aInfo.avatar_emoji||'🤖')+' '+(aInfo.name||aName)):aName;return '<option value="'+escHtml(aName)+'"'+(aName===deptHead?' selected':'')+'>'+escHtml(label)+'</option>'}).join('')}${availAgents.map(a=>'<option value="'+escHtml(a.path||a.name)+'"'+((a.path||a.name)===deptHead?' selected':'')+'>'+escHtml((a.avatar_emoji||'🤖')+' '+(a.name||a.path))+'</option>').join('')}</select></div>
|
|
2775
|
-
<hr style="border:none;border-top:1px solid var(--border);margin:20px 0">
|
|
2776
|
-
<h4>🤖 部门成员 <span class="badge badge-blue">${currentAgents.length}</span></h4>
|
|
2777
|
-
<div id="deptAgentList" style="margin:8px 0">${agentListHtml}</div>
|
|
2778
|
-
${agentOptions?`<div class="flex gap-8 mt-8"><select id="ddAddAgent" style="flex:1"><option value="">选择 Agent 添加...</option>${agentOptions}</select><button class="btn btn-sm btn-primary" onclick="addDeptAgent('${escHtml(path)}')">添加</button></div>`:'<div style="font-size:12px;color:var(--text3);margin-top:4px">所有 Agent 都已添加</div>'}
|
|
2779
|
-
<hr style="border:none;border-top:1px solid var(--border);margin:20px 0">
|
|
2780
|
-
<div class="flex gap-8">
|
|
2781
|
-
<button class="btn btn-ghost" onclick="loadDeptKB('${escHtml(path)}')">查看知识库</button>
|
|
2782
|
-
</div>
|
|
2783
|
-
<div id="deptKBArea" style="margin-top:16px"></div>
|
|
2784
|
-
<hr style="border:none;border-top:1px solid var(--border);margin:20px 0">
|
|
2785
|
-
<div class="flex gap-8" style="justify-content:flex-end">
|
|
2786
|
-
<button class="btn btn-primary" onclick="saveDeptInfo('${escHtml(path)}')">保存</button>
|
|
2787
|
-
<button class="btn btn-ghost" onclick="closeModal()">关闭</button>
|
|
2788
|
-
</div>
|
|
2789
|
-
</div></div>`;
|
|
2790
|
-
} catch(e) {
|
|
2791
|
-
console.error('showDeptDetail error:', e);
|
|
2792
|
-
showToast('加载部门详情失败: '+e.message, 'danger');
|
|
2793
|
-
}
|
|
2794
|
-
}
|
|
2795
|
-
async function addDeptAgent(path){
|
|
2796
|
-
const sel=document.getElementById('ddAddAgent');
|
|
2797
|
-
const btn=sel?sel.nextElementSibling:null;
|
|
2798
|
-
if(!sel||!sel.value){showToast('请选择要添加的 Agent','danger');return}
|
|
2799
|
-
if(sel.disabled||btn?.disabled)return;
|
|
2800
|
-
if(sel)sel.disabled=true;if(btn)btn.disabled=true;
|
|
2801
|
-
try{
|
|
2802
|
-
const r=await api(`/api/departments/${encodeURIComponent(path)}/agents`,{method:'PUT',body:JSON.stringify({agents:[sel.value],action:'add'})});
|
|
2803
|
-
if(r.error||r.ok===false){showToast(r.error||r.message||'添加失败','danger');return}
|
|
2804
|
-
showToast('已添加','success');_deptTreeNeedsRefresh=true;showDeptDetail(path);
|
|
2805
|
-
}finally{if(sel)sel.disabled=false;if(btn)btn.disabled=false}
|
|
2806
|
-
}
|
|
2807
|
-
async function removeDeptAgent(path,agentName){
|
|
2808
|
-
const r=await api(`/api/departments/${encodeURIComponent(path)}/agents`,{method:'PUT',body:JSON.stringify({agents:[agentName],action:'remove'})});
|
|
2809
|
-
if(r.error||r.ok===false){showToast(r.error||r.message||'移除失败','danger');return}
|
|
2810
|
-
showToast('已移除','success');_deptTreeNeedsRefresh=true;showDeptDetail(path);
|
|
2811
|
-
}
|
|
2812
|
-
async function saveDeptInfo(path){
|
|
2813
|
-
// 同时保存名称、emoji 和元数据(描述、负责人)
|
|
2814
|
-
const [metaRes,deptRes]=await Promise.all([
|
|
2815
|
-
api(`/api/departments/${encodeURIComponent(path)}/info`,{method:'PUT',body:JSON.stringify({description:$('ddDesc').value,head_name:$('ddHead').value})}),
|
|
2816
|
-
api(`/api/departments/${encodeURIComponent(path)}`,{method:'PUT',body:JSON.stringify({name:$('ddName').value,emoji:$('ddEmoji').value})})
|
|
2817
|
-
]);
|
|
2818
|
-
if(metaRes.error){showToast(metaRes.error,'danger');return}
|
|
2819
|
-
if(deptRes.error){showToast(deptRes.error,'danger');return}
|
|
2820
|
-
showToast('已保存','success');
|
|
2821
|
-
renderDepartments();
|
|
2822
|
-
}
|
|
2823
|
-
async function loadDeptKB(path){
|
|
2824
|
-
const files=await api(`/api/departments/${encodeURIComponent(path)}/knowledge`);
|
|
2825
|
-
const el=document.getElementById('deptKBArea');if(!el)return;
|
|
2826
|
-
let html='<h4 style="margin-bottom:8px">部门知识库</h4>';
|
|
2827
|
-
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>';
|
|
2828
|
-
if(!files||!files.length){html+='<div class="empty" style="margin-top:12px">暂无知识库文件</div>';el.innerHTML=html;return}
|
|
2829
|
-
html+='<div class="table-wrap"><table><tr><th>文件</th><th>操作</th></tr>';
|
|
2830
|
-
for(const f of files){
|
|
2831
|
-
html+=`<tr><td>${escHtml(f.name||f.path)}</td>
|
|
2832
|
-
<td><button class="btn btn-sm btn-ghost" onclick="viewDeptKBFile('${escHtml(path)}','${escHtml(f.path||f.name)}')">查看</button>
|
|
2833
|
-
<button class="btn btn-sm btn-danger" onclick="deleteDeptKBFile('${escHtml(path)}','${escHtml(f.path||f.name)}')">删除</button></td></tr>`;
|
|
2834
|
-
}
|
|
2835
|
-
html+='</table></div>';el.innerHTML=html;
|
|
2836
|
-
}
|
|
2837
|
-
async function uploadDeptKB(path,folderMode){
|
|
2838
|
-
const input=document.createElement('input');input.type='file';input.multiple=true;
|
|
2839
|
-
if(folderMode){input.webkitdirectory=true;}
|
|
2840
|
-
input.onchange=async()=>{
|
|
2841
|
-
const fd=new FormData();
|
|
2842
|
-
for(const f of input.files){
|
|
2843
|
-
if(f.webkitRelativePath){fd.append('files',f,{headers:{'X-File-Path':f.webkitRelativePath}});}
|
|
2844
|
-
else{fd.append('files',f);}
|
|
2845
|
-
}
|
|
2846
|
-
showToast('正在上传...','info');
|
|
2847
|
-
const r=await fetch(API+`/api/departments/${encodeURIComponent(path)}/knowledge/upload`,{method:'POST',body:fd});
|
|
2848
|
-
const data=await r.json();
|
|
2849
|
-
if(data.error){showToast(data.error,'danger');return}
|
|
2850
|
-
const total=data.results?data.results.length:0;
|
|
2851
|
-
const okCount=data.results?data.results.filter(x=>x.ok).length:0;
|
|
2852
|
-
showToast(`上传完成: ${okCount}/${total} 文件成功`,okCount===total?'success':'warning');
|
|
2853
|
-
loadDeptKB(path);
|
|
2854
|
-
};input.click();
|
|
2855
|
-
}
|
|
2856
|
-
|
|
2857
|
-
// ========== System Config ==========
|
|
2858
|
-
async function renderSystem(){
|
|
2859
|
-
$('content').innerHTML=`
|
|
2860
|
-
<div class="card">
|
|
2861
|
-
<h3>🕐 时区设置</h3>
|
|
2862
|
-
<p style="font-size:13px;color:var(--text2);margin-bottom:12px">设置系统时区,影响提示词中的当前时间、记忆时间戳、日志时间等所有时间显示</p>
|
|
2863
|
-
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
|
2864
|
-
<select id="sysTimezone" style="flex:1;padding:8px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg2);color:var(--text);font-size:14px"></select>
|
|
2865
|
-
<button class="btn btn-primary" id="sysTzSaveBtn" onclick="sysSaveTimezone()">保存</button>
|
|
2866
|
-
</div>
|
|
2867
|
-
<div id="sysTzMsg"></div>
|
|
2868
|
-
</div>
|
|
2869
|
-
<div class="card">
|
|
2870
|
-
<h3>📤 导出备份</h3>
|
|
2871
|
-
<p style="font-size:13px;color:var(--text2);margin-bottom:12px">将当前完整配置导出为 JSON 文件,用于备份或迁移</p>
|
|
2872
|
-
<div class="flex gap-8">
|
|
2873
|
-
<button class="btn btn-ghost" id="sysExportSafe" onclick="sysExport(false)">导出(脱敏)</button>
|
|
2874
|
-
<button class="btn btn-danger" id="sysExportFull" onclick="sysExport(true)">导出(含密钥)</button>
|
|
2875
|
-
</div>
|
|
2876
|
-
<div id="sysExportMsg"></div>
|
|
2877
|
-
</div>
|
|
2878
|
-
<div class="card">
|
|
2879
|
-
<h3>📥 导入配置</h3>
|
|
2880
|
-
<p style="font-size:13px;color:var(--text2);margin-bottom:12px">从之前导出的 JSON 备份文件恢复配置</p>
|
|
2881
|
-
<div class="drop-zone" id="sysDropZone" onclick="document.getElementById('sysFileInput').click()" ondragover="event.preventDefault();this.classList.add('dragover')" ondragleave="this.classList.remove('dragover')" ondrop="sysHandleDrop(event)">
|
|
2882
|
-
<div style="font-size:28px;margin-bottom:8px;opacity:.5">📁</div>
|
|
2883
|
-
<div style="font-size:13px">点击选择文件或拖拽 JSON 到此处</div>
|
|
2884
|
-
<div style="font-size:11px;color:var(--text2);margin-top:4px">支持 myagent_config_*.json 格式</div>
|
|
2885
|
-
<input type="file" id="sysFileInput" accept=".json" onchange="sysHandleFile(this)" style="display:none">
|
|
2886
|
-
</div>
|
|
2887
|
-
<div style="display:flex;align-items:center;gap:8px;margin-top:8px;font-size:13px;color:var(--text2)">
|
|
2888
|
-
<input type="checkbox" id="sysOverwrite" style="width:16px;height:16px;accent-color:var(--primary)">
|
|
2889
|
-
<label for="sysOverwrite">完全覆盖当前配置(否则智能合并)</label>
|
|
2890
|
-
</div>
|
|
2891
|
-
<div id="sysImportMsg"></div>
|
|
2892
|
-
</div>
|
|
2893
|
-
<div class="card">
|
|
2894
|
-
<h3>👁️ 配置预览</h3>
|
|
2895
|
-
<div class="config-preview" id="sysConfigPreview">加载中...</div>
|
|
2896
|
-
</div>`;
|
|
2897
|
-
sysLoadTimezone();
|
|
2898
|
-
sysLoadPreview();
|
|
2899
|
-
}
|
|
2900
|
-
|
|
2901
|
-
const COMMON_TIMEZONES=['Asia/Shanghai','Asia/Kuala_Lumpur','Asia/Singapore','Asia/Tokyo','Asia/Seoul','Asia/Ho_Chi_Minh','Asia/Bangkok','Asia/Jakarta','Asia/Kolkata','Asia/Dubai','Europe/London','Europe/Paris','Europe/Berlin','Europe/Moscow','America/New_York','America/Chicago','America/Denver','America/Los_Angeles','Australia/Sydney','Pacific/Auckland','UTC'];
|
|
2902
|
-
|
|
2903
|
-
async function sysLoadTimezone(){
|
|
2904
|
-
try{
|
|
2905
|
-
const r=await api('/api/config/get',{method:'POST',body:JSON.stringify({key:'timezone'})});
|
|
2906
|
-
const sel=$('sysTimezone');
|
|
2907
|
-
sel.innerHTML=COMMON_TIMEZONES.map(tz=>`<option value="${tz}"${tz===(r.value||'Asia/Shanghai')?' selected':''}>${tz}</option>`).join('');
|
|
2908
|
-
}catch(e){console.error('Load timezone failed:',e);}
|
|
2909
|
-
}
|
|
2910
|
-
|
|
2911
|
-
async function sysSaveTimezone(){
|
|
2912
|
-
const btn=$('sysTzSaveBtn');btn.disabled=true;
|
|
2913
|
-
sysSetMsg('sysTzMsg','loading','正在保存时区...');
|
|
2914
|
-
try{
|
|
2915
|
-
const tz=$('sysTimezone').value;
|
|
2916
|
-
const r=await api('/api/config/set',{method:'POST',body:JSON.stringify({key:'timezone',value:tz})});
|
|
2917
|
-
if(r.ok){sysSetMsg('sysTzMsg','success','✅ 时区已保存为 '+tz);showToast('时区已更新为 '+tz,'success');}
|
|
2918
|
-
else{sysSetMsg('sysTzMsg','error','❌ 保存失败: '+(r.error||'未知错误'));}
|
|
2919
|
-
}catch(e){sysSetMsg('sysTzMsg','error','❌ 保存失败: '+e.message);}
|
|
2920
|
-
btn.disabled=false;
|
|
2921
|
-
}
|
|
2922
|
-
|
|
2923
|
-
function sysSetMsg(id,type,msg){
|
|
2924
|
-
const el=$(id);if(!el)return;
|
|
2925
|
-
el.className='status-msg '+type;el.textContent=msg;
|
|
2926
|
-
if(type!=='loading')setTimeout(()=>{if(el.className.includes(type))el.className='status-msg';},8000);
|
|
2927
|
-
}
|
|
2928
|
-
|
|
2929
|
-
async function sysExport(includeSecrets){
|
|
2930
|
-
const btnId=includeSecrets?'sysExportFull':'sysExportSafe';
|
|
2931
|
-
const btn=$(btnId);btn.disabled=true;
|
|
2932
|
-
sysSetMsg('sysExportMsg','loading','正在导出配置...');
|
|
2933
|
-
try{
|
|
2934
|
-
const resp=await fetch(API+'/api/config/export',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({include_secrets:includeSecrets})});
|
|
2935
|
-
if(!resp.ok){const err=await resp.json();throw new Error(err.error||'导出失败');}
|
|
2936
|
-
const blob=await resp.blob();
|
|
2937
|
-
const disposition=resp.headers.get('Content-Disposition')||'';
|
|
2938
|
-
const match=disposition.match(/filename="([^"]+)"/);
|
|
2939
|
-
const filename=match?match[1]:'myagent_config.json';
|
|
2940
|
-
const url=URL.createObjectURL(blob);const a=document.createElement('a');
|
|
2941
|
-
a.href=url;a.download=filename;document.body.appendChild(a);a.click();
|
|
2942
|
-
document.body.removeChild(a);URL.revokeObjectURL(url);
|
|
2943
|
-
sysSetMsg('sysExportMsg','success','✅ 配置已导出: '+filename);showToast('配置已导出','success');
|
|
2944
|
-
}catch(e){sysSetMsg('sysExportMsg','error','❌ 导出失败: '+e.message);}
|
|
2945
|
-
btn.disabled=false;
|
|
2946
|
-
}
|
|
2947
|
-
|
|
2948
|
-
function sysHandleDrop(event){
|
|
2949
|
-
event.preventDefault();event.stopPropagation();
|
|
2950
|
-
$('sysDropZone').classList.remove('dragover');
|
|
2951
|
-
const files=event.dataTransfer.files;
|
|
2952
|
-
if(files.length>0&&files[0].name.endsWith('.json')){sysImportFile(files[0]);}
|
|
2953
|
-
else{showToast('请拖入 JSON 文件','danger');}
|
|
2954
|
-
}
|
|
2955
|
-
function sysHandleFile(input){if(input.files.length>0)sysImportFile(input.files[0]);input.value='';}
|
|
2956
|
-
|
|
2957
|
-
async function sysImportFile(file){
|
|
2958
|
-
const overwrite=$('sysOverwrite').checked;
|
|
2959
|
-
sysSetMsg('sysImportMsg','loading','正在读取 '+file.name+'...');
|
|
2960
|
-
try{
|
|
2961
|
-
const text=await file.text();
|
|
2962
|
-
let data;try{data=JSON.parse(text);}catch(e){sysSetMsg('sysImportMsg','error','文件不是有效的 JSON');return;}
|
|
2963
|
-
sysSetMsg('sysImportMsg','loading','正在导入配置...');
|
|
2964
|
-
if(overwrite)data._overwrite=true;
|
|
2965
|
-
const r=await api('/api/config/import',{method:'POST',body:JSON.stringify(data)});
|
|
2966
|
-
if(r.ok){
|
|
2967
|
-
const changed=r.changed_keys||[];
|
|
2968
|
-
let msg='✅ '+r.message;
|
|
2969
|
-
if(changed.length>0)msg+=' ('+changed.slice(0,5).join(', ')+(changed.length>5?'...':'')+')';
|
|
2970
|
-
sysSetMsg('sysImportMsg','success',msg);showToast(r.message,'success');sysLoadPreview();
|
|
2971
|
-
}else{sysSetMsg('sysImportMsg','error','❌ 导入失败: '+(r.message||'未知错误'));}
|
|
2972
|
-
}catch(e){sysSetMsg('sysImportMsg','error','❌ 导入失败: '+e.message);}
|
|
2973
|
-
}
|
|
2974
|
-
|
|
2975
|
-
async function sysLoadPreview(){
|
|
2976
|
-
const el=$('sysConfigPreview');if(!el)return;
|
|
2977
|
-
try{
|
|
2978
|
-
const cfg=await api('/api/config');const c=cfg.config||cfg;
|
|
2979
|
-
const lines=[];
|
|
2980
|
-
if(c.llm){
|
|
2981
|
-
lines.push('<span class="key">LLM:</span> '+escHtml(c.llm.provider+'/'+c.llm.model));
|
|
2982
|
-
lines.push('<span class="key">Base URL:</span> '+escHtml(c.llm.base_url||'default'));
|
|
2983
|
-
lines.push('<span class="key">Temperature:</span> '+c.llm.temperature+' | Max Tokens: '+c.llm.max_tokens);
|
|
2984
|
-
}
|
|
2985
|
-
if(c.executor){
|
|
2986
|
-
lines.push('<span class="key">执行引擎:</span> '+escHtml(c.executor.execution_mode||'local')+' | Timeout: '+(c.executor.timeout||300)+'s');
|
|
2987
|
-
}
|
|
2988
|
-
if(c.memory){
|
|
2989
|
-
lines.push('<span class="key">记忆:</span> 会话 '+(c.memory.max_session||50)+' 轮 | 自动总结: '+(c.memory.auto_summarize?'开':'关'));
|
|
2990
|
-
}
|
|
2991
|
-
if(c.agent){
|
|
2992
|
-
lines.push('<span class="key">Agent:</span> 最大迭代 '+(c.agent.max_iterations||30)+' | 并行 '+(c.agent.max_parallel||3));
|
|
2993
|
-
}
|
|
2994
|
-
if(c.chat_platforms&&c.chat_platforms.length>0){
|
|
2995
|
-
const platforms=c.chat_platforms.filter(p=>p.enabled).map(p=>p.platform);
|
|
2996
|
-
if(platforms.length>0)lines.push('<span class="key">聊天平台:</span> '+escHtml(platforms.join(', ')));
|
|
2997
|
-
}
|
|
2998
|
-
if(c.log_level)lines.push('<span class="key">日志级别:</span> '+escHtml(c.log_level));
|
|
2999
|
-
if(cfg._meta){lines.push('');lines.push('<span class="key">备份信息:</span> '+escHtml(cfg._meta.exported_at||'N/A'));}
|
|
3000
|
-
el.innerHTML=lines.join('\n')||'暂无配置信息';
|
|
3001
|
-
}catch(e){el.textContent='加载配置失败: '+e.message;}
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
// Init
|
|
3005
|
-
(function(){
|
|
3006
|
-
var hash=window.location.hash.slice(1)||'';
|
|
3007
|
-
var page=hash?hash.split('~')[0]:'';
|
|
3008
|
-
var sub=hash?hash.slice(page.length+1):'';
|
|
3009
|
-
// 兼容旧 URL query param ?page=xxx
|
|
3010
|
-
if(!page){
|
|
3011
|
-
var params=new URLSearchParams(window.location.search);
|
|
3012
|
-
page=params.get('page')||'dashboard';
|
|
3013
|
-
}
|
|
3014
|
-
if(sub)_navSubState=sub;
|
|
3015
|
-
if(pages[page]){
|
|
3016
|
-
history.replaceState({page:page,sub:sub||null},'','#'+page+(sub?'~'+sub:''));
|
|
3017
|
-
showPage(page,false);
|
|
3018
|
-
}else{
|
|
3019
|
-
history.replaceState({page:'dashboard',sub:null},'','#dashboard');
|
|
3020
|
-
showPage('dashboard',false);
|
|
3021
|
-
}
|
|
3022
|
-
})();
|
|
3023
|
-
setInterval(()=>{api('/api/status').catch(()=>{})},30000);
|
|
3024
|
-
|
|
3025
|
-
/* ── 头像上传 + 裁剪 ── */
|
|
3026
|
-
var _cropState={prefix:'',dragging:false,sx:0,sy:0};
|
|
3027
|
-
function handleAvatarUpload(input,prefix){
|
|
3028
|
-
var file=input.files[0];if(!file)return;
|
|
3029
|
-
if(file.size>5*1024*1024){showToast('图片不能超过 5MB','danger');return}
|
|
3030
|
-
var reader=new FileReader();
|
|
3031
|
-
reader.onload=function(e){
|
|
3032
|
-
var img=$(prefix+'CropImg');img.src=e.target.result;
|
|
3033
|
-
$(prefix+'CropArea').style.display='block';
|
|
3034
|
-
$(prefix+'CropOverlay').style.display='none';
|
|
3035
|
-
_cropState.prefix=prefix;
|
|
3036
|
-
};
|
|
3037
|
-
reader.readAsDataURL(file);
|
|
3038
|
-
}
|
|
3039
|
-
/* [v1.18.8] 修复: e.preventDefault() 阻止浏览器原生图片拖拽 + 坐标统一用 clientX/Y */
|
|
3040
|
-
function startCrop(e,prefix){
|
|
3041
|
-
e.preventDefault();e.stopPropagation();
|
|
3042
|
-
var rect=e.target.getBoundingClientRect();
|
|
3043
|
-
var cx=e.clientX-rect.left,cy=e.clientY-rect.top;
|
|
3044
|
-
_cropState={prefix:prefix,dragging:true,sx:cx,sy:cy};
|
|
3045
|
-
var overlay=$(prefix+'CropOverlay');
|
|
3046
|
-
overlay.style.display='block';
|
|
3047
|
-
overlay.style.left=cx+'px';overlay.style.top=cy+'px';
|
|
3048
|
-
overlay.style.width='0';overlay.style.height='0';
|
|
3049
|
-
document.addEventListener('mousemove',doCrop);document.addEventListener('mouseup',endCrop);
|
|
3050
|
-
}
|
|
3051
|
-
/* [v1.18.8] 触摸设备支持 */
|
|
3052
|
-
function startCropTouch(e,prefix){
|
|
3053
|
-
e.preventDefault();e.stopPropagation();
|
|
3054
|
-
var t=e.touches[0];if(!t)return;
|
|
3055
|
-
var rect=e.target.getBoundingClientRect();
|
|
3056
|
-
var cx=t.clientX-rect.left,cy=t.clientY-rect.top;
|
|
3057
|
-
_cropState={prefix:prefix,dragging:true,sx:cx,sy:cy};
|
|
3058
|
-
var overlay=$(prefix+'CropOverlay');
|
|
3059
|
-
overlay.style.display='block';
|
|
3060
|
-
overlay.style.left=cx+'px';overlay.style.top=cy+'px';
|
|
3061
|
-
overlay.style.width='0';overlay.style.height='0';
|
|
3062
|
-
document.addEventListener('touchmove',doCropTouch,{passive:false});document.addEventListener('touchend',endCropTouch);
|
|
3063
|
-
}
|
|
3064
|
-
function doCropTouch(e){
|
|
3065
|
-
e.preventDefault();
|
|
3066
|
-
if(!_cropState.dragging||!e.touches[0])return;
|
|
3067
|
-
var t=e.touches[0];
|
|
3068
|
-
_doCropMove(t.clientX,t.clientY);
|
|
3069
|
-
}
|
|
3070
|
-
function endCropTouch(){
|
|
3071
|
-
_cropState.dragging=false;
|
|
3072
|
-
document.removeEventListener('touchmove',doCropTouch);document.removeEventListener('touchend',endCropTouch);
|
|
3073
|
-
}
|
|
3074
|
-
function doCrop(e){
|
|
3075
|
-
if(!_cropState.dragging)return;
|
|
3076
|
-
_doCropMove(e.clientX,e.clientY);
|
|
3077
|
-
}
|
|
3078
|
-
function _doCropMove(clientX,clientY){
|
|
3079
|
-
var img=$(_cropState.prefix+'CropImg'),rect=img.getBoundingClientRect();
|
|
3080
|
-
var overlay=$(_cropState.prefix+'CropOverlay');
|
|
3081
|
-
var cx=clientX-rect.left,cy=clientY-rect.top;
|
|
3082
|
-
var x1=Math.min(_cropState.sx,cx),y1=Math.min(_cropState.sy,cy);
|
|
3083
|
-
var x2=Math.max(_cropState.sx,cx),y2=Math.max(_cropState.sy,cy);
|
|
3084
|
-
x2=Math.min(x2,rect.width);y2=Math.min(y2,rect.height);
|
|
3085
|
-
x1=Math.max(x1,0);y1=Math.max(y1,0);
|
|
3086
|
-
var w=x2-x1,h=y2-y1;if(w<5||h<5)return;
|
|
3087
|
-
overlay.style.left=x1+'px';overlay.style.top=y1+'px';overlay.style.width=w+'px';overlay.style.height=h+'px';
|
|
3088
|
-
}
|
|
3089
|
-
function endCrop(){_cropState.dragging=false;document.removeEventListener('mousemove',doCrop);document.removeEventListener('mouseup',endCrop);}
|
|
3090
|
-
function cancelAvatarCrop(prefix){$(prefix+'CropArea').style.display='none';}
|
|
3091
|
-
function confirmAvatarCrop(prefix,agentPath){
|
|
3092
|
-
var img=$(prefix+'CropImg'),overlay=$(prefix+'CropOverlay');
|
|
3093
|
-
if(!img||!overlay){showToast('裁剪数据异常','danger');return}
|
|
3094
|
-
// [v1.18.7] 防止 clientWidth=0 导致 NaN
|
|
3095
|
-
var clientW=Math.max(img.clientWidth,1),clientH=Math.max(img.clientHeight,1);
|
|
3096
|
-
var naturalW=img.naturalWidth||clientW,naturalH=img.naturalHeight||clientH;
|
|
3097
|
-
var scale=naturalW/clientW;
|
|
3098
|
-
if(!isFinite(scale)||scale<=0)scale=1;
|
|
3099
|
-
var cx=Math.round(parseFloat(overlay.style.left||'0')*scale);
|
|
3100
|
-
var cy=Math.round(parseFloat(overlay.style.top||'0')*scale);
|
|
3101
|
-
var cw=Math.round(parseFloat(overlay.style.width||'0')*scale);
|
|
3102
|
-
var ch=Math.round(parseFloat(overlay.style.height||'0')*scale);
|
|
3103
|
-
if(cw<10||ch<10||isNaN(cx)||isNaN(cy)){showToast('请拖动选择裁剪区域','danger');return}
|
|
3104
|
-
var src=$(prefix+'CropImg').src;
|
|
3105
|
-
if(!src||!src.startsWith('data:image')){showToast('请先上传图片','danger');return}
|
|
3106
|
-
var blob=dataURItoBlob(src);
|
|
3107
|
-
if(!blob||blob.size===0){showToast('图片数据异常','danger');return}
|
|
3108
|
-
var formData=new FormData();
|
|
3109
|
-
formData.append('file',blob,'avatar.png');
|
|
3110
|
-
showToast('正在上传裁剪...','info');
|
|
3111
|
-
fetch('/api/agents/'+encodeURIComponent(agentPath)+'/avatar?crop_x='+cx+'&crop_y='+cy+'&crop_w='+cw+'&crop_h='+ch+'&size=128',{
|
|
3112
|
-
method:'POST',body:formData
|
|
3113
|
-
}).then(r=>r.json()).then(d=>{
|
|
3114
|
-
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');}
|
|
3115
|
-
else showToast(d.error||'上传失败','danger');
|
|
3116
|
-
}).catch(e=>showToast('上传失败: '+e,'danger'));
|
|
3117
|
-
}
|
|
3118
|
-
function dataURItoBlob(dataURI){
|
|
3119
|
-
if(!dataURI||typeof dataURI!=='string')return null;
|
|
3120
|
-
var parts=dataURI.split(',');
|
|
3121
|
-
if(parts.length<2)return null;
|
|
3122
|
-
var mimeMatch=parts[0].match(/:(.*?);/);
|
|
3123
|
-
var mime=mimeMatch?mimeMatch[1]:'image/png';
|
|
3124
|
-
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});}
|
|
3125
|
-
catch(e){return null}
|
|
3126
|
-
}
|
|
3127
|
-
async function removeAvatarImage(agentPath){
|
|
3128
|
-
var ad=await api('/api/agents/'+encodeURIComponent(agentPath));if(!ad||!ad.avatar_image)return;
|
|
3129
|
-
await api('/api/agents/'+encodeURIComponent(agentPath),{method:'PUT',body:JSON.stringify({avatar_image:''})});
|
|
3130
|
-
$('eaAvatarImage').value='';$('eaAvatarPreview').innerHTML=escHtml(ad.avatar_emoji||'🤖');showToast('已移除头像图片','success');
|
|
3131
|
-
}
|
|
3132
|
-
</script>
|
|
357
|
+
<script src="admin/admin-core.js"></script>
|
|
358
|
+
<script src="admin/admin-dashboard.js"></script>
|
|
359
|
+
<script src="admin/admin-agents.js"></script>
|
|
360
|
+
<script src="admin/admin-platforms.js"></script>
|
|
361
|
+
<script src="admin/admin-sessions.js"></script>
|
|
362
|
+
<script src="admin/admin-memory.js"></script>
|
|
363
|
+
<script src="admin/admin-permissions.js"></script>
|
|
364
|
+
<script src="admin/admin-llm.js"></script>
|
|
365
|
+
<script src="admin/admin-executor.js"></script>
|
|
366
|
+
<script src="admin/admin-skills.js"></script>
|
|
367
|
+
<script src="admin/admin-files.js"></script>
|
|
368
|
+
<script src="admin/admin-logs.js"></script>
|
|
369
|
+
<script src="admin/admin-tasks.js"></script>
|
|
370
|
+
<script src="admin/admin-org.js"></script>
|
|
371
|
+
<script src="admin/admin-system.js"></script>
|
|
3133
372
|
</body>
|
|
3134
373
|
</html>
|