myagent-ai 1.23.19 → 1.23.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/chatbot/whatsapp_bot.py +0 -0
  2. package/chatbot/whatsapp_bridge/bridge.mjs +0 -0
  3. package/chatbot/whatsapp_bridge/package.json +0 -0
  4. package/core/stt.py +0 -0
  5. package/core/tool_dispatcher.py +0 -0
  6. package/core/web_control.py +0 -0
  7. package/data/novnc/lib/base64.js +0 -0
  8. package/data/novnc/lib/crypto/aes.js +0 -0
  9. package/data/novnc/lib/crypto/bigint.js +0 -0
  10. package/data/novnc/lib/crypto/crypto.js +0 -0
  11. package/data/novnc/lib/crypto/des.js +0 -0
  12. package/data/novnc/lib/crypto/dh.js +0 -0
  13. package/data/novnc/lib/crypto/md5.js +0 -0
  14. package/data/novnc/lib/crypto/rsa.js +0 -0
  15. package/data/novnc/lib/decoders/copyrect.js +0 -0
  16. package/data/novnc/lib/decoders/hextile.js +0 -0
  17. package/data/novnc/lib/decoders/jpeg.js +0 -0
  18. package/data/novnc/lib/decoders/raw.js +0 -0
  19. package/data/novnc/lib/decoders/rre.js +0 -0
  20. package/data/novnc/lib/decoders/tight.js +0 -0
  21. package/data/novnc/lib/decoders/tightpng.js +0 -0
  22. package/data/novnc/lib/decoders/zrle.js +0 -0
  23. package/data/novnc/lib/deflator.js +0 -0
  24. package/data/novnc/lib/display.js +0 -0
  25. package/data/novnc/lib/encodings.js +0 -0
  26. package/data/novnc/lib/inflator.js +0 -0
  27. package/data/novnc/lib/input/domkeytable.js +0 -0
  28. package/data/novnc/lib/input/fixedkeys.js +0 -0
  29. package/data/novnc/lib/input/gesturehandler.js +0 -0
  30. package/data/novnc/lib/input/keyboard.js +0 -0
  31. package/data/novnc/lib/input/keysym.js +0 -0
  32. package/data/novnc/lib/input/keysymdef.js +0 -0
  33. package/data/novnc/lib/input/util.js +0 -0
  34. package/data/novnc/lib/input/vkeys.js +0 -0
  35. package/data/novnc/lib/input/xtscancodes.js +0 -0
  36. package/data/novnc/lib/ra2.js +0 -0
  37. package/data/novnc/lib/rfb.js +0 -0
  38. package/data/novnc/lib/util/browser.js +0 -0
  39. package/data/novnc/lib/util/cursor.js +0 -0
  40. package/data/novnc/lib/util/element.js +0 -0
  41. package/data/novnc/lib/util/events.js +0 -0
  42. package/data/novnc/lib/util/eventtarget.js +0 -0
  43. package/data/novnc/lib/util/int.js +0 -0
  44. package/data/novnc/lib/util/logging.js +0 -0
  45. package/data/novnc/lib/util/strings.js +0 -0
  46. package/data/novnc/lib/vendor/pako/lib/utils/common.js +0 -0
  47. package/data/novnc/lib/vendor/pako/lib/zlib/adler32.js +0 -0
  48. package/data/novnc/lib/vendor/pako/lib/zlib/constants.js +0 -0
  49. package/data/novnc/lib/vendor/pako/lib/zlib/crc32.js +0 -0
  50. package/data/novnc/lib/vendor/pako/lib/zlib/deflate.js +0 -0
  51. package/data/novnc/lib/vendor/pako/lib/zlib/gzheader.js +0 -0
  52. package/data/novnc/lib/vendor/pako/lib/zlib/inffast.js +0 -0
  53. package/data/novnc/lib/vendor/pako/lib/zlib/inflate.js +0 -0
  54. package/data/novnc/lib/vendor/pako/lib/zlib/inftrees.js +0 -0
  55. package/data/novnc/lib/vendor/pako/lib/zlib/messages.js +0 -0
  56. package/data/novnc/lib/vendor/pako/lib/zlib/trees.js +0 -0
  57. package/data/novnc/lib/vendor/pako/lib/zlib/zstream.js +0 -0
  58. package/data/novnc/lib/websock.js +0 -0
  59. package/package.json +1 -1
  60. package/scripts/cli.py +0 -0
  61. package/skills/agent_tool_skill.py +0 -0
  62. package/web/ui/admin/admin-agents.js +570 -0
  63. package/web/ui/admin/admin-core.js +322 -0
  64. package/web/ui/admin/admin-dashboard.js +153 -0
  65. package/web/ui/admin/admin-executor.js +67 -0
  66. package/web/ui/admin/admin-files.js +81 -0
  67. package/web/ui/admin/admin-llm.js +190 -0
  68. package/web/ui/admin/admin-logs.js +69 -0
  69. package/web/ui/admin/admin-memory.js +91 -0
  70. package/web/ui/admin/admin-org.js +283 -0
  71. package/web/ui/admin/admin-permissions.js +147 -0
  72. package/web/ui/admin/admin-platforms.js +221 -0
  73. package/web/ui/admin/admin-sessions.js +182 -0
  74. package/web/ui/admin/admin-skills.js +217 -0
  75. package/web/ui/admin/admin-system.js +154 -0
  76. package/web/ui/admin/admin-tasks.js +131 -0
  77. package/web/ui/chat/chat_main.js +292 -304
  78. package/web/ui/chat/flow_engine.js +96 -41
  79. package/web/ui/index.html +15 -2776
