myagent-ai 1.18.6 → 1.18.7
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/package.json +1 -1
- package/web/api_server.py +99 -17
- package/web/ui/chat/chat_main.js +11 -1
- package/web/ui/index.html +277 -79
package/package.json
CHANGED
package/web/api_server.py
CHANGED
|
@@ -373,6 +373,7 @@ class ApiServer:
|
|
|
373
373
|
r.add_get("/api/workdir", self.handle_get_workdir)
|
|
374
374
|
# ── Task Plan ──
|
|
375
375
|
r.add_get("/api/task-plan", self.handle_get_task_plan)
|
|
376
|
+
r.add_get("/api/task-plan/all", self.handle_get_all_task_plans)
|
|
376
377
|
r.add_put("/api/task-plan", self.handle_update_task_plan)
|
|
377
378
|
r.add_post("/api/task-plan", self.handle_add_task_item)
|
|
378
379
|
r.add_delete("/api/task-plan/{idx:int}", self.handle_delete_task_item)
|
|
@@ -2103,6 +2104,36 @@ window.toggleFullscreen = function() {{
|
|
|
2103
2104
|
self._task_list_store[store_key] = tasks
|
|
2104
2105
|
return web.json_response({"ok": True, "tasks": tasks, "session": session_id})
|
|
2105
2106
|
|
|
2107
|
+
async def handle_get_all_task_plans(self, request):
|
|
2108
|
+
"""GET /api/task-plan/all - 获取所有活跃的 task plan(用于后台管理任务列表)。
|
|
2109
|
+
|
|
2110
|
+
返回所有 _task_list_store 中的非空任务列表,包含 session_id/agent_path 信息。
|
|
2111
|
+
"""
|
|
2112
|
+
all_plans = []
|
|
2113
|
+
for key, tasks in self._task_list_store.items():
|
|
2114
|
+
if not tasks:
|
|
2115
|
+
continue
|
|
2116
|
+
# 统计状态
|
|
2117
|
+
status_counts = {"pending": 0, "running": 0, "done": 0}
|
|
2118
|
+
for t in tasks:
|
|
2119
|
+
s = t.get("status", "pending")
|
|
2120
|
+
status_counts[s] = status_counts.get(s, 0) + 1
|
|
2121
|
+
# 判断 key 类型
|
|
2122
|
+
is_session = key.startswith(("web_", "cli_", "group_", "tg_", "discord_", "feishu_"))
|
|
2123
|
+
all_plans.append({
|
|
2124
|
+
"key": key,
|
|
2125
|
+
"type": "session" if is_session else "agent",
|
|
2126
|
+
"total": len(tasks),
|
|
2127
|
+
"pending": status_counts["pending"],
|
|
2128
|
+
"running": status_counts["running"],
|
|
2129
|
+
"done": status_counts["done"],
|
|
2130
|
+
"tasks": tasks,
|
|
2131
|
+
"label": key.split("_web_")[0][:30] if is_session else key,
|
|
2132
|
+
})
|
|
2133
|
+
# 按 total 降序
|
|
2134
|
+
all_plans.sort(key=lambda x: x["total"], reverse=True)
|
|
2135
|
+
return web.json_response({"plans": all_plans, "total_keys": len(self._task_list_store)})
|
|
2136
|
+
|
|
2106
2137
|
async def handle_shutdown(self, request):
|
|
2107
2138
|
self.core._running = False
|
|
2108
2139
|
asyncio.create_task(self.core.shutdown())
|
|
@@ -2830,13 +2861,31 @@ window.toggleFullscreen = function() {{
|
|
|
2830
2861
|
async def handle_get_executor(self, request):
|
|
2831
2862
|
info = self.core.executor.get_execution_info() if self.core.executor else {}
|
|
2832
2863
|
cfg = self.core.config.executor if self.core.config else None
|
|
2864
|
+
# [v1.18.7] 附加执行锁状态和沙盒说明
|
|
2865
|
+
lock = self._execution_lock
|
|
2833
2866
|
return web.json_response({
|
|
2834
2867
|
**info,
|
|
2835
2868
|
"timeout": cfg.timeout if cfg else 300,
|
|
2836
2869
|
"auto_fix": cfg.auto_fix if cfg else True,
|
|
2837
2870
|
"max_output_length": cfg.max_output_length if cfg else 50000,
|
|
2871
|
+
# 执行锁信息
|
|
2872
|
+
"lock": {
|
|
2873
|
+
"locked": lock["locked"],
|
|
2874
|
+
"locked_by": lock["locked_by"],
|
|
2875
|
+
"locked_at": lock["locked_at"],
|
|
2876
|
+
},
|
|
2877
|
+
# 沙盒模式说明
|
|
2878
|
+
"sandbox_desc": self._sandbox_description(info.get("mode"), info.get("sandbox_type"), info.get("docker_available")),
|
|
2838
2879
|
})
|
|
2839
2880
|
|
|
2881
|
+
def _sandbox_description(self, mode, sandbox_type, docker_available):
|
|
2882
|
+
"""生成沙盒模式说明文本"""
|
|
2883
|
+
if mode != "sandbox":
|
|
2884
|
+
return "本机模式: 代码直接在宿主机运行,拥有完整系统权限。多个 Agent 共享全局执行锁,同一时间只有一个 Agent 可以执行代码。"
|
|
2885
|
+
if docker_available:
|
|
2886
|
+
return "Docker 沙盒: 代码在 Docker 容器中运行,通过网络隔离保证安全性。容器使用指定镜像,执行完毕后自动销毁。不走全局锁,支持多 Agent 并发。"
|
|
2887
|
+
return "轻量级进程沙盒: Docker 不可用时的降级方案。代码在临时工作目录中通过子进程执行,提供目录隔离。不走全局锁,支持多 Agent 并发。安全性低于 Docker 沙盒。"
|
|
2888
|
+
|
|
2840
2889
|
async def handle_update_executor(self, request):
|
|
2841
2890
|
data = await request.json()
|
|
2842
2891
|
cfg_path = self.core.config_mgr._config_file
|
|
@@ -3852,35 +3901,65 @@ window.toggleFullscreen = function() {{
|
|
|
3852
3901
|
lines = int(request.query.get("lines", "200"))
|
|
3853
3902
|
level = request.query.get("level", "").upper()
|
|
3854
3903
|
logs = []
|
|
3855
|
-
|
|
3904
|
+
# [v1.18.7] 遍历所有 .log 文件(不仅限于 myagent*.log),按修改时间倒序
|
|
3905
|
+
try:
|
|
3906
|
+
all_log_files = sorted(
|
|
3907
|
+
[f for f in log_dir.glob("*.log") if f.is_file()],
|
|
3908
|
+
key=lambda f: f.stat().st_mtime,
|
|
3909
|
+
reverse=True,
|
|
3910
|
+
)
|
|
3911
|
+
except Exception:
|
|
3912
|
+
all_log_files = []
|
|
3913
|
+
for lf in all_log_files:
|
|
3856
3914
|
try:
|
|
3857
3915
|
text = lf.read_text(encoding="utf-8", errors="ignore")
|
|
3858
3916
|
for line in text.strip().split("\n")[-lines:]:
|
|
3859
|
-
if level and level not in line:
|
|
3917
|
+
if level and level not in line:
|
|
3918
|
+
continue
|
|
3860
3919
|
logs.append(line)
|
|
3861
|
-
if len(logs) >= lines:
|
|
3862
|
-
|
|
3920
|
+
if len(logs) >= lines:
|
|
3921
|
+
break
|
|
3922
|
+
except Exception:
|
|
3923
|
+
pass
|
|
3863
3924
|
return web.json_response(logs[-lines:])
|
|
3864
3925
|
|
|
3865
3926
|
async def handle_log_stream(self, request):
|
|
3866
3927
|
resp = web.StreamResponse()
|
|
3867
3928
|
resp.content_type = "text/event-stream"
|
|
3868
3929
|
resp.headers["Cache-Control"] = "no-cache"
|
|
3930
|
+
resp.headers["Connection"] = "keep-alive"
|
|
3869
3931
|
await resp.prepare(request)
|
|
3870
|
-
|
|
3932
|
+
log_dir = self.core.config_mgr.logs_dir
|
|
3933
|
+
# [v1.18.7] 自动发现最新的日志文件而非硬编码 myagent.log
|
|
3871
3934
|
last_pos = 0
|
|
3935
|
+
last_file = None
|
|
3872
3936
|
try:
|
|
3873
3937
|
while True:
|
|
3874
3938
|
try:
|
|
3875
|
-
|
|
3876
|
-
|
|
3939
|
+
# 每 5 秒重新扫描最新日志文件(应对日志轮转)
|
|
3940
|
+
current_file = None
|
|
3941
|
+
if log_dir.exists():
|
|
3942
|
+
candidates = [f for f in log_dir.glob("*.log") if f.is_file()]
|
|
3943
|
+
if candidates:
|
|
3944
|
+
current_file = max(candidates, key=lambda f: f.stat().st_mtime)
|
|
3945
|
+
if current_file:
|
|
3946
|
+
# 日志文件切换时重置位置
|
|
3947
|
+
if last_file and current_file.resolve() != last_file.resolve():
|
|
3948
|
+
last_pos = 0
|
|
3949
|
+
last_file = current_file
|
|
3950
|
+
size = current_file.stat().st_size
|
|
3877
3951
|
if size > last_pos:
|
|
3878
|
-
with open(
|
|
3879
|
-
f.seek(last_pos)
|
|
3880
|
-
|
|
3881
|
-
|
|
3952
|
+
with open(current_file, "r", encoding="utf-8", errors="ignore") as f:
|
|
3953
|
+
f.seek(last_pos)
|
|
3954
|
+
new_data = f.read()
|
|
3955
|
+
last_pos = size
|
|
3956
|
+
if new_data:
|
|
3957
|
+
await resp.write(f"data: {json.dumps(new_data.strip())}\n\n")
|
|
3958
|
+
except Exception:
|
|
3959
|
+
pass
|
|
3882
3960
|
await asyncio.sleep(0.5)
|
|
3883
|
-
except asyncio.CancelledError:
|
|
3961
|
+
except asyncio.CancelledError:
|
|
3962
|
+
pass
|
|
3884
3963
|
return resp
|
|
3885
3964
|
|
|
3886
3965
|
# ── 配置管理 (热重载 / 导入 / 导出) ──
|
|
@@ -6127,11 +6206,14 @@ window.toggleFullscreen = function() {{
|
|
|
6127
6206
|
break
|
|
6128
6207
|
image_data.extend(chunk)
|
|
6129
6208
|
|
|
6130
|
-
#
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6209
|
+
# 解析裁剪参数([v1.18.7] 防止 NaN 传入导致 ValueError)
|
|
6210
|
+
try:
|
|
6211
|
+
crop_x = int(float(request.query.get("crop_x", 0)))
|
|
6212
|
+
crop_y = int(float(request.query.get("crop_y", 0)))
|
|
6213
|
+
crop_w = int(float(request.query.get("crop_w", 0)))
|
|
6214
|
+
crop_h = int(float(request.query.get("crop_h", 0)))
|
|
6215
|
+
except (ValueError, TypeError):
|
|
6216
|
+
crop_x = crop_y = crop_w = crop_h = 0
|
|
6135
6217
|
out_size = min(int(request.query.get("size", 128)), 512)
|
|
6136
6218
|
|
|
6137
6219
|
# 使用 Pillow 处理图片
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -1871,14 +1871,24 @@ function renderSessions(filter = '') {
|
|
|
1871
1871
|
s.id !== '__new__' && (!fl || s.name.toLowerCase().includes(fl) || s.id.toLowerCase().includes(fl))
|
|
1872
1872
|
);
|
|
1873
1873
|
|
|
1874
|
+
const agent = findAgentByPath(state.activeAgent);
|
|
1874
1875
|
list.innerHTML = filtered.map(s => {
|
|
1875
1876
|
const previewText = s.preview
|
|
1876
1877
|
? escapeHtml(s.preview.length > 40 ? s.preview.slice(0, 40) + '...' : s.preview)
|
|
1877
1878
|
: (s.messages > 0 ? s.messages + ' 条消息' : '暂无消息');
|
|
1879
|
+
// [v1.18.7] 用 agent 头像替代固定 💬 图标,头像在前、会话名在后
|
|
1880
|
+
var _avatarContent = '';
|
|
1881
|
+
if (s.id === '__new__') {
|
|
1882
|
+
_avatarContent = '✨';
|
|
1883
|
+
} else if (agent && agent.avatar_image) {
|
|
1884
|
+
_avatarContent = '<img src="' + escapeHtml(agent.avatar_image) + '" style="width:100%;height:100%;object-fit:cover;border-radius:8px" onerror="this.outerHTML=\'' + escapeHtml(agent.avatar_emoji || '🤖') + '\'">';
|
|
1885
|
+
} else {
|
|
1886
|
+
_avatarContent = agent ? (agent.avatar_emoji || '🤖') : '💬';
|
|
1887
|
+
}
|
|
1878
1888
|
return `
|
|
1879
1889
|
<div class="session-item ${s.id === state.activeSessionId ? 'active' : ''}"
|
|
1880
1890
|
onclick="selectSession('${escapeHtml(s.id)}')" title="${escapeHtml(s.id)}">
|
|
1881
|
-
<div class="session-icon">${
|
|
1891
|
+
<div class="session-icon">${_avatarContent}</div>
|
|
1882
1892
|
<div class="session-info">
|
|
1883
1893
|
<div class="session-name">${escapeHtml(s.name)}</div>
|
|
1884
1894
|
<div class="session-preview">${previewText}</div>
|
package/web/ui/index.html
CHANGED
|
@@ -635,7 +635,8 @@ async function doCreateAgent(){
|
|
|
635
635
|
name,description:$('caDesc').value,avatar_emoji:$('caEmoji').value,
|
|
636
636
|
avatar_color:$('caColorText').value,execution_mode:$('caExecMode').value,
|
|
637
637
|
model_id:$('caModelId').value,system_prompt:$('caPrompt').value,
|
|
638
|
-
work_dir:$('caWorkDir').value,department:$('caDept').value
|
|
638
|
+
work_dir:$('caWorkDir').value,department:$('caDept').value,
|
|
639
|
+
avatar_image:$('caAvatarImage')?.value||''
|
|
639
640
|
})});
|
|
640
641
|
if(r.error){showToast(r.error,'danger');return}
|
|
641
642
|
closeModal();showToast('Agent 创建成功','success');renderAgents();
|
|
@@ -764,7 +765,8 @@ async function doSaveAgent(path){
|
|
|
764
765
|
description:$('eaDesc').value,avatar_emoji:$('eaEmoji').value,avatar_color:$('eaColorText').value,
|
|
765
766
|
model_id:$('eaModelId').value,
|
|
766
767
|
backup_model_ids:Array.from($('eaBackupModels').selectedOptions).map(o=>o.value),
|
|
767
|
-
work_dir:$('eaWorkDir').value,system_prompt:$('eaPrompt').value
|
|
768
|
+
work_dir:$('eaWorkDir').value,system_prompt:$('eaPrompt').value,
|
|
769
|
+
avatar_image:$('eaAvatarImage')?.value||''
|
|
768
770
|
})});
|
|
769
771
|
if(r.error){showToast(r.error,'danger');return}
|
|
770
772
|
showToast('已保存','success');renderAgents();
|
|
@@ -1625,29 +1627,63 @@ async function deleteModel(id,name){
|
|
|
1625
1627
|
|
|
1626
1628
|
// ========== Executor ==========
|
|
1627
1629
|
async function renderExecutor(){
|
|
1628
|
-
const e=await api('/api/executor');
|
|
1630
|
+
const e=await api('/api/executor');if(!e)return;
|
|
1631
|
+
const isSandbox=e.mode==='sandbox';const dockerOk=e.docker_available;
|
|
1632
|
+
const lock=e.lock||{};
|
|
1633
|
+
const sandboxType=e.sandbox_type||'';
|
|
1634
|
+
const sandboxDesc=e.sandbox_desc||'';
|
|
1629
1635
|
let html=`<div class="card"><h3>执行模式</h3>
|
|
1630
|
-
<div style="display:flex;gap:12px;margin-bottom:16px">
|
|
1631
|
-
<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">
|
|
1636
|
+
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
|
|
1637
|
+
<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">
|
|
1632
1638
|
<input type="radio" name="execMode" value="local" ${!isSandbox?'checked':''} onchange="switchMode('local')">
|
|
1633
1639
|
<div><strong>🖥️ 本机执行</strong><br><span style="font-size:12px;color:var(--text2)">直接在本机运行代码,功能完整,速度最快</span></div></label>
|
|
1634
|
-
<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">
|
|
1640
|
+
<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">
|
|
1635
1641
|
<input type="radio" name="execMode" value="sandbox" ${isSandbox?'checked':''} onchange="switchMode('sandbox')" ${!dockerOk?'disabled':''}>
|
|
1636
|
-
<div><strong>📦
|
|
1637
|
-
<div style="font-size:13px;color:var(--text2)
|
|
1642
|
+
<div><strong>📦 沙盒执行</strong><br><span style="font-size:12px;color:var(--text2)">${dockerOk?'Docker 隔离容器':'轻量级进程沙盒'}${!dockerOk?' (Docker 不可用)':''}</span></div></label></div>
|
|
1643
|
+
<div style="font-size:13px;color:var(--text2);display:flex;flex-wrap:wrap;gap:8px">
|
|
1644
|
+
<span>当前模式: <span class="badge ${isSandbox?'badge-yellow':'badge-green'}">${isSandbox?'沙盒':'本机'}</span></span>
|
|
1645
|
+
${isSandbox?`<span>沙盒类型: <span class="tag">${sandboxType==='docker'?'Docker':'轻量级进程'}</span></span>`:''}
|
|
1646
|
+
<span>Docker: <span class="badge ${dockerOk?'badge-green':'badge-red'}">${dockerOk?'可用':'不可用'}</span></span>
|
|
1647
|
+
<span>累计执行: <span class="tag">${e.execution_count||0} 次</span></span>
|
|
1648
|
+
</div>
|
|
1649
|
+
<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>
|
|
1650
|
+
</div>`;
|
|
1651
|
+
// 执行锁状态
|
|
1652
|
+
html+=`<div class="card"><h3>🔒 全局执行锁</h3>
|
|
1653
|
+
<div style="font-size:13px">
|
|
1654
|
+
${lock.locked?
|
|
1655
|
+
`<span class="badge badge-red">已锁定</span> <strong>${escHtml(lock.locked_by||'')}</strong> — 锁定于 ${escHtml(lock.locked_at||'')}
|
|
1656
|
+
<div style="margin-top:8px"><button class="btn btn-sm btn-danger" onclick="releaseLock()">🔓 释放锁</button></div>`:
|
|
1657
|
+
`<span class="badge badge-green">未锁定</span> — 所有 Agent 可自由运行`
|
|
1658
|
+
}
|
|
1659
|
+
<div style="margin-top:6px;font-size:12px;color:var(--text3)">${isSandbox?'沙盒模式不走全局锁,支持多 Agent 并发执行':'本机模式下多个 Agent 共享此锁,同一时间只有一个 Agent 可以执行代码'}</div>
|
|
1660
|
+
</div></div>`;
|
|
1661
|
+
// 沙盒设置
|
|
1638
1662
|
html+=`<div class="card"><h3>沙盒设置</h3><div class="form-row">
|
|
1639
|
-
<div class="form-group"><label>Docker 镜像</label><input id="sbImage" value="${e.sandbox_image||'python:3.12-slim'}"></div>
|
|
1640
|
-
<div class="form-group"><label>内存限制</label><input id="sbMemory" value="${e.sandbox_memory||'512m'}" placeholder="512m"></div>
|
|
1663
|
+
<div class="form-group"><label>Docker 镜像</label><input id="sbImage" value="${escHtml(e.sandbox_image||'python:3.12-slim')}"></div>
|
|
1664
|
+
<div class="form-group"><label>内存限制</label><input id="sbMemory" value="${escHtml(e.sandbox_memory||'512m')}" placeholder="512m"></div>
|
|
1641
1665
|
<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>
|
|
1642
1666
|
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveExecutor()">保存设置</button></div></div>`;
|
|
1667
|
+
// 执行参数
|
|
1643
1668
|
html+=`<div class="card"><h3>执行参数</h3><div class="form-row">
|
|
1644
1669
|
<div class="form-group"><label>超时时间 (秒)</label><input id="exTimeout" type="number" value="${e.timeout||300}"></div>
|
|
1645
|
-
<div class="form-group"><label
|
|
1646
|
-
<div class="form-group"><label
|
|
1670
|
+
<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>
|
|
1671
|
+
<div class="form-group"><label>最大输出长度</label><input id="exMaxOutput" type="number" value="${e.max_output_length||50000}"></div></div>
|
|
1672
|
+
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveExecutor()">保存参数</button></div></div>`;
|
|
1647
1673
|
$('content').innerHTML=html;
|
|
1648
1674
|
}
|
|
1649
|
-
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
|
|
1650
|
-
async function saveExecutor(){
|
|
1675
|
+
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();}}
|
|
1676
|
+
async function saveExecutor(){
|
|
1677
|
+
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)};
|
|
1678
|
+
const r=await api('/api/executor',{method:'PUT',body:JSON.stringify(body)});
|
|
1679
|
+
if(r.ok)showToast('已保存','success');else showToast('保存失败: '+(r.error||''),'danger');
|
|
1680
|
+
renderExecutor();
|
|
1681
|
+
}
|
|
1682
|
+
async function releaseLock(){
|
|
1683
|
+
const r=await api('/api/execution-lock',{method:'POST',body:JSON.stringify({action:'release'})});
|
|
1684
|
+
if(r.ok)showToast('锁已释放','success');else showToast('释放失败: '+(r.error||''),'danger');
|
|
1685
|
+
renderExecutor();
|
|
1686
|
+
}
|
|
1651
1687
|
|
|
1652
1688
|
// ========== Skills ==========
|
|
1653
1689
|
async function renderSkills(){
|
|
@@ -1719,95 +1755,238 @@ async function viewSkillDetail(name){
|
|
|
1719
1755
|
}
|
|
1720
1756
|
|
|
1721
1757
|
// ========== Files ==========
|
|
1758
|
+
var _workdirAgent=''; // 当前选中的 agent(空=全局工作目录)
|
|
1722
1759
|
async function renderFiles(){
|
|
1723
|
-
const wd=await api('/api/workdir')
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
<
|
|
1727
|
-
<
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1760
|
+
const [wd,agents]=await Promise.all([api('/api/workdir'),api('/api/agents').catch(()=>[])]);
|
|
1761
|
+
const agentList=Array.isArray(agents)?agents.filter(a=>!a.system&&a.path!=='default'):[];
|
|
1762
|
+
let html=`<div class="flex items-center gap-8 mb-16 flex-wrap">
|
|
1763
|
+
<span style="font-size:14px;color:var(--text2)">📁 工作目录: <strong>${escHtml(wd.path||'')}</strong></span>
|
|
1764
|
+
<select id="workdirAgentSelect" onchange="onWorkdirAgentChange()" style="width:auto">
|
|
1765
|
+
<option value="">全局工作目录</option>
|
|
1766
|
+
${agentList.map(a=>`<option value="${escHtml(a.path)}">${escHtml((a.avatar_emoji||'🤖')+' '+a.name)} (${escHtml(a.path)})</option>`).join('')}
|
|
1767
|
+
</select>
|
|
1768
|
+
<button class="btn btn-sm btn-ghost" onclick="changeWorkdir()">更改全局</button>
|
|
1769
|
+
<button class="btn btn-sm btn-ghost" onclick="renderFiles()">🔄 刷新</button></div>
|
|
1770
|
+
<div id="workdirContent">加载中...</div>`;
|
|
1771
|
+
$('content').innerHTML=html;
|
|
1772
|
+
loadWorkdirContent('');
|
|
1773
|
+
}
|
|
1774
|
+
function onWorkdirAgentChange(){
|
|
1775
|
+
_workdirAgent=$('workdirAgentSelect').value;
|
|
1776
|
+
loadWorkdirContent('');
|
|
1777
|
+
}
|
|
1778
|
+
async function loadWorkdirContent(subPath){
|
|
1779
|
+
const params=new URLSearchParams();
|
|
1780
|
+
if(subPath)params.set('path',subPath);
|
|
1781
|
+
const files=await api('/api/workdir/files?'+params.toString());
|
|
1782
|
+
const el=document.getElementById('workdirContent');
|
|
1783
|
+
if(!el)return;
|
|
1784
|
+
// 面包屑
|
|
1785
|
+
var bcHtml='';
|
|
1786
|
+
if(subPath){
|
|
1787
|
+
var parts=subPath.split('/');var crumbs=['<span style="cursor:pointer;color:var(--primary)" onclick="loadWorkdirContent(\'\')">根目录</span>'];var acc='';
|
|
1788
|
+
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>');}
|
|
1789
|
+
bcHtml=crumbs.join('');
|
|
1790
|
+
}else{bcHtml='<span style="color:var(--text3)">根目录</span>';}
|
|
1791
|
+
// Agent 工作目录信息
|
|
1792
|
+
if(_workdirAgent){
|
|
1793
|
+
var agentCfg=await api('/api/agents/'+encodeURIComponent(_workdirAgent)).catch(()=>null);
|
|
1794
|
+
if(agentCfg&&agentCfg.work_dir){
|
|
1795
|
+
bcHtml+=` <span style="margin-left:12px;font-size:11px;color:var(--text3)">Agent 工作目录: ${escHtml(agentCfg.work_dir)}</span>`;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
var html=`<div style="font-size:12px;margin-bottom:8px">${bcHtml}</div>`;
|
|
1799
|
+
html+='<div class="table-wrap"><table><tr><th>名称</th><th>大小</th><th></th></tr>';
|
|
1800
|
+
if(!files||!files.length){
|
|
1801
|
+
html+='<tr><td colspan="3" class="empty">目录为空</td></tr>';
|
|
1802
|
+
}else{
|
|
1803
|
+
// 排序:目录在前
|
|
1804
|
+
var dirs=files.filter(f=>f.type==='dir').sort((a,b)=>a.name.localeCompare(b.name));
|
|
1805
|
+
var fils=files.filter(f=>f.type==='file').sort((a,b)=>a.name.localeCompare(b.name));
|
|
1806
|
+
for(const d of dirs){
|
|
1807
|
+
var dp=d.path||d.name;
|
|
1808
|
+
html+=`<tr style="cursor:pointer" onclick="loadWorkdirContent('${escHtml(dp)}')"><td>📂 ${escHtml(d.name)}</td><td>-</td><td></td></tr>`;
|
|
1809
|
+
}
|
|
1810
|
+
for(const f of fils){
|
|
1811
|
+
var fp=f.path||f.name;
|
|
1812
|
+
var sizeStr=f.size>1048576?(f.size/1048576).toFixed(1)+' MB':f.size>1024?(f.size/1024).toFixed(1)+' KB':f.size+' B';
|
|
1813
|
+
html+=`<tr><td>📄 ${escHtml(f.name)}</td><td>${sizeStr}</td>
|
|
1814
|
+
<td><button class="btn btn-sm btn-ghost" onclick="downloadWorkdirFileGlobal('${escHtml(fp)}')">下载</button></td></tr>`;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
html+='</table></div>';
|
|
1818
|
+
el.innerHTML=html;
|
|
1819
|
+
}
|
|
1820
|
+
function downloadWorkdirFileGlobal(relPath){
|
|
1821
|
+
var link=document.createElement('a');
|
|
1822
|
+
link.href=API+'/api/workdir/download/'+encodeURIComponent(relPath);
|
|
1823
|
+
link.download='';
|
|
1824
|
+
document.body.appendChild(link);link.click();document.body.removeChild(link);
|
|
1825
|
+
}
|
|
1826
|
+
async function changeWorkdir(){const p=prompt('新路径:');if(!p)return;await api('/api/workdir',{method:'PUT',body:JSON.stringify({path:p})});showToast('已更新','success');renderFiles();}
|
|
1736
1827
|
|
|
1737
1828
|
// ========== Logs ==========
|
|
1738
1829
|
async function renderLogs(){
|
|
1739
|
-
let html=`<div class="flex gap-8 mb-16">
|
|
1740
|
-
<select id="logLines" style="width:auto"><option value="100">100 行</option><option value="500" selected>500 行</option><option value="1000">1000 行</option></select>
|
|
1830
|
+
let html=`<div class="flex gap-8 mb-16 flex-wrap">
|
|
1831
|
+
<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>
|
|
1832
|
+
<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>
|
|
1741
1833
|
<button class="btn btn-primary" onclick="loadLogs()">刷新</button>
|
|
1742
|
-
<button class="btn btn-ghost" onclick="toggleLogStream()" id="streamBtn"
|
|
1743
|
-
|
|
1834
|
+
<button class="btn btn-ghost" onclick="toggleLogStream()" id="streamBtn">▶ 实时日志</button>
|
|
1835
|
+
<button class="btn btn-sm btn-ghost" onclick="clearLogViewer()" title="清空显示">🗑️ 清屏</button></div>
|
|
1836
|
+
<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>`;
|
|
1744
1837
|
$('content').innerHTML=html;loadLogs();
|
|
1745
1838
|
}
|
|
1746
1839
|
async function loadLogs(){
|
|
1747
|
-
const lines=$('logLines').value;const
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1840
|
+
const lines=$('logLines').value;const level=$('logLevel').value;
|
|
1841
|
+
const logs=await api(`/api/logs?lines=${lines}${level?'&level='+level:''}`);
|
|
1842
|
+
const viewer=$('logViewer');
|
|
1843
|
+
viewer.innerHTML='';
|
|
1844
|
+
if(!logs||!logs.length){viewer.innerHTML='<div style="color:var(--text3)">暂无日志</div>';return;}
|
|
1845
|
+
for(const l of logs){appendLogLine(viewer,l);}
|
|
1846
|
+
viewer.scrollTop=viewer.scrollHeight;
|
|
1847
|
+
}
|
|
1848
|
+
function appendLogLine(viewer,line){
|
|
1849
|
+
var div=document.createElement('div');
|
|
1850
|
+
div.style.cssText='white-space:pre-wrap;word-break:break-all;padding:1px 0;border-bottom:1px solid var(--border)';
|
|
1851
|
+
// 高亮 ERROR/WARNING
|
|
1852
|
+
if(line.includes('ERROR')||line.includes('CRITICAL')){div.style.color='var(--danger)';div.style.fontWeight='600';}
|
|
1853
|
+
else if(line.includes('WARNING')){div.style.color='#f59e0b';}
|
|
1854
|
+
else if(line.includes('DEBUG')){div.style.color='var(--text3)';}
|
|
1855
|
+
div.textContent=line;
|
|
1856
|
+
viewer.appendChild(div);
|
|
1857
|
+
}
|
|
1858
|
+
function clearLogViewer(){if($('logViewer'))$('logViewer').innerHTML='';}
|
|
1859
|
+
let logStreamActive=false;let _logES=null;
|
|
1752
1860
|
async function toggleLogStream(){
|
|
1753
|
-
if(logStreamActive){
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1861
|
+
if(logStreamActive){
|
|
1862
|
+
logStreamActive=false;
|
|
1863
|
+
if(_logES){_logES.close();_logES=null;}
|
|
1864
|
+
$('streamBtn').textContent='▶ 实时日志';
|
|
1865
|
+
$('streamBtn').style.background='';return;
|
|
1866
|
+
}
|
|
1867
|
+
logStreamActive=true;$('streamBtn').textContent='⏹ 停止实时';$('streamBtn').style.background='var(--danger)22';
|
|
1868
|
+
_logES=new EventSource(API+'/api/logs/stream');
|
|
1869
|
+
_logES.onmessage=function(e){
|
|
1870
|
+
try{
|
|
1871
|
+
var lines=JSON.parse(e.data).split('\n');
|
|
1872
|
+
var viewer=$('logViewer');if(!viewer)return;
|
|
1873
|
+
for(const l of lines){if(l.trim())appendLogLine(viewer,l);}
|
|
1874
|
+
viewer.scrollTop=viewer.scrollHeight;
|
|
1875
|
+
while(viewer.children.length>3000)viewer.removeChild(viewer.firstChild);
|
|
1876
|
+
}catch(ex){}
|
|
1877
|
+
};
|
|
1878
|
+
// [v1.18.7] 断线自动重连
|
|
1879
|
+
_logES.onerror=function(){
|
|
1880
|
+
if(_logES){_logES.close();_logES=null;}
|
|
1881
|
+
if(logStreamActive){
|
|
1882
|
+
$('streamBtn').textContent='⟳ 重连中...';
|
|
1883
|
+
setTimeout(function(){
|
|
1884
|
+
if(logStreamActive){toggleLogStream();}// 重新连接
|
|
1885
|
+
},3000);
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1758
1888
|
}
|
|
1759
1889
|
|
|
1760
1890
|
// ========== Tasks ==========
|
|
1761
1891
|
async function renderTasks(){
|
|
1762
|
-
|
|
1763
|
-
const
|
|
1764
|
-
const
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1892
|
+
// [v1.18.7] 同时获取实时 task plan 和持久化任务
|
|
1893
|
+
const [planR,persistR]=await Promise.all([api('/api/task-plan/all').catch(()=>({plans:[],total_keys:0})),api('/api/tasks').catch(()=>({tasks:[],total:0}))]);
|
|
1894
|
+
const plans=planR.plans||[];
|
|
1895
|
+
const persistTasks=persistR.tasks||[];
|
|
1896
|
+
// 统计
|
|
1897
|
+
var totalPlans=0,totalDone=0,totalPending=0,totalRunning=0;
|
|
1898
|
+
for(const p of plans){totalPlans+=p.total;totalDone+=p.done;totalPending+=p.pending;totalRunning+=p.running;}
|
|
1899
|
+
// 持久化任务统计
|
|
1900
|
+
var ptPending=0,ptRunning=0,ptCompleted=0,ptFailed=0;
|
|
1901
|
+
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++;}
|
|
1902
|
+
let html=`<div class="card" style="margin-bottom:16px"><h3>📊 概览</h3>
|
|
1903
|
+
<div class="grid grid-4" style="margin-top:12px">
|
|
1904
|
+
<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>
|
|
1905
|
+
<div class="stat"><div class="label">待执行</div><div class="value">${totalPending}</div><div style="font-size:11px;color:var(--text3)">pending</div></div>
|
|
1906
|
+
<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>
|
|
1907
|
+
<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>
|
|
1908
|
+
</div>
|
|
1909
|
+
${persistTasks.length?`<div style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border);font-size:13px;color:var(--text2)">
|
|
1910
|
+
持久化任务: ${ptPending} 待处理 · ${ptRunning} 运行中 · <span style="color:var(--success)">${ptCompleted} 已完成</span> · <span style="color:var(--danger)">${ptFailed} 失败</span>
|
|
1911
|
+
</div>`:''}
|
|
1912
|
+
</div>`;
|
|
1771
1913
|
html+=`<div class="flex gap-8 mb-16 flex-wrap">
|
|
1772
|
-
<
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1914
|
+
<button class="btn btn-sm btn-ghost" onclick="renderTasks()">🔄 刷新</button></div>`;
|
|
1915
|
+
// 实时 Task Plan 列表
|
|
1916
|
+
if(plans.length){
|
|
1917
|
+
html+='<h3 style="margin-bottom:8px">📋 实时任务计划 ('+plans.length+' 个活跃)</h3>';
|
|
1918
|
+
for(const p of plans){
|
|
1919
|
+
html+=`<div class="card" style="margin-bottom:12px;padding:12px 16px">
|
|
1920
|
+
<div class="flex justify-between items-center mb-8" style="flex-wrap:wrap;gap:8px">
|
|
1921
|
+
<div><strong>${escHtml(p.label)}</strong> <span class="tag">${p.type==='session'?'会话':'Agent'}</span></div>
|
|
1922
|
+
<div class="flex gap-4" style="font-size:12px">
|
|
1923
|
+
<span class="badge badge-yellow">${p.pending} 待执行</span>
|
|
1924
|
+
<span class="badge badge-blue">${p.running} 执行中</span>
|
|
1925
|
+
<span class="badge badge-green">${p.done} 已完成</span>
|
|
1926
|
+
</div>
|
|
1927
|
+
</div>
|
|
1928
|
+
<div style="margin-top:4px">`;
|
|
1929
|
+
for(var i=0;i<p.tasks.length;i++){
|
|
1930
|
+
var t=p.tasks[i];
|
|
1931
|
+
var st=t.status||'pending';
|
|
1932
|
+
var stBadge=st==='done'?'<span class="badge badge-green" style="font-size:10px">✓ done</span>':
|
|
1933
|
+
st==='running'?'<span class="badge badge-blue" style="font-size:10px">⟳ running</span>':
|
|
1934
|
+
'<span class="badge badge-yellow" style="font-size:10px">○ pending</span>';
|
|
1935
|
+
var checked=st==='done'?'checked':'';
|
|
1936
|
+
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)':''}">
|
|
1937
|
+
<input type="checkbox" ${checked} style="flex-shrink:0" onclick="toggleTaskPlanItem('${escHtml(p.key)}',${i},this.checked)">
|
|
1938
|
+
<span style="flex:1">${escHtml(t.text||'')}</span>${stBadge}
|
|
1939
|
+
</div>`;
|
|
1940
|
+
}
|
|
1941
|
+
html+=`</div></div>`;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
if(!plans.length){
|
|
1945
|
+
html+='<div class="empty" style="margin-bottom:16px">暂无活跃的实时任务计划</div>';
|
|
1946
|
+
}
|
|
1947
|
+
// 持久化任务列表
|
|
1948
|
+
if(persistTasks.length){
|
|
1949
|
+
html+=`<h3 style="margin-bottom:8px;margin-top:16px">💾 持久化任务 (${persistTasks.length} 条)</h3>`;
|
|
1950
|
+
html+='<div class="table-wrap"><table><tr><th>任务ID</th><th>描述</th><th>来源</th><th>状态</th><th>时间</th><th>操作</th></tr>';
|
|
1951
|
+
for(const t of persistTasks){
|
|
1952
|
+
var statusBadge=t.status==='completed'?'<span class="badge badge-green">已完成</span>':
|
|
1787
1953
|
t.status==='running'?'<span class="badge badge-blue">运行中</span>':
|
|
1788
1954
|
t.status==='failed'?'<span class="badge badge-red">失败</span>':
|
|
1789
1955
|
'<span class="badge badge-yellow">待处理</span>';
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
const canRetry=t.status==='failed'||t.status==='pending';
|
|
1793
|
-
const meta=t.metadata||{};
|
|
1794
|
-
const source=meta.source==='group_chat'?'群聊':meta.source||'';
|
|
1956
|
+
var meta=t.metadata||{};
|
|
1957
|
+
var source=meta.source==='group_chat'?'群聊':meta.source||'';
|
|
1795
1958
|
html+=`<tr data-status="${t.status}">
|
|
1796
1959
|
<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>
|
|
1797
1960
|
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtml(t.description)}">${escHtml((t.description||'').slice(0,100))}</td>
|
|
1798
1961
|
<td>${source}${meta.group_name?' / '+escHtml(meta.group_name):''}</td>
|
|
1799
|
-
<td>${statusBadge}
|
|
1962
|
+
<td>${statusBadge}</td>
|
|
1800
1963
|
<td style="font-size:12px;white-space:nowrap">${fmtTimeAgo(t.updated_at)}</td>
|
|
1801
1964
|
<td class="flex gap-8">
|
|
1802
|
-
${
|
|
1965
|
+
${t.status==='failed'||t.status==='pending'?`<button class="btn btn-sm btn-primary" onclick="retryTask('${escHtml(t.task_id)}')">重试</button>`:''}
|
|
1803
1966
|
<button class="btn btn-sm btn-danger" onclick="deleteTask('${escHtml(t.task_id)}')">删除</button>
|
|
1804
1967
|
</td></tr>`;
|
|
1805
1968
|
}
|
|
1806
|
-
html+='</table></div
|
|
1969
|
+
html+='</table></div>';
|
|
1970
|
+
}
|
|
1971
|
+
if(!plans.length&&!persistTasks.length){
|
|
1972
|
+
html+='<div class="empty">暂无任务记录</div>';
|
|
1807
1973
|
}
|
|
1808
1974
|
$('content').innerHTML=html;
|
|
1809
1975
|
}
|
|
1810
1976
|
|
|
1977
|
+
async function toggleTaskPlanItem(key,idx,checked){
|
|
1978
|
+
await api('/api/task-plan/all');
|
|
1979
|
+
// 直接调用 task-plan API 修改状态
|
|
1980
|
+
const r=await api('/api/task-plan?agent='+encodeURIComponent(key));
|
|
1981
|
+
if(!r||!r.tasks)return;
|
|
1982
|
+
var tasks=r.tasks;
|
|
1983
|
+
if(idx>=0&&idx<tasks.length){
|
|
1984
|
+
tasks[idx].status=checked?'done':'pending';
|
|
1985
|
+
await api('/api/task-plan',{method:'PUT',body:JSON.stringify({agent:key,session:r.session,tasks:tasks})});
|
|
1986
|
+
renderTasks();
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1811
1990
|
function filterTasks(){
|
|
1812
1991
|
const s=$('taskStatusFilter')?.value||'';
|
|
1813
1992
|
document.querySelectorAll('#taskList tr').forEach(tr=>{
|
|
@@ -2306,12 +2485,23 @@ function endCrop(){_cropState.dragging=false;document.removeEventListener('mouse
|
|
|
2306
2485
|
function cancelAvatarCrop(prefix){$(prefix+'CropArea').style.display='none';}
|
|
2307
2486
|
function confirmAvatarCrop(prefix,agentPath){
|
|
2308
2487
|
var img=$(prefix+'CropImg'),overlay=$(prefix+'CropOverlay');
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
var
|
|
2312
|
-
|
|
2488
|
+
if(!img||!overlay){showToast('裁剪数据异常','danger');return}
|
|
2489
|
+
// [v1.18.7] 防止 clientWidth=0 导致 NaN
|
|
2490
|
+
var clientW=Math.max(img.clientWidth,1),clientH=Math.max(img.clientHeight,1);
|
|
2491
|
+
var naturalW=img.naturalWidth||clientW,naturalH=img.naturalHeight||clientH;
|
|
2492
|
+
var scale=naturalW/clientW;
|
|
2493
|
+
if(!isFinite(scale)||scale<=0)scale=1;
|
|
2494
|
+
var cx=Math.round(parseFloat(overlay.style.left||'0')*scale);
|
|
2495
|
+
var cy=Math.round(parseFloat(overlay.style.top||'0')*scale);
|
|
2496
|
+
var cw=Math.round(parseFloat(overlay.style.width||'0')*scale);
|
|
2497
|
+
var ch=Math.round(parseFloat(overlay.style.height||'0')*scale);
|
|
2498
|
+
if(cw<10||ch<10||isNaN(cx)||isNaN(cy)){showToast('请拖动选择裁剪区域','danger');return}
|
|
2499
|
+
var src=$(prefix+'CropImg').src;
|
|
2500
|
+
if(!src||!src.startsWith('data:image')){showToast('请先上传图片','danger');return}
|
|
2501
|
+
var blob=dataURItoBlob(src);
|
|
2502
|
+
if(!blob||blob.size===0){showToast('图片数据异常','danger');return}
|
|
2313
2503
|
var formData=new FormData();
|
|
2314
|
-
formData.append('file'
|
|
2504
|
+
formData.append('file',blob,'avatar.png');
|
|
2315
2505
|
showToast('正在上传裁剪...','info');
|
|
2316
2506
|
fetch('/api/agents/'+encodeURIComponent(agentPath)+'/avatar?crop_x='+cx+'&crop_y='+cy+'&crop_w='+cw+'&crop_h='+ch+'&size=128',{
|
|
2317
2507
|
method:'POST',body:formData
|
|
@@ -2320,7 +2510,15 @@ function confirmAvatarCrop(prefix,agentPath){
|
|
|
2320
2510
|
else showToast(d.error||'上传失败','danger');
|
|
2321
2511
|
}).catch(e=>showToast('上传失败: '+e,'danger'));
|
|
2322
2512
|
}
|
|
2323
|
-
function dataURItoBlob(dataURI){
|
|
2513
|
+
function dataURItoBlob(dataURI){
|
|
2514
|
+
if(!dataURI||typeof dataURI!=='string')return null;
|
|
2515
|
+
var parts=dataURI.split(',');
|
|
2516
|
+
if(parts.length<2)return null;
|
|
2517
|
+
var mimeMatch=parts[0].match(/:(.*?);/);
|
|
2518
|
+
var mime=mimeMatch?mimeMatch[1]:'image/png';
|
|
2519
|
+
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});}
|
|
2520
|
+
catch(e){return null}
|
|
2521
|
+
}
|
|
2324
2522
|
async function removeAvatarImage(agentPath){
|
|
2325
2523
|
var ad=await api('/api/agents/'+encodeURIComponent(agentPath));if(!ad||!ad.avatar_image)return;
|
|
2326
2524
|
await api('/api/agents/'+encodeURIComponent(agentPath),{method:'PUT',body:JSON.stringify({avatar_image:''})});
|