myagent-ai 1.23.19 → 1.23.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/chatbot/whatsapp_bot.py +0 -0
- package/chatbot/whatsapp_bridge/bridge.mjs +0 -0
- package/chatbot/whatsapp_bridge/package.json +0 -0
- package/core/stt.py +0 -0
- package/core/tool_dispatcher.py +0 -0
- package/core/web_control.py +0 -0
- package/data/novnc/lib/base64.js +0 -0
- package/data/novnc/lib/crypto/aes.js +0 -0
- package/data/novnc/lib/crypto/bigint.js +0 -0
- package/data/novnc/lib/crypto/crypto.js +0 -0
- package/data/novnc/lib/crypto/des.js +0 -0
- package/data/novnc/lib/crypto/dh.js +0 -0
- package/data/novnc/lib/crypto/md5.js +0 -0
- package/data/novnc/lib/crypto/rsa.js +0 -0
- package/data/novnc/lib/decoders/copyrect.js +0 -0
- package/data/novnc/lib/decoders/hextile.js +0 -0
- package/data/novnc/lib/decoders/jpeg.js +0 -0
- package/data/novnc/lib/decoders/raw.js +0 -0
- package/data/novnc/lib/decoders/rre.js +0 -0
- package/data/novnc/lib/decoders/tight.js +0 -0
- package/data/novnc/lib/decoders/tightpng.js +0 -0
- package/data/novnc/lib/decoders/zrle.js +0 -0
- package/data/novnc/lib/deflator.js +0 -0
- package/data/novnc/lib/display.js +0 -0
- package/data/novnc/lib/encodings.js +0 -0
- package/data/novnc/lib/inflator.js +0 -0
- package/data/novnc/lib/input/domkeytable.js +0 -0
- package/data/novnc/lib/input/fixedkeys.js +0 -0
- package/data/novnc/lib/input/gesturehandler.js +0 -0
- package/data/novnc/lib/input/keyboard.js +0 -0
- package/data/novnc/lib/input/keysym.js +0 -0
- package/data/novnc/lib/input/keysymdef.js +0 -0
- package/data/novnc/lib/input/util.js +0 -0
- package/data/novnc/lib/input/vkeys.js +0 -0
- package/data/novnc/lib/input/xtscancodes.js +0 -0
- package/data/novnc/lib/ra2.js +0 -0
- package/data/novnc/lib/rfb.js +0 -0
- package/data/novnc/lib/util/browser.js +0 -0
- package/data/novnc/lib/util/cursor.js +0 -0
- package/data/novnc/lib/util/element.js +0 -0
- package/data/novnc/lib/util/events.js +0 -0
- package/data/novnc/lib/util/eventtarget.js +0 -0
- package/data/novnc/lib/util/int.js +0 -0
- package/data/novnc/lib/util/logging.js +0 -0
- package/data/novnc/lib/util/strings.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/utils/common.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/adler32.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/constants.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/crc32.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/deflate.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/gzheader.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/inffast.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/inflate.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/inftrees.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/messages.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/trees.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/zstream.js +0 -0
- package/data/novnc/lib/websock.js +0 -0
- package/package.json +1 -1
- package/scripts/cli.py +0 -0
- package/skills/agent_tool_skill.py +0 -0
- package/web/ui/admin/admin-agents.js +570 -0
- package/web/ui/admin/admin-core.js +322 -0
- package/web/ui/admin/admin-dashboard.js +153 -0
- package/web/ui/admin/admin-executor.js +67 -0
- package/web/ui/admin/admin-files.js +81 -0
- package/web/ui/admin/admin-llm.js +190 -0
- package/web/ui/admin/admin-logs.js +69 -0
- package/web/ui/admin/admin-memory.js +91 -0
- package/web/ui/admin/admin-org.js +283 -0
- package/web/ui/admin/admin-permissions.js +147 -0
- package/web/ui/admin/admin-platforms.js +221 -0
- package/web/ui/admin/admin-sessions.js +182 -0
- package/web/ui/admin/admin-skills.js +217 -0
- package/web/ui/admin/admin-system.js +154 -0
- package/web/ui/admin/admin-tasks.js +131 -0
- package/web/ui/chat/chat_main.js +292 -304
- package/web/ui/chat/flow_engine.js +96 -41
- package/web/ui/index.html +15 -2776
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
// ========== Agents ==========
|
|
2
|
+
(function(){
|
|
3
|
+
try {
|
|
4
|
+
// ========== Agents (FULL REWRITE) ==========
|
|
5
|
+
async function renderAgents(){
|
|
6
|
+
const [agents,models,depts]=await Promise.all([api('/api/agents'),api('/api/models'),api('/api/departments')]);
|
|
7
|
+
allAgentsCache=Array.isArray(agents)?agents:[];allModelsCache=Array.isArray(models)?models:[];allDeptsCache=Array.isArray(depts)?depts:(depts?.tree||[]);
|
|
8
|
+
const deptMap={};(function buildDeptMap(list,pfx){
|
|
9
|
+
for(const d of list||[]){const path=pfx?(pfx+'/'+d.name):d.name;deptMap[path]={name:d.name,path};buildDeptMap(d.children||[],path)}
|
|
10
|
+
})(allDeptsCache,'');
|
|
11
|
+
let html=`<div class="flex justify-between items-center mb-16 flex-wrap gap-8">
|
|
12
|
+
<div style="color:var(--text2);font-size:13px">共 ${allAgentsCache.length} 个 Agent</div>
|
|
13
|
+
<div class="flex gap-8"><input id="agentSearch" placeholder="搜索 Agent..." style="width:220px" oninput="filterAgents()">
|
|
14
|
+
<button class="btn btn-primary" onclick="openCreateAgentModal()">+ 新建 Agent</button></div></div>`;
|
|
15
|
+
html+='<div id="agentList" class="grid" style="grid-template-columns:repeat(auto-fill,minmax(340px,1fr))">';
|
|
16
|
+
for(const a of allAgentsCache){
|
|
17
|
+
html+=agentCardHtml(a,deptMap);
|
|
18
|
+
}
|
|
19
|
+
html+='</div>';
|
|
20
|
+
$('content').innerHTML=html;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function agentCardHtml(a,deptMap){
|
|
24
|
+
const isSys=!!a.system;
|
|
25
|
+
const execMode=a.execution_mode==='local';
|
|
26
|
+
const modelName=a.model_id||a.model||'默认';
|
|
27
|
+
const dept=deptMap?.[a.department]||null;
|
|
28
|
+
const emoji=a.avatar_emoji||'🤖';
|
|
29
|
+
const color=a.avatar_color||'#6366f1';
|
|
30
|
+
const avatarHtml=a.avatar_image?'<img src="'+escHtml(a.avatar_image)+'" style="width:100%;height:100%;object-fit:cover">':emoji;
|
|
31
|
+
const enabled=a.enabled!==false;
|
|
32
|
+
return `<div class="agent-card" data-name="${escHtml(a.name||'')}" data-desc="${escHtml(a.description||'')}">
|
|
33
|
+
<div class="avatar" style="background:${color}22;border:2px solid ${color};overflow:hidden">${avatarHtml}</div>
|
|
34
|
+
<div class="info">
|
|
35
|
+
<h4>${escHtml(a.name||a.path)} ${isSys?'<span class="badge badge-purple">系统</span>':''} ${!enabled?'<span class="badge badge-red">已禁用</span>':''}</h4>
|
|
36
|
+
<p>${escHtml(a.description||'无描述')}</p>
|
|
37
|
+
<div class="meta">
|
|
38
|
+
<span class="tag" title="ID">${escHtml(a.id||'-')}</span>
|
|
39
|
+
<span class="tag">${execMode?'🟢本地':'🟡沙盒'}</span>
|
|
40
|
+
<span class="tag">🤖 ${escHtml(modelName)}</span>
|
|
41
|
+
${a.session_count?`<span class="tag">💬 ${a.session_count}</span>`:''}
|
|
42
|
+
${dept?`<span class="tag">🏢 ${escHtml(dept.name)}</span>`:''}
|
|
43
|
+
${a.created_at?`<span class="tag">${fmtDate(a.created_at)}</span>`:''}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="flex flex-col gap-8" style="flex-shrink:0">
|
|
47
|
+
<button class="btn btn-sm" style="background:var(--success);color:#fff" onclick="event.stopPropagation();chatWithAgent('${escHtml(a.path)}')">对话</button>
|
|
48
|
+
<button class="btn btn-sm btn-ghost" onclick="event.stopPropagation();showWorkdirModal('${escHtml(a.path)}')">📁 工作目录</button>
|
|
49
|
+
<button class="btn btn-sm btn-primary" onclick="event.stopPropagation();openEditAgentModal('${escHtml(a.path)}')">编辑</button>
|
|
50
|
+
${!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>`:''}
|
|
51
|
+
</div>
|
|
52
|
+
</div>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function filterAgents(){
|
|
56
|
+
const q=($('agentSearch')?.value||'').toLowerCase();
|
|
57
|
+
document.querySelectorAll('#agentList .agent-card').forEach(el=>{
|
|
58
|
+
const n=(el.dataset.name||'').toLowerCase();const d=(el.dataset.desc||'').toLowerCase();
|
|
59
|
+
el.style.display=(n.includes(q)||d.includes(q))?'':'none';
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// [v1.18.5] 工作目录文件浏览
|
|
64
|
+
let _workdirCurrentPath='';
|
|
65
|
+
async function showWorkdirModal(agentPath){
|
|
66
|
+
_workdirCurrentPath='';
|
|
67
|
+
const title='📁 工作目录'+(agentPath?' ('+escHtml(agentPath)+')':'');
|
|
68
|
+
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal modal-wide" onclick="event.stopPropagation()" style="max-width:600px">
|
|
69
|
+
<div class="flex justify-between items-center mb-16">
|
|
70
|
+
<h3>${title}</h3>
|
|
71
|
+
<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>
|
|
72
|
+
</div>
|
|
73
|
+
<div id="workdirBreadcrumb" style="font-size:12px;color:var(--text3);margin-bottom:8px"></div>
|
|
74
|
+
<div id="workdirContent"><div class="empty">加载中...</div></div>
|
|
75
|
+
<div class="flex gap-8 mt-16"><button class="btn btn-ghost" onclick="closeModal()">关闭</button></div>
|
|
76
|
+
</div></div>`;
|
|
77
|
+
loadWorkdirFiles('');
|
|
78
|
+
}
|
|
79
|
+
async function loadWorkdirFiles(subPath,recursive){
|
|
80
|
+
_workdirCurrentPath=subPath||'';
|
|
81
|
+
const params=new URLSearchParams();
|
|
82
|
+
if(subPath)params.set('path',subPath);
|
|
83
|
+
if(recursive)params.set('recursive','1');
|
|
84
|
+
const files=await api('/api/workdir/files?'+params.toString());
|
|
85
|
+
const bc=document.getElementById('workdirBreadcrumb');
|
|
86
|
+
const el=document.getElementById('workdirContent');
|
|
87
|
+
if(!el)return;
|
|
88
|
+
// 面包屑导航
|
|
89
|
+
if(subPath){
|
|
90
|
+
const parts=subPath.split('/');let crumbs=['<span style="cursor:pointer" onclick="loadWorkdirFiles(\'\')">根目录</span>'];
|
|
91
|
+
let acc='';
|
|
92
|
+
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>')}
|
|
93
|
+
if(bc)bc.innerHTML=crumbs.join('');
|
|
94
|
+
}else{if(bc)bc.innerHTML='根目录'}
|
|
95
|
+
if(!files||!files.length){el.innerHTML='<div class="empty">暂无文件</div>';return}
|
|
96
|
+
// 排序:目录在前,文件在后
|
|
97
|
+
const dirs=files.filter(f=>f.type==='dir').sort((a,b)=>a.name.localeCompare(b.name));
|
|
98
|
+
const fils=files.filter(f=>f.type==='file').sort((a,b)=>a.name.localeCompare(b.name));
|
|
99
|
+
let html='<div class="table-wrap"><table><tr><th>名称</th><th>大小</th><th></th></tr>';
|
|
100
|
+
for(const d of dirs){
|
|
101
|
+
const dp=d.path||d.name;
|
|
102
|
+
html+=`<tr style="cursor:pointer" onclick="loadWorkdirFiles('${escHtml(dp)}')"><td>📂 ${escHtml(d.name)}</td><td>-</td><td></td></tr>`;
|
|
103
|
+
}
|
|
104
|
+
for(const f of fils){
|
|
105
|
+
const fp=f.path||f.name;
|
|
106
|
+
const sizeStr=f.size>1048576?(f.size/1048576).toFixed(1)+' MB':f.size>1024?(f.size/1024).toFixed(1)+' KB':f.size+' B';
|
|
107
|
+
html+=`<tr><td style="cursor:pointer" onclick="downloadWorkdirFile('${escHtml(fp)}')">📄 ${escHtml(f.name)}</td><td>${sizeStr}</td>
|
|
108
|
+
<td><button class="btn btn-sm btn-ghost" onclick="downloadWorkdirFile('${escHtml(fp)}')">下载</button></td></tr>`;
|
|
109
|
+
}
|
|
110
|
+
html+='</table></div>';
|
|
111
|
+
el.innerHTML=html;
|
|
112
|
+
}
|
|
113
|
+
function downloadWorkdirFile(relPath){
|
|
114
|
+
const link=document.createElement('a');
|
|
115
|
+
link.href=API+'/api/workdir/download/'+encodeURIComponent(relPath);
|
|
116
|
+
link.download='';
|
|
117
|
+
document.body.appendChild(link);
|
|
118
|
+
link.click();
|
|
119
|
+
document.body.removeChild(link);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Create Agent Modal
|
|
123
|
+
function _flattenDepts(list,pfx,result){
|
|
124
|
+
result=result||[];
|
|
125
|
+
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);}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
function openCreateAgentModal(){
|
|
129
|
+
const modelOpts=allModelsCache.map(m=>`<option value="${escHtml(m.id)}">${escHtml(m.name)} (${escHtml(m.provider)})</option>`).join('');
|
|
130
|
+
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal modal-wide" onclick="event.stopPropagation()">
|
|
131
|
+
<h3>🧠 新建 Agent</h3>
|
|
132
|
+
<div class="form-row">
|
|
133
|
+
<div class="form-group"><label>名称 *</label><input id="caName" placeholder="Agent 名称"></div>
|
|
134
|
+
<div class="form-group"><label>描述</label><input id="caDesc" placeholder="Agent 描述"></div>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="form-row">
|
|
137
|
+
<div class="form-group"><label>头像</label>
|
|
138
|
+
<div class="flex gap-8 items-center" style="flex-wrap:wrap">
|
|
139
|
+
<div id="caAvatarPreview" class="avatar" style="width:48px;height:48px;background:#6366f122;border:2px solid #6366f1;font-size:24px">🤖</div>
|
|
140
|
+
<div style="flex:1;min-width:200px">
|
|
141
|
+
<div class="flex gap-8 items-center">
|
|
142
|
+
<input id="caEmoji" placeholder="🤖" maxlength="4" style="width:60px" oninput="$('caAvatarPreview').textContent=this.value||'🤖'">
|
|
143
|
+
<label class="btn btn-sm" style="cursor:pointer"><input type="file" accept="image/*" hidden onchange="handleAvatarUpload(this,'ca')">
|
|
144
|
+
📷 上传图片
|
|
145
|
+
</label>
|
|
146
|
+
</div>
|
|
147
|
+
<div id="caCropArea" style="display:none;margin-top:8px">
|
|
148
|
+
<div style="position:relative;display:inline-block">
|
|
149
|
+
<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')">
|
|
150
|
+
<div id="caCropOverlay" style="position:absolute;border:2px dashed #4f46e5;background:rgba(79,70,229,0.15);display:none;pointer-events:none"></div>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="flex gap-8 mt-8">
|
|
153
|
+
<button class="btn btn-sm btn-primary" onclick="confirmAvatarCrop('ca','${name||''}')">✂️ 裁剪并使用</button>
|
|
154
|
+
<button class="btn btn-sm btn-ghost" onclick="cancelAvatarCrop('ca')">取消</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
<input type="hidden" id="caAvatarImage" value="">
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
<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>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="form-row">
|
|
164
|
+
<div class="form-group"><label>执行模式</label><select id="caExecMode"><option value="sandbox">沙盒 (Docker)</option><option value="local">本机</option></select></div>
|
|
165
|
+
<div class="form-group"><label>绑定模型</label><select id="caModelId"><option value="">使用全局默认</option>${modelOpts}</select></div>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="form-group"><label>系统提示</label><textarea id="caPrompt" rows="4" placeholder="输入 Agent 的系统提示..."></textarea></div>
|
|
168
|
+
<div class="form-row">
|
|
169
|
+
<div class="form-group"><label>工作目录</label><input id="caWorkDir" placeholder="留空使用默认"></div>
|
|
170
|
+
<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>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="flex gap-8 mt-16">
|
|
173
|
+
<button class="btn btn-primary" onclick="doCreateAgent()">创建</button>
|
|
174
|
+
<button class="btn btn-ghost" onclick="closeModal()">取消</button>
|
|
175
|
+
</div>
|
|
176
|
+
</div></div>`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function doCreateAgent(){
|
|
180
|
+
const name=$('caName').value.trim();
|
|
181
|
+
if(!name){showToast('请输入名称','danger');return}
|
|
182
|
+
const r=await api('/api/agents',{method:'POST',body:JSON.stringify({
|
|
183
|
+
name,description:$('caDesc').value,avatar_emoji:$('caEmoji').value,
|
|
184
|
+
avatar_color:$('caColorText').value,execution_mode:$('caExecMode').value,
|
|
185
|
+
model_id:$('caModelId').value,system_prompt:$('caPrompt').value,
|
|
186
|
+
work_dir:$('caWorkDir').value,department:$('caDept').value,
|
|
187
|
+
avatar_image:$('caAvatarImage')?.value||''
|
|
188
|
+
})});
|
|
189
|
+
if(r.error){showToast(r.error,'danger');return}
|
|
190
|
+
closeModal();showToast('Agent 创建成功','success');renderAgents();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Edit Agent Modal
|
|
194
|
+
async function openEditAgentModal(path){
|
|
195
|
+
window._currentEditAgentPath=path;
|
|
196
|
+
const a=await api(`/api/agents/${encodeURIComponent(path)}`);
|
|
197
|
+
if(a.error){showToast(a.error,'danger');return}
|
|
198
|
+
const isSys=!!a.system;
|
|
199
|
+
const modelOpts=allModelsCache.map(m=>`<option value="${escHtml(m.id)}" ${a.model_id===m.id?'selected':''}>${escHtml(m.name)} (${escHtml(m.provider)})</option>`).join('');
|
|
200
|
+
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('');
|
|
201
|
+
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('');
|
|
202
|
+
|
|
203
|
+
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal modal-wide" onclick="event.stopPropagation()">
|
|
204
|
+
<h3>🧠 ${escHtml(a.name||path)} ${isSys?'<span class="badge badge-purple">系统</span>':''}</h3>
|
|
205
|
+
<div class="tabs" id="editAgentTabs">
|
|
206
|
+
<div class="tab active" onclick="agentTabSwitch(this,'atBasic')">基本信息</div>
|
|
207
|
+
<div class="tab" onclick="agentTabSwitch(this,'atSoul')">人格系统</div>
|
|
208
|
+
<div class="tab" onclick="agentTabSwitch(this,'atKB')">知识库</div>
|
|
209
|
+
<div class="tab" onclick="agentTabSwitch(this,'atSessions')">会话历史</div>
|
|
210
|
+
<div class="tab" onclick="agentTabSwitch(this,'atSettings')">设置</div>
|
|
211
|
+
<div class="tab" onclick="agentTabSwitch(this,'atPerms')">🔑 权限</div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<!-- 基本信息 -->
|
|
215
|
+
<div id="atBasic">
|
|
216
|
+
<div class="form-row">
|
|
217
|
+
<div class="form-group"><label>名称</label><input id="eaName" value="${escHtml(a.name||'')}" ${isSys?'disabled':''}></div>
|
|
218
|
+
<div class="form-group"><label>描述</label><input id="eaDesc" value="${escHtml(a.description||'')}" ${isSys?'disabled':''}></div>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="form-row">
|
|
221
|
+
<div class="form-group"><label>头像</label>
|
|
222
|
+
<div class="flex gap-8 items-center" style="flex-wrap:wrap">
|
|
223
|
+
<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>
|
|
224
|
+
<div style="flex:1;min-width:200px">
|
|
225
|
+
<div class="flex gap-8 items-center">
|
|
226
|
+
<input id="eaEmoji" value="${escHtml(a.avatar_emoji||'')}" style="width:60px" oninput="if(!$('eaAvatarImage').value){$('eaAvatarPreview').textContent=this.value||'🤖';$('eaAvatarPreview').innerHTML=''}" ${isSys?'disabled':''}>
|
|
227
|
+
<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':''}>
|
|
228
|
+
📷 上传图片
|
|
229
|
+
</label>
|
|
230
|
+
${a.avatar_image?'<button class="btn btn-sm btn-ghost" onclick="removeAvatarImage("'+escHtml(path)+'")">🗑️ 移除图片</button>':''}
|
|
231
|
+
</div>
|
|
232
|
+
<div id="eaCropArea" style="display:none;margin-top:8px">
|
|
233
|
+
<div style="position:relative;display:inline-block">
|
|
234
|
+
<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')">
|
|
235
|
+
<div id="eaCropOverlay" style="position:absolute;border:2px dashed #4f46e5;background:rgba(79,70,229,0.15);display:none;pointer-events:none"></div>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="flex gap-8 mt-8">
|
|
238
|
+
<button class="btn btn-sm btn-primary" onclick="confirmAvatarCrop('ea','${escHtml(path)}')">✂️ 裁剪并使用</button>
|
|
239
|
+
<button class="btn btn-sm btn-ghost" onclick="cancelAvatarCrop('ea')">取消</button>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
<input type="hidden" id="eaAvatarImage" value="${escHtml(a.avatar_image||'')}">
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
<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>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="form-group"><label>绑定模型</label><select id="eaModelId"><option value="">使用全局默认</option>${modelOpts}</select></div>
|
|
249
|
+
<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>
|
|
250
|
+
<div class="form-row">
|
|
251
|
+
<div class="form-group"><label>Agent ID <span style="color:var(--text2)">(只读)</span></label><input id="eaId" value="${escHtml(a.id||'-')}" disabled></div>
|
|
252
|
+
<div class="form-group"><label>创建时间 <span style="color:var(--text2)">(只读)</span></label><input id="eaCreated" value="${fmtDate(a.created_at)}" disabled></div>
|
|
253
|
+
</div>
|
|
254
|
+
<div class="form-group"><label>工作目录</label><input id="eaWorkDir" value="${escHtml(a.work_dir||'')}" placeholder="留空使用默认"></div>
|
|
255
|
+
<div class="form-group"><label>系统提示</label><textarea id="eaPrompt" rows="4" ${isSys?'disabled':''}></textarea></div>
|
|
256
|
+
<div class="flex gap-8 mt-16">
|
|
257
|
+
<button class="btn btn-primary" onclick="doSaveAgent('${escHtml(path)}')">保存</button>
|
|
258
|
+
<button class="btn btn-ghost" onclick="closeModal()">关闭</button>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<!-- 人格系统 -->
|
|
263
|
+
<div id="atSoul" class="hidden">
|
|
264
|
+
<div class="form-group"><label>soul.md</label><textarea id="eaSoul" rows="10" style="min-height:200px"></textarea>
|
|
265
|
+
<button class="btn btn-sm btn-primary mt-8" onclick="doSaveSoul('${escHtml(path)}')">保存 Soul</button></div>
|
|
266
|
+
<div class="form-group"><label>identity.md</label><textarea id="eaIdentity" rows="10" style="min-height:200px"></textarea>
|
|
267
|
+
<button class="btn btn-sm btn-primary mt-8" onclick="doSaveIdentity('${escHtml(path)}')">保存 Identity</button></div>
|
|
268
|
+
<div class="form-group"><label>user.md</label><textarea id="eaUser" rows="6" style="min-height:120px"></textarea>
|
|
269
|
+
<button class="btn btn-sm btn-primary mt-8" onclick="doSaveUser('${escHtml(path)}')">保存 User</button></div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<!-- 知识库 -->
|
|
273
|
+
<div id="atKB" class="hidden">
|
|
274
|
+
<div id="kbContent"><div class="empty">加载中...</div></div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<!-- 会话历史 -->
|
|
278
|
+
<div id="atSessions" class="hidden">
|
|
279
|
+
<div id="sessionsContent"><div class="empty">加载中...</div></div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<!-- 设置 -->
|
|
283
|
+
<div id="atSettings" class="hidden">
|
|
284
|
+
<div class="form-group"><label>启用状态</label>
|
|
285
|
+
<label class="toggle"><input type="checkbox" id="eaEnabled" ${a.enabled!==false?'checked':''}><span class="slider"></span></label></div>
|
|
286
|
+
<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>
|
|
287
|
+
<div class="flex gap-8 mt-16">
|
|
288
|
+
<button class="btn btn-primary" onclick="doSaveAgentSettings('${escHtml(path)}')">保存设置</button>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<!-- 权限 -->
|
|
293
|
+
<div id="atPerms" class="hidden">
|
|
294
|
+
<div id="atPermsContent"><div class="empty">加载中...</div></div>
|
|
295
|
+
</div>
|
|
296
|
+
</div></div>`;
|
|
297
|
+
// [v1.20.2] 安全设置 textarea 内容(避免模板字符串注入和 HTML 实体问题)
|
|
298
|
+
if($('eaPrompt'))$('eaPrompt').value=a.system_prompt||'';
|
|
299
|
+
if($('eaSoul'))$('eaSoul').value=a.soul||'';
|
|
300
|
+
if($('eaIdentity'))$('eaIdentity').value=a.identity||'';
|
|
301
|
+
if($('eaUser'))$('eaUser').value=a.user||'';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function agentTabSwitch(el,tabId){
|
|
305
|
+
el.parentElement.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
|
306
|
+
el.classList.add('active');
|
|
307
|
+
const allTabs=['atBasic','atSoul','atKB','atSessions','atSettings','atPerms'];
|
|
308
|
+
allTabs.forEach(id=>$(id).classList.toggle('hidden',id!==tabId));
|
|
309
|
+
// Lazy load
|
|
310
|
+
if(tabId==='atKB')loadAgentKB();
|
|
311
|
+
if(tabId==='atSessions')loadAgentSessions();
|
|
312
|
+
if(tabId==='atPerms')loadAgentPerms();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function doSaveAgent(path){
|
|
316
|
+
const r=await api(`/api/agents/${encodeURIComponent(path)}`,{method:'PUT',body:JSON.stringify({
|
|
317
|
+
name:$('eaName').value,
|
|
318
|
+
description:$('eaDesc').value,avatar_emoji:$('eaEmoji').value,avatar_color:$('eaColorText').value,
|
|
319
|
+
model_id:$('eaModelId').value,
|
|
320
|
+
backup_model_ids:Array.from($('eaBackupModels').selectedOptions).map(o=>o.value),
|
|
321
|
+
work_dir:$('eaWorkDir').value,system_prompt:$('eaPrompt').value,
|
|
322
|
+
avatar_image:$('eaAvatarImage')?.value||''
|
|
323
|
+
})});
|
|
324
|
+
if(r.error){showToast(r.error,'danger');return}
|
|
325
|
+
showToast('已保存','success');
|
|
326
|
+
// [v1.20.2] 如果 agent 被重命名,更新当前编辑路径,防止后续保存出现 not found
|
|
327
|
+
if(r.renamed_to){window._currentEditAgentPath=r.renamed_to}
|
|
328
|
+
renderAgents();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function doSaveAgentSettings(path){
|
|
332
|
+
const r=await api(`/api/agents/${encodeURIComponent(path)}`,{method:'PUT',body:JSON.stringify({
|
|
333
|
+
enabled:$('eaEnabled').checked,department:$('eaDept').value
|
|
334
|
+
})});
|
|
335
|
+
if(r.error){showToast(r.error,'danger');return}
|
|
336
|
+
showToast('设置已保存','success');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
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')}
|
|
340
|
+
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')}
|
|
341
|
+
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')}
|
|
342
|
+
|
|
343
|
+
async function loadAgentKB(){
|
|
344
|
+
// Find current agent path from the modal title or store it
|
|
345
|
+
const title=$('editAgentTabs')?.parentElement?.querySelector('h3')?.textContent||'';
|
|
346
|
+
const match=title.match(/🧠\s+(.+)/);
|
|
347
|
+
if(!match)return;
|
|
348
|
+
// We need the agent path - store it during openEditAgentModal
|
|
349
|
+
const path=window._currentEditAgentPath;
|
|
350
|
+
if(!path)return;
|
|
351
|
+
const kb=await api(`/api/agents/${encodeURIComponent(path)}/knowledge`);
|
|
352
|
+
const files=Array.isArray(kb)?kb:(kb?.files||[]);
|
|
353
|
+
let html=`<div class="flex justify-between items-center mb-16">
|
|
354
|
+
<h4 style="font-size:14px;color:var(--text2)">知识库文件 (${files.length})</h4>
|
|
355
|
+
<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>`;
|
|
356
|
+
if(files.length===0){html+='<div class="empty">暂无知识库文件</div>';}
|
|
357
|
+
else{html+='<div class="table-wrap"><table><tr><th>文件名</th><th>大小</th><th>操作</th></tr>';
|
|
358
|
+
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>
|
|
359
|
+
<td><button class="btn btn-sm btn-ghost" onclick="viewAgentKB('${escHtml(path)}','${escHtml(f.name||f.filename||'')}')">查看</button>
|
|
360
|
+
<button class="btn btn-sm btn-danger" onclick="deleteAgentKB('${escHtml(path)}','${escHtml(f.name||f.filename||'')}')">删除</button></td></tr>`}
|
|
361
|
+
html+='</table></div>';}
|
|
362
|
+
$('kbContent').innerHTML=html;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function uploadAgentKB(path,folderMode){
|
|
366
|
+
const input=document.createElement('input');input.type='file';input.multiple=true;
|
|
367
|
+
if(folderMode){input.webkitdirectory=true;}
|
|
368
|
+
input.onchange=async()=>{
|
|
369
|
+
const fd=new FormData();
|
|
370
|
+
for(const f of input.files){
|
|
371
|
+
if(f.webkitRelativePath){fd.append('files',f,{headers:{'X-File-Path':f.webkitRelativePath}});}
|
|
372
|
+
else{fd.append('files',f);}
|
|
373
|
+
}
|
|
374
|
+
showToast('正在上传...','info');
|
|
375
|
+
const r=await fetch(API+`/api/agents/${encodeURIComponent(path)}/knowledge/upload`,{method:'POST',body:fd});
|
|
376
|
+
const data=await r.json();
|
|
377
|
+
if(data.error){showToast(data.error,'danger');return}
|
|
378
|
+
const total=data.results?data.results.length:0;
|
|
379
|
+
const okCount=data.results?data.results.filter(x=>x.ok).length:0;
|
|
380
|
+
showToast(`上传完成: ${okCount}/${total} 文件成功`,okCount===total?'success':'warning');
|
|
381
|
+
loadAgentKB(path);
|
|
382
|
+
};input.click();
|
|
383
|
+
}
|
|
384
|
+
async function viewAgentKB(path,filename){
|
|
385
|
+
const r=await fetch(API+'/api/agents/'+encodeURIComponent(path)+'/knowledge/file?path='+encodeURIComponent(filename));
|
|
386
|
+
const ct=r.headers.get('content-type')||'';
|
|
387
|
+
if(ct.includes('application/json')){
|
|
388
|
+
const data=await r.json();
|
|
389
|
+
if(data.error){showToast(data.error,'danger');return}
|
|
390
|
+
const content=typeof data==='string'?data:(data.content||JSON.stringify(data,null,2));
|
|
391
|
+
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:800px">
|
|
392
|
+
<h3>${escHtml(filename)}</h3>
|
|
393
|
+
<div class="log-viewer" style="max-height:60vh;white-space:pre-wrap">${escHtml(content.slice(0,5000))}${content.length>5000?'\n...(已截断)':''}</div>
|
|
394
|
+
<div class="flex gap-8 mt-16"><button class="btn btn-ghost" onclick="closeModal()">关闭</button></div>
|
|
395
|
+
</div></div>`;
|
|
396
|
+
} else {
|
|
397
|
+
// 二进制文件 — 下载
|
|
398
|
+
const blob=await r.blob();
|
|
399
|
+
const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=filename;a.click();
|
|
400
|
+
URL.revokeObjectURL(a.href);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function deleteAgentKB(path,filename){
|
|
404
|
+
if(!confirm('确认删除 '+filename+'?'))return;
|
|
405
|
+
await api(`/api/agents/${encodeURIComponent(path)}/knowledge?path=${encodeURIComponent(filename)}`,{method:'DELETE'});
|
|
406
|
+
showToast('已删除','success');loadAgentKB(path);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function loadAgentSessions(){
|
|
410
|
+
const path=window._currentEditAgentPath;if(!path)return;
|
|
411
|
+
const data=await api(`/api/agents/${encodeURIComponent(path)}/sessions`);
|
|
412
|
+
const sessions=Array.isArray(data)?data:(data?.sessions||[]);
|
|
413
|
+
let html=`<div class="flex justify-between items-center mb-16"><h4 style="font-size:14px;color:var(--text2)">会话 (${sessions.length})</h4></div>`;
|
|
414
|
+
if(sessions.length===0){html+='<div class="empty">暂无会话</div>';}
|
|
415
|
+
else{html+='<div class="table-wrap"><table><tr><th>会话</th><th>消息数</th><th>最后活动</th><th></th></tr>';
|
|
416
|
+
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>`}
|
|
417
|
+
html+='</table></div>';}
|
|
418
|
+
$('sessionsContent').innerHTML=html;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function viewSessionMsgs(sid){
|
|
422
|
+
const msgs=await api(`/api/sessions/${encodeURIComponent(sid)}/messages?limit=100`);
|
|
423
|
+
if(!Array.isArray(msgs)){$('sessionsContent').innerHTML='<div class="empty">加载失败</div>';return;}
|
|
424
|
+
let html='<h4 style="margin-bottom:12px">会话消息 <span class="badge badge-blue">'+msgs.length+' 条</span></h4>';
|
|
425
|
+
html+='<div style="max-height:500px;overflow-y:auto">';
|
|
426
|
+
for(const m of msgs){
|
|
427
|
+
const role=m.role||'assistant';
|
|
428
|
+
const key=m.key||'';
|
|
429
|
+
const content=m.content||'';
|
|
430
|
+
const time=(m.time||'').slice(0,19);
|
|
431
|
+
if(role==='tool'){
|
|
432
|
+
const isResult=key==='tool_result';
|
|
433
|
+
const isCall=key==='tool_call';
|
|
434
|
+
const icon=isResult?'📋':(isCall?'⚙️':'🔧');
|
|
435
|
+
const label=isResult?'工具执行结果':(isCall?'工具调用':'工具过程');
|
|
436
|
+
const isOk=isResult&&!content.includes('失败');
|
|
437
|
+
const badge=isResult?`<span class="badge ${isOk?'badge-green':'badge-red'}" style="margin-left:6px">${isOk?'成功':'失败'}</span>`:'';
|
|
438
|
+
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>`;
|
|
439
|
+
}else{
|
|
440
|
+
const isUser=role==='user';
|
|
441
|
+
const bg=isUser?'var(--primary)':'var(--surface2)';
|
|
442
|
+
const fg=isUser?'#fff':'var(--text)';
|
|
443
|
+
const align=isUser?'text-align:right':'';
|
|
444
|
+
const avatar=isUser?'👤':'🤖';
|
|
445
|
+
const displayContent=content.length>500?content.slice(0,500)+'...':content;
|
|
446
|
+
html+=`<div style="margin:8px 0;display:flex;gap:8px;${align}">`;
|
|
447
|
+
if(!isUser)html+=`<div style="flex-shrink:0;font-size:16px;margin-top:2px">${avatar}</div>`;
|
|
448
|
+
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>`;
|
|
449
|
+
if(isUser)html+=`<div style="flex-shrink:0;font-size:16px;margin-top:2px">${avatar}</div>`;
|
|
450
|
+
html+=`</div>`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
html+='</div><div class="flex gap-8 mt-8"><button class="btn btn-ghost" onclick="loadAgentSessions()">返回</button></div>';
|
|
454
|
+
$('sessionsContent').innerHTML=html;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function loadAgentPerms(){
|
|
458
|
+
const path=window._currentEditAgentPath;if(!path)return;
|
|
459
|
+
const [agentPerms,globalPerms,agentInfo]=await Promise.all([
|
|
460
|
+
api('/api/permissions/'+encodeURIComponent(path)),
|
|
461
|
+
api('/api/permissions'),
|
|
462
|
+
api(`/api/agents/${encodeURIComponent(path)}`)
|
|
463
|
+
]);
|
|
464
|
+
const perms=globalPerms.all_permissions||[];
|
|
465
|
+
const labels=globalPerms.labels||{};
|
|
466
|
+
const defaults=globalPerms.defaults||{};
|
|
467
|
+
const ap=agentPerms.permissions||{};
|
|
468
|
+
const execMode=agentInfo.execution_mode||'sandbox';
|
|
469
|
+
const isSys=!!agentInfo.system;
|
|
470
|
+
|
|
471
|
+
// ── 执行模式卡片 ──
|
|
472
|
+
let html='<div class="card" style="margin-bottom:16px">';
|
|
473
|
+
html+='<h3 style="font-size:14px;color:var(--text2);margin-bottom:8px">⚡ 执行环境</h3>';
|
|
474
|
+
html+='<p style="color:var(--text2);font-size:12px;margin-bottom:12px">选择 Agent 代码执行的运行环境。沙盒模式隔离更安全,本机模式可使用全部本地资源。</p>';
|
|
475
|
+
html+='<div style="display:flex;gap:12px">';
|
|
476
|
+
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()">`;
|
|
477
|
+
html+=`<input type="radio" name="permExecMode" id="permExecSandbox" value="sandbox" ${execMode==='sandbox'?'checked':''} style="display:none">`;
|
|
478
|
+
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>`;
|
|
479
|
+
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()">`;
|
|
480
|
+
html+=`<input type="radio" name="permExecMode" id="permExecLocal" value="local" ${execMode==='local'?'checked':''} style="display:none">`;
|
|
481
|
+
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>`;
|
|
482
|
+
html+='</div></div>';
|
|
483
|
+
|
|
484
|
+
// ── 功能权限卡片 ──
|
|
485
|
+
html+='<div class="card" style="margin-bottom:16px">';
|
|
486
|
+
html+='<h3 style="font-size:14px;color:var(--text2);margin-bottom:8px">🔑 功能权限</h3>';
|
|
487
|
+
html+='<p style="color:var(--text2);font-size:12px;margin-bottom:12px">精细控制 Agent 的各项能力。未设置的项目将使用全局默认值。</p>';
|
|
488
|
+
html+='<div class="grid grid-3" style="gap:12px">';
|
|
489
|
+
for(const p of perms){
|
|
490
|
+
const label=labels[p]||p;
|
|
491
|
+
const defVal=defaults[p]!==false;
|
|
492
|
+
const curVal=ap[p]!==undefined?ap[p]:defVal;
|
|
493
|
+
const checked=curVal?'checked':'';
|
|
494
|
+
const isDefault=ap[p]===undefined?'<span style=\"color:var(--text3);font-size:11px;margin-left:4px\">(默认)</span>':'';
|
|
495
|
+
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>`;
|
|
496
|
+
}
|
|
497
|
+
html+='</div></div>';
|
|
498
|
+
|
|
499
|
+
// ── 操作按钮 ──
|
|
500
|
+
html+='<div class="flex gap-8">';
|
|
501
|
+
html+='<button class="btn btn-primary" onclick="saveAgentPermsFromTab()">💾 保存全部</button>';
|
|
502
|
+
html+='<button class="btn btn-ghost" onclick="resetAgentPermsFromTab()">🔄 重置功能权限为默认</button>';
|
|
503
|
+
html+='</div>';
|
|
504
|
+
$('atPermsContent').innerHTML=html;
|
|
505
|
+
}
|
|
506
|
+
function updateExecModeUI(){
|
|
507
|
+
const sandbox=document.getElementById('permExecSandbox');
|
|
508
|
+
const local=document.getElementById('permExecLocal');
|
|
509
|
+
if(!sandbox||!local)return;
|
|
510
|
+
const isLocal=local.checked;
|
|
511
|
+
[sandbox.parentElement,local.parentElement].forEach(el=>{
|
|
512
|
+
const radio=el.querySelector('input[type=radio]');
|
|
513
|
+
if(radio.checked){el.style.borderColor='var(--primary)';el.style.background='var(--accent-light)'}
|
|
514
|
+
else{el.style.borderColor='var(--border)';el.style.background='transparent'}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
async function saveAgentPermsFromTab(){
|
|
518
|
+
const path=window._currentEditAgentPath;if(!path)return;
|
|
519
|
+
// 1. 保存执行模式
|
|
520
|
+
const execSandbox=document.getElementById('permExecSandbox');
|
|
521
|
+
const execLocal=document.getElementById('permExecLocal');
|
|
522
|
+
const newExecMode=(execSandbox&&execSandbox.checked)?'sandbox':((execLocal&&execLocal.checked)?'local':'sandbox');
|
|
523
|
+
const execR=await api(`/api/agents/${encodeURIComponent(path)}`,{method:'PUT',body:JSON.stringify({execution_mode:newExecMode})});
|
|
524
|
+
if(execR.error){showToast('执行模式保存失败: '+execR.error,'danger');return}
|
|
525
|
+
|
|
526
|
+
// 2. 保存功能权限
|
|
527
|
+
const [agentPerms,globalPerms]=await Promise.all([
|
|
528
|
+
api('/api/permissions/'+encodeURIComponent(path)),
|
|
529
|
+
api('/api/permissions')
|
|
530
|
+
]);
|
|
531
|
+
const perms=globalPerms.all_permissions||[];
|
|
532
|
+
const defaults=globalPerms.defaults||{};
|
|
533
|
+
const data={};
|
|
534
|
+
for(const p of perms){
|
|
535
|
+
const el=document.getElementById('agent_perm_'+p);
|
|
536
|
+
if(!el)continue;
|
|
537
|
+
const defVal=defaults[p]!==false;
|
|
538
|
+
const curVal=el.checked;
|
|
539
|
+
if(curVal!==defVal){data[p]=curVal}
|
|
540
|
+
}
|
|
541
|
+
const r=await api('/api/permissions/'+encodeURIComponent(path),{method:'PUT',body:JSON.stringify(data)});
|
|
542
|
+
if(r.error){showToast('权限保存失败: '+r.error,'danger');return}
|
|
543
|
+
showToast('权限配置已保存','success');
|
|
544
|
+
}
|
|
545
|
+
async function resetAgentPermsFromTab(){
|
|
546
|
+
const path=window._currentEditAgentPath;if(!path)return;
|
|
547
|
+
if(!confirm('确认将此 Agent 的权限重置为全局默认值?'))return;
|
|
548
|
+
const r=await api('/api/permissions/'+encodeURIComponent(path),{method:'DELETE'});
|
|
549
|
+
if(r.error){showToast(r.error,'danger');return}
|
|
550
|
+
showToast('已重置为默认权限','success');
|
|
551
|
+
loadAgentPerms();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function confirmDeleteAgent(path,name){
|
|
555
|
+
showConfirm('删除 Agent','确认删除 Agent "'+escHtml(name)+'" 吗?\n\n删除后将同时清理:\n 📁 工作目录及所有文件\n 💬 该 Agent 的所有会话历史\n 🧠 相关记忆数据\n 🔗 所有子 Agent\n\n⚠️ 此操作不可撤销!',async()=>{
|
|
556
|
+
const r=await api(`/api/agents/${encodeURIComponent(path)}`,{method:'DELETE'});
|
|
557
|
+
if(r.error){showToast(r.error,'danger');return}
|
|
558
|
+
closeModal();showToast('已删除','success');renderAgents();
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 直接以执行模式打开与指定 Agent 的对话
|
|
563
|
+
function chatWithAgent(path){
|
|
564
|
+
window.location.href='/ui/chat/chat_container.html?agent='+encodeURIComponent(path)+'&mode=exec';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
|
|
568
|
+
window._adminRenderers['agents'] = renderAgents;
|
|
569
|
+
} catch(e) { console.error('[admin-agents] load error:', e); }
|
|
570
|
+
})();
|