@@ -0,0 +1,221 @@
1
+ // ========== Platforms ==========
2
+ (function(){
3
+ try {
4
+ // ========== Platforms ==========
5
+ async function renderPlatforms(){
6
+ const ps=await api('/api/platforms');
7
+ if(ps.error){$('content').innerHTML='<div class="empty" style="color:var(--danger)">加载失败: '+escHtml(ps.error)+'</div>';return}
8
+ const icons={telegram:'📱',discord:'🎮',feishu:'🐦',qq:'🐧',wechat:'💚',whatsapp:'💬'};
9
+ let html=`<div class="flex justify-between items-center mb-16">
10
+ <div style="color:var(--text2);font-size:13px">共 ${ps.length} 个平台实例</div>
11
+ <button class="btn btn-primary" onclick="showAddPlatformModal()">+ 添加平台</button></div>`;
12
+ if(!ps.length){html+='<div class="empty">暂无聊天平台配置,点击上方按钮添加</div>';$('content').innerHTML=html;return}
13
+ html+='<div class="grid">';
14
+ for(const p of ps){
15
+ const pid=p.id||p.platform;
16
+ const displayName=p.display_name||(icons[p.platform]||'📡')+' '+p.platform;
17
+ const bindInfo=p.bind_agent?'绑定: '+escHtml(p.bind_agent):(p.bind_agents&&p.bind_agents.length?'绑定: '+p.bind_agents.map(a=>escHtml(a)).join(', '):'');
18
+ const _supportsQR=['telegram','wechat','whatsapp'].includes(p.platform);
19
+ html+=`<div class="card"><div class="flex justify-between items-center">
20
+ <h3 style="color:var(--text)">${icons[p.platform]||'📡'} ${escHtml(p.display_name||p.platform)}</h3>
21
+ <span class="badge ${p.enabled?'badge-green':'badge-red'}">${p.enabled?'已启用':'未启用'}</span>
22
+ </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>
23
+ ${bindInfo?'<p style="font-size:12px;color:var(--text3);margin-top:4px">'+bindInfo+'</p>':''}
24
+ <div class="mt-8 flex gap-8">
25
+ <button class="btn btn-sm ${p.enabled?'btn-danger':'btn-success'}" onclick="togglePlatform('${escHtml(pid)}',${!p.enabled})">${p.enabled?'停用':'启用'}</button>
26
+ <button class="btn btn-sm btn-ghost" onclick="showEditPlatformModal('${escHtml(pid)}')">配置</button>
27
+ ${_supportsQR?'<button class="btn btn-sm btn-primary" onclick="showPlatformQRModal(\''+escHtml(pid)+'\')">📱 QR绑定</button>':''}
28
+ </div></div>`;
29
+ }
30
+ html+='</div>';$('content').innerHTML=html;
31
+ }
32
+ async function togglePlatform(id,enable){
33
+ const r=await api(`/api/platforms/${id}`,{method:'PUT',body:JSON.stringify({enabled})});
34
+ if(r.error){showToast('操作失败: '+r.error,'danger');return}
35
+ showToast(enable?'平台已启用':'平台已停用',enable?'success':'info');
36
+ renderPlatforms();
37
+ }
38
+ function showAddPlatformModal(){
39
+ $('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:520px">
40
+ <h3>+ 添加聊天平台</h3>
41
+ <div class="form-group"><label>平台类型</label><select id="apType" onchange="onPlatformTypeChange()">
42
+ <option value="telegram">Telegram</option><option value="discord">Discord</option>
43
+ <option value="feishu">飞书</option><option value="qq">QQ</option><option value="wechat">微信</option>
44
+ <option value="whatsapp">WhatsApp</option>
45
+ </select></div>
46
+ <div class="form-group"><label>Token / Access Token</label><input id="apToken" placeholder="Bot Token 或 Access Token"></div>
47
+ <div id="apAppIdGroup" class="form-group"><label>App ID</label><input id="apAppId" placeholder="应用ID (可选)"></div>
48
+ <div id="apWebhookGroup" class="form-group"><label>Webhook URL</label><input id="apWebhook" placeholder="Webhook 地址 (可选)"></div>
49
+ <div class="form-group"><label>绑定 Agent</label>
50
+ <div class="flex gap-8">
51
+ <input id="apBindAgent" placeholder="手输 Agent ID (可选)" style="flex:1">
52
+ <button class="btn btn-sm btn-ghost" onclick="showAgentSelectorForPlatform('apBindAgent','apBindAgents')">选择 Agent</button>
53
+ </div>
54
+ <div id="apBindAgents" style="font-size:11px;color:var(--text3);margin-top:4px"></div>
55
+ <div class="hint">手输 Agent ID 或点击"选择 Agent"从部门中选择。支持绑定多个 Agent。</div>
56
+ </div>
57
+ <div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="doAddPlatform()">添加</button><button class="btn btn-ghost" onclick="closeModal()">取消</button></div>
58
+ </div></div>`;
59
+ }
60
+ function onPlatformTypeChange(){
61
+ var t=$('apType').value;
62
+ var showAppId=(t==='feishu'||t==='whatsapp');
63
+ var showWebhook=(t==='whatsapp');
64
+ if($('apAppIdGroup'))$('apAppIdGroup').style.display=showAppId?'':'none';
65
+ if($('apWebhookGroup'))$('apWebhookGroup').style.display=showWebhook?'':'none';
66
+ }
67
+ async function doAddPlatform(){
68
+ var bindAgent=$('apBindAgent').value.trim();
69
+ var bindAgents=[];
70
+ var selectedEls=document.querySelectorAll('#apBindAgents .selected-agent-tag');
71
+ selectedEls.forEach(function(el){var v=el.getAttribute('data-agent');if(v)bindAgents.push(v);});
72
+ if(bindAgent)bindAgents.unshift(bindAgent);
73
+ bindAgents=[...new Set(bindAgents)];
74
+ 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})});
75
+ if(r.error){showToast(r.error,'danger');return}closeModal();showToast('已添加: '+(r.display_name||r.platform),'success');renderPlatforms();
76
+ }
77
+ async function showEditPlatformModal(id){
78
+ const p=await api(`/api/platforms/${id}`);
79
+ if(p.error){showToast(p.error,'danger');return}
80
+ const pid=p.id||id;
81
+ var bindAgentVal=p.bind_agent||'';
82
+ var bindAgentsArr=p.bind_agents||[];
83
+ $('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:520px">
84
+ <h3>配置 ${escHtml(p.display_name||p.platform)}</h3>
85
+ <div class="form-group"><label>显示名称</label><input id="epDisplayName" value="${escHtml(p.display_name||'')}" placeholder="留空自动生成"></div>
86
+ <div class="form-group"><label>Token / Access Token</label><input id="epToken" value="${escHtml(p.token||'')}" placeholder="Bot Token"></div>
87
+ <div id="epAppIdGroup" class="form-group"><label>App ID</label><input id="epAppId" value="${escHtml(p.app_id||'')}" placeholder="应用ID"></div>
88
+ <div class="form-group"><label>App Secret</label><input id="epSecret" type="password" placeholder="${p.has_secret?'已配置(留空不修改)':'未配置'}"></div>
89
+ <div id="epWebhookGroup" class="form-group"><label>Webhook URL</label><input id="epWebhook" value="${escHtml(p.webhook_url||'')}" placeholder="Webhook 地址"></div>
90
+ <div class="form-group"><label>允许用户 (逗号分隔)</label><input id="epUsers" value="${escHtml((p.allowed_users||[]).join(','))}" placeholder="user1,user2"></div>
91
+ <div class="form-group"><label>绑定 Agent</label>
92
+ <div class="flex gap-8">
93
+ <input id="epBindAgent" value="${escHtml(bindAgentVal)}" placeholder="手输 Agent ID (可选)" style="flex:1">
94
+ <button class="btn btn-sm btn-ghost" onclick="showAgentSelectorForPlatform('epBindAgent','epBindAgents')">选择 Agent</button>
95
+ </div>
96
+ <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>
97
+ <div class="hint">手输 Agent ID 或点击"选择 Agent"从部门中选择。一个平台可以绑定多个 Agent。</div>
98
+ </div>
99
+ <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>
100
+ </div></div>`;
101
+ }
102
+ async function doSavePlatform(id){
103
+ var bindAgent=$('epBindAgent').value.trim();
104
+ var bindAgents=[];
105
+ var selectedEls=document.querySelectorAll('#epBindAgents .selected-agent-tag');
106
+ selectedEls.forEach(function(el){var v=el.getAttribute('data-agent');if(v)bindAgents.push(v);});
107
+ if(bindAgent)bindAgents.unshift(bindAgent);
108
+ bindAgents=[...new Set(bindAgents)];
109
+ 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})});
110
+ if(r.error){showToast(r.error,'danger');return}closeModal();showToast('已保存','success');renderPlatforms();
111
+ }
112
+ async function doDeletePlatform(id){
113
+ showConfirm('删除平台','确认删除该平台实例吗?',async()=>{
114
+ const r=await api(`/api/platforms/${id}`,{method:'DELETE'});if(r.error){showToast(r.error,'danger');closeModal();return}closeModal();showToast('已删除','success');renderPlatforms();
115
+ });
116
+ }
117
+
118
+ // [v1.20.3] QR 码绑定模态框
119
+ let _qrPollTimer=null;
120
+ async function showPlatformQRModal(platformId){
121
+ if(_qrPollTimer){clearInterval(_qrPollTimer);_qrPollTimer=null}
122
+ $('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>';
123
+ var r=await api('/api/platforms/'+encodeURIComponent(platformId)+'/qr',{method:'POST'});
124
+ if(r.error){$('qrStatus').textContent='错误: '+escHtml(r.error);return}
125
+ if(r.qr_code){
126
+ $('qrCodeImg').innerHTML='<img src="data:image/png;base64,'+r.qr_code+'" style="width:256px;height:256px;border-radius:8px" alt="QR Code">';
127
+ $('qrStatus').innerHTML='<span style="color:var(--warn)">⏳ 等待扫码...</span>';
128
+ $('qrActions').style.display='block';
129
+ }else if(r.status==='connected'){
130
+ $('qrStatus').innerHTML='<span style="color:var(--success)">✅ 已连接</span>';
131
+ $('qrCodeImg').innerHTML='<div style="color:var(--success);font-size:24px;padding:40px">✅<br><span style="font-size:14px">已成功连接</span></div>';
132
+ }else{$('qrStatus').textContent=r.error||'QR 码生成失败,请检查平台配置';}
133
+ _qrPollTimer=setInterval(async()=>{
134
+ try{
135
+ var s=await api('/api/platforms/'+encodeURIComponent(platformId)+'/qr');
136
+ if(s.status==='connected'){
137
+ $('qrStatus').innerHTML='<span style="color:var(--success)">✅ 已连接</span>';
138
+ $('qrCodeImg').innerHTML='<div style="color:var(--success);font-size:24px;padding:40px">✅<br><span style="font-size:14px">已成功连接</span></div>';
139
+ $('qrActions').style.display='none';
140
+ clearInterval(_qrPollTimer);_qrPollTimer=null;
141
+ showToast('QR 绑定成功!','success');renderPlatforms();
142
+ }else if(s.status==='waiting_scan'&&s.qr_code){
143
+ $('qrCodeImg').innerHTML='<img src="data:image/png;base64,'+s.qr_code+'" style="width:256px;height:256px;border-radius:8px" alt="QR Code">';
144
+ $('qrStatus').innerHTML='<span style="color:var(--warn)">⏳ 等待扫码...</span>';
145
+ $('qrActions').style.display='block';
146
+ }else if(s.status==='disconnected'){
147
+ $('qrStatus').innerHTML='<span style="color:var(--danger)">❌ 已断开</span>';
148
+ $('qrActions').style.display='block';
149
+ }
150
+ }catch(e){}
151
+ },3000);
152
+ }
153
+ async function requestNewQR(platformId){
154
+ $('qrStatus').textContent='正在刷新 QR 码...';
155
+ $('qrCodeImg').innerHTML='加载中...';
156
+ var r=await api('/api/platforms/'+encodeURIComponent(platformId)+'/qr',{method:'POST'});
157
+ if(r.error){$('qrStatus').textContent='错误: '+escHtml(r.error);return}
158
+ if(r.qr_code){
159
+ $('qrCodeImg').innerHTML='<img src="data:image/png;base64,'+r.qr_code+'" style="width:256px;height:256px;border-radius:8px" alt="QR Code">';
160
+ $('qrStatus').innerHTML='<span style="color:var(--warn)">⏳ 等待扫码...</span>';
161
+ }
162
+ }
163
+
164
+ // Agent 选择器(从部门树选择 Agent)
165
+ async function showAgentSelectorForPlatform(inputId,containerId){
166
+ try{
167
+ var agents=await api('/api/agents');
168
+ if(!Array.isArray(agents)){showToast('加载 Agent 列表失败','danger');return}
169
+ var agentList=agents.filter(function(a){return!a.system&&a.path!=='default'});
170
+ var deptMap={};
171
+ agentList.forEach(function(a){
172
+ var dept=a.department||'未分组';
173
+ if(!deptMap[dept])deptMap[dept]=[];
174
+ deptMap[dept].push(a);
175
+ });
176
+ var html='<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:480px;max-height:70vh;overflow-y:auto">';
177
+ html+='<h3>选择 Agent</h3>';
178
+ for(var dept in deptMap){
179
+ html+='<div style="margin-bottom:12px"><div style="font-size:13px;font-weight:600;color:var(--text2);margin-bottom:4px">📁 '+escHtml(dept)+'</div>';
180
+ deptMap[dept].forEach(function(a){
181
+ var selected=false;
182
+ var existing=document.querySelectorAll('#'+containerId+' .selected-agent-tag');
183
+ existing.forEach(function(el){if(el.getAttribute('data-agent')===a.path)selected=true;});
184
+ 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>';
185
+ });
186
+ html+='</div>';
187
+ }
188
+ html+='<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="closeModal()">确定</button></div></div></div>';
189
+ $('modalContainer').innerHTML=html;
190
+ }catch(e){showToast('加载失败: '+e.message,'danger')}
191
+ }
192
+ function toggleAgentSelection(cb,inputId,containerId){
193
+ var agentPath=cb.value;
194
+ var container=document.getElementById(containerId);
195
+ if(cb.checked){
196
+ // 添加 tag
197
+ if(!container.querySelector('[data-agent="'+agentPath+'"]')){
198
+ var tag=document.createElement('span');
199
+ tag.className='selected-agent-tag';
200
+ tag.setAttribute('data-agent',agentPath);
201
+ 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';
202
+ tag.title='点击移除';
203
+ tag.textContent=agentPath+' ×';
204
+ tag.onclick=function(){this.remove();};
205
+ container.appendChild(tag);
206
+ }
207
+ // 同步到输入框
208
+ var first=container.querySelector('.selected-agent-tag');
209
+ if(first)document.getElementById(inputId).value=first.getAttribute('data-agent');
210
+ }else{
211
+ var existing=container.querySelector('[data-agent="'+agentPath+'"]');
212
+ if(existing)existing.remove();
213
+ var first=container.querySelector('.selected-agent-tag');
214
+ document.getElementById(inputId).value=first?first.getAttribute('data-agent'):'';
215
+ }
216
+ }
217
+
218
+ if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
219
+ window._adminRenderers['platforms'] = renderPlatforms;
220
+ } catch(e) { console.error('[admin-platforms] load error:', e); }
221
+ })();
@@ -0,0 +1,182 @@
1
+ // ========== Sessions ==========
2
+ (function(){
3
+ try {
4
+ // ========== Sessions ==========
5
+ async function renderSessions(){
6
+ const ss=await api('/api/sessions');
7
+ let html='<div class="table-wrap"><table><tr><th>会话</th><th>Agent</th><th>消息数</th><th>最后活动</th><th>操作</th></tr>';
8
+ for(const s of (ss||[])){
9
+ // 从 session_id 提取 agent 名 (格式: agent_web_timestamp)
10
+ const parts=(s.id||'').split('_web_');
11
+ const agentName=parts[0]||'default';
12
+ const displayName=s.display_name||s.id;
13
+ 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>
14
+ <td><button class="btn btn-sm" style="background:var(--success);color:#fff" onclick="enterSession('${escHtml(s.id)}','${escHtml(agentName)}')">切入</button>
15
+ <button class="btn btn-sm btn-ghost" onclick="viewSession('${s.id}')">查看</button>
16
+ <button class="btn btn-sm btn-danger" onclick="clearSession('${s.id}')">清除</button></td></tr>`;}
17
+ html+='</table></div>';if(!ss||!ss.length)html='<div class="empty">暂无会话</div>';
18
+ $('content').innerHTML=html;
19
+ }
20
+ async function viewSession(sid){
21
+ window._viewSessionSid=sid;window._viewSessionOffset=0;
22
+ _navSubState='view:'+sid;
23
+ navigateTo('sessions','view:'+sid,_loadSessionMessages);
24
+ }
25
+ async function _loadSessionMessages(){
26
+ const sid=window._viewSessionSid;if(!sid)return;
27
+ const offset=window._viewSessionOffset||0;
28
+ const msgs=await api(`/api/sessions/${encodeURIComponent(sid)}/messages?limit=100&offset=${offset}`);
29
+ if(!Array.isArray(msgs)){showToast('加载失败','danger');return}
30
+ const hasMore=msgs.length>=100;
31
+ let html='<h3 style="margin-bottom:12px">会话: '+escHtml(sid)+' <span class="badge badge-blue">'+msgs.length+' 条</span></h3>';
32
+ html+='<div style="max-height:600px;overflow-y:auto">';
33
+ for(let i=0;i<msgs.length;i++){
34
+ const m=msgs[i];
35
+ const mid='msg_'+i;
36
+ const role=m.role||'assistant';
37
+ const key=m.key||'';
38
+ if(role==='tool'){
39
+ const isResult=key==='tool_result';
40
+ const isCall=key==='tool_call';
41
+ const icon=isResult?'📋':(isCall?'⚙️':'🔧');
42
+ const label=isResult?'工具执行结果':(isCall?'工具调用':'工具过程');
43
+ const isOk=isResult&&!((m.content||'').includes('失败'));
44
+ const badge=isResult?`<span class="badge ${isOk?'badge-green':'badge-red'}" style="margin-left:6px">${isOk?'成功':'失败'}</span>`:'';
45
+ const tc=(m.content||'');
46
+ const tcTrunc=tc.length>800;
47
+ 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>`;
48
+ } else {
49
+ const isUser=role==='user';
50
+ const bg=isUser?'var(--primary)':'var(--surface2)';
51
+ const fg=isUser?'#fff':'var(--text)';
52
+ const avatar=isUser?'👤':'🤖';
53
+ const time=(m.time||'').slice(0,19);
54
+ const content=(m.content||'');
55
+ const long=content.length>500;
56
+ const displayContent=long?content.slice(0,500)+'... (共'+content.length+'字符)':content;
57
+ html+=`<div style="margin:8px 0;display:flex;gap:8px;${isUser?'flex-direction:row-reverse':''}">`;
58
+ html+=`<div style="flex-shrink:0;font-size:16px;margin-top:2px">${avatar}</div>`;
59
+ 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>`;
60
+ 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>`;
61
+ 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,'&quot;')}">展开全部 (${content.length}字符)</button>`;
62
+ html+=`</div></div>`;
63
+ }
64
+ }
65
+ html+='</div>';
66
+ if(hasMore)html+=`<button class="btn btn-ghost mt-8" onclick="window._viewSessionOffset=${offset+100};_loadSessionMessages()">加载更多...</button>`;
67
+ html+='<div class="flex gap-8 mt-8"><button class="btn btn-ghost" onclick="goBack()">返回</button>';
68
+ html+=`<button class="btn" style="background:var(--accent);color:#fff" onclick="viewSessionRaw('${escHtml(sid)}')">Raw 原始消息</button>`;
69
+ html+=`<button class="btn btn-primary" onclick="enterSession('${escHtml(sid)}','${escHtml(sid.split('_web_')[0]||'default')}')">在聊天中查看完整记录</button></div>`;
70
+ $('content').innerHTML=html;
71
+ }
72
+ // ========== Raw 原始消息查看 ==========
73
+ async function viewSessionRaw(sid){
74
+ window._viewSessionSid=sid;
75
+ _navSubState='raw:'+sid;
76
+ // 不用 navigateTo 因为需要先 fetch 数据再渲染,直接记录历史后继续
77
+ _navHistory.push({page:currentPage,sub:window._navSubState});
78
+ if(_navHistory.length>6)_navHistory.shift();
79
+ var hash='sessions'+'~raw:'+sid;
80
+ history.pushState({page:'sessions',sub:'raw:'+sid},'','#'+hash);
81
+
82
+ const msgs=await api(`/api/session/raw?sid=${encodeURIComponent(sid)}&limit=5000`);
83
+ if(!Array.isArray(msgs)){showToast('加载失败','danger');return}
84
+ // 按时间分组(同秒内合并)
85
+ const keyLabelMap={'llm_output':'LLM 输出','llm_input':'LLM 输入','tool_call':'工具调用','tool_result':'工具结果','tool_result_raw':'工具原始数据','reasoning':'推理过程','conversation_insight':'会话洞察','':'对话','llm_request':'LLM 请求'};
86
+ let html=`<h3 style="margin-bottom:12px">Raw: ${escHtml(sid)} <span class="badge badge-blue">${msgs.length} 条</span></h3>`;
87
+ // 筛选按钮
88
+ html+=`<div style="margin-bottom:10px;display:flex;gap:6px;flex-wrap:wrap" id="rawFilterBar">`;
89
+ html+=`<button class="btn btn-sm" style="background:var(--accent);color:#fff" data-filter="all" onclick="rawFilter('all',this)">全部</button>`;
90
+ // 收集所有 key 类型
91
+ const keys=[...new Set(msgs.map(m=>m.key||''))];
92
+ for(const k of keys){
93
+ const label=keyLabelMap[k]||k||'对话';
94
+ const count=msgs.filter(m=>(m.key||'')===k).length;
95
+ html+=`<button class="btn btn-sm btn-ghost" data-filter="${escHtml(k)}" onclick="rawFilter('${escHtml(k)}',this)">${escHtml(label)} (${count})</button>`;
96
+ }
97
+ html+=`</div>`;
98
+ // 时间索引导航
99
+ html+=`<div class="raw-time-nav" id="rawTimeNav"></div>`;
100
+ // 消息列表
101
+ html+=`<div style="max-height:65vh;overflow-y:auto;font-family:monospace" id="rawMsgList">`;
102
+ for(let i=0;i<msgs.length;i++){
103
+ const m=msgs[i];
104
+ const role=m.role||'?';
105
+ const key=m.key||'';
106
+ const time=(m.time||'').slice(0,19);
107
+ const content=(m.content||'');
108
+ const keyLabel=keyLabelMap[key]||key;
109
+ // 颜色标识
110
+ let borderColor='var(--border)';
111
+ let bgColor='var(--surface)';
112
+ if(key==='llm_input'){borderColor='#06b6d4';bgColor='#001a1f'}
113
+ else if(key==='llm_output'){borderColor='#e6a817';bgColor='#1a1700'}
114
+ else if(key==='tool_call'){borderColor='#3b82f6';bgColor='#001029'}
115
+ else if(key==='tool_result'){borderColor='#22c55e';bgColor='#001a0d'}
116
+ else if(key==='tool_result_raw'){borderColor='#10b981';bgColor='#001510'}
117
+ else if(key==='reasoning'){borderColor='#a855f7';bgColor='#0d0020'}
118
+ else if(role==='user'){borderColor='var(--primary)';bgColor='var(--surface)'}
119
+ else if(role==='assistant'){borderColor='#6b7280';bgColor='var(--surface2)'}
120
+ const mid='raw_'+i;
121
+ 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">`;
122
+ 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')">`;
123
+ html+=`<span><span style="color:var(--text);font-weight:600">${escHtml(role)}</span>`;
124
+ if(keyLabel)html+=` <span class="badge badge-blue" style="font-size:10px">${escHtml(keyLabel)}</span>`;
125
+ html+=`</span>`;
126
+ html+=`<span>${escHtml(time)} <span style="margin-left:4px;opacity:0.5">${content.length}字符</span></span>`;
127
+ html+=`</div>`;
128
+ 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">`;
129
+ html+=escHtml(content.length>8000?content.slice(0,8000)+'\n... (共'+content.length+'字符)':content);
130
+ 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,'&quot;')}">展开全部 (${content.length}字符)</button>`;
131
+ html+=`</div></div>`;
132
+ }
133
+ html+=`</div>`;
134
+ html+=`<div class="flex gap-8 mt-8"><button class="btn btn-ghost" onclick="goBack()">返回查看</button>`;
135
+ html+=`<button class="btn btn-ghost" onclick="goBack()">返回会话列表</button></div>`;
136
+ $('content').innerHTML=html;
137
+ // 生成时间索引
138
+ _buildTimeNav(msgs);
139
+ }
140
+ function rawFilter(key,btn){
141
+ const items=document.querySelectorAll('.raw-item');
142
+ for(const item of items){
143
+ if(key==='all'||item.dataset.key===key){item.style.display=''}
144
+ else{item.style.display='none'}
145
+ }
146
+ // 更新按钮样式
147
+ const bar=document.getElementById('rawFilterBar');
148
+ if(bar){
149
+ bar.querySelectorAll('.btn').forEach(b=>{b.className='btn btn-sm btn-ghost'});
150
+ if(btn)btn.className='btn btn-sm';
151
+ if(btn)btn.style.background='var(--accent)';
152
+ if(btn)btn.style.color='#fff';
153
+ }
154
+ }
155
+ function _buildTimeNav(msgs){
156
+ // 按分钟分组生成时间跳转锚点
157
+ const nav=document.getElementById('rawTimeNav');
158
+ if(!nav||!msgs.length)return;
159
+ const minutes={};
160
+ for(let i=0;i<msgs.length;i++){
161
+ const t=(msgs[i].time||'').slice(0,16); // YYYY-MM-DDTHH:MM
162
+ if(!minutes[t])minutes[t]=[];
163
+ minutes[t].push(i);
164
+ }
165
+ const times=Object.keys(minutes);
166
+ if(times.length<=1)return;
167
+ let navHtml='<span style="color:var(--text3);font-size:12px">时间索引:</span> ';
168
+ for(const t of times){
169
+ 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>`;
170
+ }
171
+ nav.innerHTML=navHtml;
172
+ }
173
+ async function clearSession(sid){await api(`/api/sessions/${encodeURIComponent(sid)}`,{method:'DELETE'});renderSessions()}
174
+ // 切入会话: 打开聊天页面并自动加载指定会话
175
+ function enterSession(sid,agentName){
176
+ window.location.href='/ui/chat/chat_container.html?agent='+encodeURIComponent(agentName)+'&mode=exec&session='+encodeURIComponent(sid);
177
+ }
178
+
179
+ if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
180
+ window._adminRenderers['sessions'] = renderSessions;
181
+ } catch(e) { console.error('[admin-sessions] load error:', e); }
182
+ })();
@@ -0,0 +1,217 @@
1
+ // ========== Skills ==========
2
+ (function(){
3
+ try {
4
+ // ========== Skills ==========
5
+ // [v1.23.0] CLI 命令分类配置(与 registry.py CLI_CATEGORY_LABELS/ICONS 同步)
6
+ 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}'};
7
+ const CLI_CAT_LABELS={perception:'感知',search:'搜索',file:'文件操作',document:'文档生成',system:'系统',browser:'浏览器',gui:'GUI 桌面',memory:'记忆'};
8
+
9
+ async function renderSkills(){
10
+ const cats=await api('/api/skills?view=categorized');
11
+ const bt=cats.builtin_tools||[];
12
+ const cc=cats.cli_commands||[];
13
+ const ps=cats.python_skills||[];
14
+ const sg=cats.skill_guides||[];
15
+ const total=bt.length+cc.length+ps.length+sg.length;
16
+ const disabledAll=[...ps,...sg].filter(s=>s.disabled).length;
17
+
18
+ let html=`<div class="flex justify-between items-center mb-16 flex-wrap gap-8">
19
+ <div style="color:var(--text2);font-size:13px">
20
+ 共 <strong>${total}</strong> 个工具/技能 (内置${bt.length} · CLI${cc.length} · Python${ps.length} · 指南${sg.length}) · ${disabledAll} 已禁用
21
+ </div>
22
+ <div class="flex gap-8"><input id="skillSearch" placeholder="搜索工具/技能/CLI命令..." style="width:220px" oninput="filterSkills()">
23
+ </div></div>`;
24
+
25
+ // ── 第一区: 内置平台工具 (LLM 直接调用) ──
26
+ if(bt.length){
27
+ html+=`<div class="card" style="padding:0;overflow:hidden;margin-bottom:16px">
28
+ <h3 style="padding:12px 16px;margin:0;display:flex;align-items:center;gap:8px">
29
+ <span>⚙️</span> 内置平台工具
30
+ <span class="badge badge-blue">${bt.length}</span>
31
+ <span style="font-size:11px;color:var(--text2);font-weight:normal;margin-left:auto">LLM 直接调用 · 不可禁用</span>
32
+ </h3>`;
33
+ for(const s of bt){html+=builtinRowHtml(s);}
34
+ html+='</div>';
35
+ }
36
+
37
+ // ── 第二区: CLI 命令 (通过 command 工具间接调用) ──
38
+ if(cc.length){
39
+ // 按分类分组显示
40
+ const groups={};
41
+ for(const c of cc){
42
+ const cat=c.category||'other';
43
+ if(!groups[cat])groups[cat]=[];
44
+ groups[cat].push(c);
45
+ }
46
+ html+=`<div class="card" style="padding:0;overflow:hidden;margin-bottom:16px">
47
+ <h3 style="padding:12px 16px;margin:0;display:flex;align-items:center;gap:8px">
48
+ <span>⚡</span> CLI 命令
49
+ <span class="badge badge-cyan">${cc.length}</span>
50
+ <span style="font-size:11px;color:var(--text2);font-weight:normal;margin-left:auto">通过 command 工具调用 · 不可禁用</span>
51
+ </h3>`;
52
+ for(const [cat,cmds] of Object.entries(groups)){
53
+ const icon=CLI_CAT_ICONS[cat]||'🔧';
54
+ const label=CLI_CAT_LABELS[cat]||cat;
55
+ html+=`<div style="padding:8px 16px 4px;font-size:12px;color:var(--text2);font-weight:600">${icon} ${label} (${cmds.length})</div>`;
56
+ for(const s of cmds){html+=cliCmdRowHtml(s);}
57
+ }
58
+ html+='</div>';
59
+ }
60
+
61
+ // ── 第三区: Python 可执行技能 ──
62
+ if(ps.length){
63
+ html+=`<div class="card" style="padding:0;overflow:hidden;margin-bottom:16px">
64
+ <h3 style="padding:12px 16px;margin:0;display:flex;align-items:center;gap:8px">
65
+ <span>🔧</span> Python 技能
66
+ <span class="badge badge-green">${ps.length}</span>
67
+ <span style="font-size:11px;color:var(--text2);font-weight:normal;margin-left:auto">CLI 底层实现 · SkillRegistry 管理</span>
68
+ </h3>`;
69
+ for(const s of ps){html+=skillRowHtml(s);}
70
+ html+='</div>';
71
+ }
72
+
73
+ // ── 第四区: 技能指南 (RAG) ──
74
+ if(sg.length){
75
+ html+=`<div class="card" style="padding:0;overflow:hidden;margin-bottom:16px">
76
+ <h3 style="padding:12px 16px;margin:0;display:flex;align-items:center;gap:8px">
77
+ <span>📝</span> 技能指南
78
+ <span class="badge badge-purple">${sg.length}</span>
79
+ <span style="font-size:11px;color:var(--text2);font-weight:normal;margin-left:auto">Markdown Prompt · RAG 按需检索</span>
80
+ </h3>`;
81
+ for(const s of sg){html+=guideRowHtml(s);}
82
+ html+='</div>';
83
+ }
84
+
85
+ if(!total)html+='<div class="empty">暂无工具/技能</div>';
86
+ $('content').innerHTML=html;
87
+ }
88
+
89
+ /** 内置平台工具行 (只读,无 toggle) */
90
+ function builtinRowHtml(s){
91
+ const paramCount=(s.parameters||[]).length;
92
+ const paramTag=paramCount>0?`<span class="tag">${paramCount} 参数</span>`:'';
93
+ const catBadge=s.category?`<span class="badge badge-blue">${escHtml(s.category)}</span>`:'';
94
+ return `<div class="skill-row" data-name="${escHtml(s.name||'')}" data-desc="${escHtml(s.description||'')}">
95
+ <div style="flex-shrink:0;width:36px;text-align:center;font-size:18px">⚙️</div>
96
+ <div class="skill-info">
97
+ <div class="skill-name">${escHtml(s.name)} <span class="badge badge-blue">内置</span></div>
98
+ <div class="skill-desc">${escHtml(s.description||'暂无描述')}</div>
99
+ </div>
100
+ <div class="flex gap-8 items-center" style="flex-shrink:0">
101
+ ${catBadge}${paramTag}
102
+ <button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
103
+ </div>
104
+ </div>`;
105
+ }
106
+
107
+ /** CLI 命令行 (只读,无 toggle,紧凑显示) */
108
+ function cliCmdRowHtml(s){
109
+ 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>`:'';
110
+ const aliasTag=(s.aliases||[]).length>0?`<span class="tag">${s.aliases.map(a=>'alias: '+a).join(', ')}</span>`:'';
111
+ return `<div class="skill-row" data-name="${escHtml(s.name||'')}" data-desc="${escHtml(s.description||'')}" style="padding:8px 16px 8px 48px">
112
+ <div class="skill-info">
113
+ <div class="skill-name" style="font-size:13px">${escHtml(s.name)} <span class="badge badge-cyan" style="font-size:10px">CLI</span> ${aliasTag}</div>
114
+ <div class="skill-desc" style="font-size:12px">${escHtml(s.description||'')} ${cliText}</div>
115
+ </div>
116
+ <div class="flex gap-8 items-center" style="flex-shrink:0">
117
+ <button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
118
+ </div>
119
+ </div>`;
120
+ }
121
+
122
+ /** Python 可执行技能行 (可 toggle) */
123
+ function skillRowHtml(s){
124
+ const isDisabled=s.disabled;
125
+ const paramCount=(s.parameters||[]).length;
126
+ const paramTag=paramCount>0?`<span class="tag">${paramCount} 参数</span>`:'';
127
+ const catBadge=s.category?`<span class="badge badge-blue">${escHtml(s.category)}</span>`:'';
128
+ return `<div class="skill-row" data-name="${escHtml(s.name||'')}" data-desc="${escHtml(s.description||'')}">
129
+ <div style="flex-shrink:0;width:36px;text-align:center;font-size:18px">🔧</div>
130
+ <div class="skill-info">
131
+ <div class="skill-name">${escHtml(s.name)} ${isDisabled?'<span class="badge badge-red">已禁用</span>':''} ${s.dangerous?'<span class="badge badge-red">⚠ 危险</span>':''}</div>
132
+ <div class="skill-desc">${escHtml(s.description||'暂无描述')}</div>
133
+ </div>
134
+ <div class="flex gap-8 items-center" style="flex-shrink:0">
135
+ ${catBadge}${paramTag}
136
+ <label class="toggle" title="${isDisabled?'启用':'禁用'}"><input type="checkbox" ${!isDisabled?'checked':''} onchange="toggleSkill('${escHtml(s.name)}',this.checked)"><span class="slider"></span></label>
137
+ <button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
138
+ </div>
139
+ </div>`;
140
+ }
141
+
142
+ /** 技能指南行 (可 toggle + 参考资料) */
143
+ function guideRowHtml(s){
144
+ const isDisabled=s.disabled;
145
+ const refTag=(s.references||[]).length>0?`<span class="tag">${s.references.length} 参考资料</span>`:'';
146
+ const tplTag=s.has_templates?'<span class="tag">含模板</span>':'';
147
+ const scriptTag=s.has_scripts?'<span class="tag">含脚本</span>`:'';
148
+ const catBadge=s.category?`<span class="badge badge-purple">${escHtml(s.category)}</span>`:'';
149
+ return `<div class="skill-row" data-name="${escHtml(s.name||'')}" data-desc="${escHtml(s.description||'')}">
150
+ <div style="flex-shrink:0;width:36px;text-align:center;font-size:18px">📝</div>
151
+ <div class="skill-info">
152
+ <div class="skill-name">${escHtml(s.name)} ${isDisabled?'<span class="badge badge-red">已禁用</span>':''}</div>
153
+ <div class="skill-desc">${escHtml(s.description||'暂无描述')}</div>
154
+ </div>
155
+ <div class="flex gap-8 items-center" style="flex-shrink:0">
156
+ ${catBadge}${refTag}${tplTag}${scriptTag}
157
+ <label class="toggle" title="${isDisabled?'启用':'禁用'}"><input type="checkbox" ${!isDisabled?'checked':''} onchange="toggleSkill('${escHtml(s.name)}',this.checked)"><span class="slider"></span></label>
158
+ <button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
159
+ </div>
160
+ </div>`;
161
+ }
162
+
163
+ function filterSkills(){
164
+ const q=($('skillSearch')?.value||'').toLowerCase();
165
+ document.querySelectorAll('.skill-row').forEach(el=>{
166
+ const n=(el.dataset.name||'').toLowerCase();const d=(el.dataset.desc||'').toLowerCase();
167
+ el.style.display=(n.includes(q)||d.includes(q))?'':'none';
168
+ });
169
+ }
170
+
171
+ async function toggleSkill(name,enabled){
172
+ const r=await api(`/api/skills/${encodeURIComponent(name)}/toggle`,{method:'POST',body:JSON.stringify({enabled})});
173
+ if(r.error){showToast(r.error,'danger');return}
174
+ showToast(enabled?'已启用':'已禁用','success');renderSkills();
175
+ }
176
+
177
+ async function viewSkillDetail(name){
178
+ const s=await api(`/api/skills/${encodeURIComponent(name)}`);
179
+ if(s.error){showToast(s.error,'danger');return}
180
+
181
+ // 判断类型标签
182
+ const st=s.skill_type||'';
183
+ let typeLabel,typeBadge;
184
+ if(st==='builtin_platform'||st==='builtin_agent_tool'){
185
+ typeLabel='内置平台工具';typeBadge='<span class="badge badge-blue">内置平台</span>';
186
+ }else if(st==='cli_command'){
187
+ typeLabel='CLI 命令';typeBadge='<span class="badge badge-cyan">CLI 命令</span>';
188
+ }else if(st==='markdown'){
189
+ typeLabel='技能指南 (RAG)';typeBadge='<span class="badge badge-purple">技能指南</span>';
190
+ }else{
191
+ typeLabel='Python 可执行技能';typeBadge='<span class="badge badge-green">Python 技能</span>';
192
+ }
193
+
194
+ // CLI 命令显示调用方式
195
+ 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>`:'';
196
+ 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>`:'';
197
+
198
+ let html=`<h3>${escHtml(s.name)}</h3>
199
+ <div class="form-group"><label>描述</label><div style="font-size:13px;color:var(--text)">${escHtml(s.description||'无')}</div></div>
200
+ <div class="form-row">
201
+ <div class="form-group"><label>类型</label><div>${typeBadge}</div></div>
202
+ <div class="form-group"><label>类别</label><div><span class="badge badge-blue">${escHtml(s.category||'general')}</span></div></div>
203
+ </div>
204
+ ${s.note?`<div class="form-group"><label>备注</label><div style="font-size:13px;color:var(--text2)">${escHtml(s.note)}</div></div>`:''}
205
+ ${cliInfo}${aliasInfo}
206
+ <div class="form-group"><label>危险操作</label><div>${s.dangerous?'<span class="badge badge-red">是</span>':'<span class="badge badge-green">否</span>'}</div></div>
207
+ ${(s.parameters||[]).length>0?`<div class="form-group"><label>参数 (${s.parameters.length})</label>
208
+ <div class="table-wrap"><table><tr><th>名称</th><th>类型</th><th>必需</th><th>描述</th></tr>
209
+ ${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('')}
210
+ </table></div></div>`:''}`;
211
+ $('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>`;
212
+ }
213
+
214
+ if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
215
+ window._adminRenderers['skills'] = renderSkills;
216
+ } catch(e) { console.error('[admin-skills] load error:', e); }
217
+ })();