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,154 @@
1
+ // ========== System Config ==========
2
+ (function(){
3
+ try {
4
+ // ========== System Config ==========
5
+ async function renderSystem(){
6
+ $('content').innerHTML=`
7
+ <div class="card">
8
+ <h3>🕐 时区设置</h3>
9
+ <p style="font-size:13px;color:var(--text2);margin-bottom:12px">设置系统时区,影响提示词中的当前时间、记忆时间戳、日志时间等所有时间显示</p>
10
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
11
+ <select id="sysTimezone" style="flex:1;padding:8px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg2);color:var(--text);font-size:14px"></select>
12
+ <button class="btn btn-primary" id="sysTzSaveBtn" onclick="sysSaveTimezone()">保存</button>
13
+ </div>
14
+ <div id="sysTzMsg"></div>
15
+ </div>
16
+ <div class="card">
17
+ <h3>📤 导出备份</h3>
18
+ <p style="font-size:13px;color:var(--text2);margin-bottom:12px">将当前完整配置导出为 JSON 文件,用于备份或迁移</p>
19
+ <div class="flex gap-8">
20
+ <button class="btn btn-ghost" id="sysExportSafe" onclick="sysExport(false)">导出(脱敏)</button>
21
+ <button class="btn btn-danger" id="sysExportFull" onclick="sysExport(true)">导出(含密钥)</button>
22
+ </div>
23
+ <div id="sysExportMsg"></div>
24
+ </div>
25
+ <div class="card">
26
+ <h3>📥 导入配置</h3>
27
+ <p style="font-size:13px;color:var(--text2);margin-bottom:12px">从之前导出的 JSON 备份文件恢复配置</p>
28
+ <div class="drop-zone" id="sysDropZone" onclick="document.getElementById('sysFileInput').click()" ondragover="event.preventDefault();this.classList.add('dragover')" ondragleave="this.classList.remove('dragover')" ondrop="sysHandleDrop(event)">
29
+ <div style="font-size:28px;margin-bottom:8px;opacity:.5">📁</div>
30
+ <div style="font-size:13px">点击选择文件或拖拽 JSON 到此处</div>
31
+ <div style="font-size:11px;color:var(--text2);margin-top:4px">支持 myagent_config_*.json 格式</div>
32
+ <input type="file" id="sysFileInput" accept=".json" onchange="sysHandleFile(this)" style="display:none">
33
+ </div>
34
+ <div style="display:flex;align-items:center;gap:8px;margin-top:8px;font-size:13px;color:var(--text2)">
35
+ <input type="checkbox" id="sysOverwrite" style="width:16px;height:16px;accent-color:var(--primary)">
36
+ <label for="sysOverwrite">完全覆盖当前配置(否则智能合并)</label>
37
+ </div>
38
+ <div id="sysImportMsg"></div>
39
+ </div>
40
+ <div class="card">
41
+ <h3>👁️ 配置预览</h3>
42
+ <div class="config-preview" id="sysConfigPreview">加载中...</div>
43
+ </div>`;
44
+ sysLoadTimezone();
45
+ sysLoadPreview();
46
+ }
47
+
48
+ const COMMON_TIMEZONES=['Asia/Shanghai','Asia/Kuala_Lumpur','Asia/Singapore','Asia/Tokyo','Asia/Seoul','Asia/Ho_Chi_Minh','Asia/Bangkok','Asia/Jakarta','Asia/Kolkata','Asia/Dubai','Europe/London','Europe/Paris','Europe/Berlin','Europe/Moscow','America/New_York','America/Chicago','America/Denver','America/Los_Angeles','Australia/Sydney','Pacific/Auckland','UTC'];
49
+
50
+ async function sysLoadTimezone(){
51
+ try{
52
+ const r=await api('/api/config/get',{method:'POST',body:JSON.stringify({key:'timezone'})});
53
+ const sel=$('sysTimezone');
54
+ sel.innerHTML=COMMON_TIMEZONES.map(tz=>`<option value="${tz}"${tz===(r.value||'Asia/Shanghai')?' selected':''}>${tz}</option>`).join('');
55
+ }catch(e){console.error('Load timezone failed:',e);}
56
+ }
57
+
58
+ async function sysSaveTimezone(){
59
+ const btn=$('sysTzSaveBtn');btn.disabled=true;
60
+ sysSetMsg('sysTzMsg','loading','正在保存时区...');
61
+ try{
62
+ const tz=$('sysTimezone').value;
63
+ const r=await api('/api/config/set',{method:'POST',body:JSON.stringify({key:'timezone',value:tz})});
64
+ if(r.ok){sysSetMsg('sysTzMsg','success','✅ 时区已保存为 '+tz);showToast('时区已更新为 '+tz,'success');}
65
+ else{sysSetMsg('sysTzMsg','error','❌ 保存失败: '+(r.error||'未知错误'));}
66
+ }catch(e){sysSetMsg('sysTzMsg','error','❌ 保存失败: '+e.message);}
67
+ btn.disabled=false;
68
+ }
69
+
70
+ function sysSetMsg(id,type,msg){
71
+ const el=$(id);if(!el)return;
72
+ el.className='status-msg '+type;el.textContent=msg;
73
+ if(type!=='loading')setTimeout(()=>{if(el.className.includes(type))el.className='status-msg';},8000);
74
+ }
75
+
76
+ async function sysExport(includeSecrets){
77
+ const btnId=includeSecrets?'sysExportFull':'sysExportSafe';
78
+ const btn=$(btnId);btn.disabled=true;
79
+ sysSetMsg('sysExportMsg','loading','正在导出配置...');
80
+ try{
81
+ const resp=await fetch(API+'/api/config/export',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({include_secrets:includeSecrets})});
82
+ if(!resp.ok){const err=await resp.json();throw new Error(err.error||'导出失败');}
83
+ const blob=await resp.blob();
84
+ const disposition=resp.headers.get('Content-Disposition')||'';
85
+ const match=disposition.match(/filename="([^"]+)"/);
86
+ const filename=match?match[1]:'myagent_config.json';
87
+ const url=URL.createObjectURL(blob);const a=document.createElement('a');
88
+ a.href=url;a.download=filename;document.body.appendChild(a);a.click();
89
+ document.body.removeChild(a);URL.revokeObjectURL(url);
90
+ sysSetMsg('sysExportMsg','success','✅ 配置已导出: '+filename);showToast('配置已导出','success');
91
+ }catch(e){sysSetMsg('sysExportMsg','error','❌ 导出失败: '+e.message);}
92
+ btn.disabled=false;
93
+ }
94
+
95
+ function sysHandleDrop(event){
96
+ event.preventDefault();event.stopPropagation();
97
+ $('sysDropZone').classList.remove('dragover');
98
+ const files=event.dataTransfer.files;
99
+ if(files.length>0&&files[0].name.endsWith('.json')){sysImportFile(files[0]);}
100
+ else{showToast('请拖入 JSON 文件','danger');}
101
+ }
102
+ function sysHandleFile(input){if(input.files.length>0)sysImportFile(input.files[0]);input.value='';}
103
+
104
+ async function sysImportFile(file){
105
+ const overwrite=$('sysOverwrite').checked;
106
+ sysSetMsg('sysImportMsg','loading','正在读取 '+file.name+'...');
107
+ try{
108
+ const text=await file.text();
109
+ let data;try{data=JSON.parse(text);}catch(e){sysSetMsg('sysImportMsg','error','文件不是有效的 JSON');return;}
110
+ sysSetMsg('sysImportMsg','loading','正在导入配置...');
111
+ if(overwrite)data._overwrite=true;
112
+ const r=await api('/api/config/import',{method:'POST',body:JSON.stringify(data)});
113
+ if(r.ok){
114
+ const changed=r.changed_keys||[];
115
+ let msg='✅ '+r.message;
116
+ if(changed.length>0)msg+=' ('+changed.slice(0,5).join(', ')+(changed.length>5?'...':'')+')';
117
+ sysSetMsg('sysImportMsg','success',msg);showToast(r.message,'success');sysLoadPreview();
118
+ }else{sysSetMsg('sysImportMsg','error','❌ 导入失败: '+(r.message||'未知错误'));}
119
+ }catch(e){sysSetMsg('sysImportMsg','error','❌ 导入失败: '+e.message);}
120
+ }
121
+
122
+ async function sysLoadPreview(){
123
+ const el=$('sysConfigPreview');if(!el)return;
124
+ try{
125
+ const cfg=await api('/api/config');const c=cfg.config||cfg;
126
+ const lines=[];
127
+ if(c.llm){
128
+ lines.push('<span class="key">LLM:</span> '+escHtml(c.llm.provider+'/'+c.llm.model));
129
+ lines.push('<span class="key">Base URL:</span> '+escHtml(c.llm.base_url||'default'));
130
+ lines.push('<span class="key">Temperature:</span> '+c.llm.temperature+' | Max Tokens: '+c.llm.max_tokens);
131
+ }
132
+ if(c.executor){
133
+ lines.push('<span class="key">执行引擎:</span> '+escHtml(c.executor.execution_mode||'local')+' | Timeout: '+(c.executor.timeout||300)+'s');
134
+ }
135
+ if(c.memory){
136
+ lines.push('<span class="key">记忆:</span> 会话 '+(c.memory.max_session||50)+' 轮 | 自动总结: '+(c.memory.auto_summarize?'开':'关'));
137
+ }
138
+ if(c.agent){
139
+ lines.push('<span class="key">Agent:</span> 最大迭代 '+(c.agent.max_iterations||30)+' | 并行 '+(c.agent.max_parallel||3));
140
+ }
141
+ if(c.chat_platforms&&c.chat_platforms.length>0){
142
+ const platforms=c.chat_platforms.filter(p=>p.enabled).map(p=>p.platform);
143
+ if(platforms.length>0)lines.push('<span class="key">聊天平台:</span> '+escHtml(platforms.join(', ')));
144
+ }
145
+ if(c.log_level)lines.push('<span class="key">日志级别:</span> '+escHtml(c.log_level));
146
+ if(cfg._meta){lines.push('');lines.push('<span class="key">备份信息:</span> '+escHtml(cfg._meta.exported_at||'N/A'));}
147
+ el.innerHTML=lines.join('\n')||'暂无配置信息';
148
+ }catch(e){el.textContent='加载配置失败: '+e.message;}
149
+ }
150
+
151
+ if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
152
+ window._adminRenderers['system'] = renderSystem;
153
+ } catch(e) { console.error('[admin-system] load error:', e); }
154
+ })();
@@ -0,0 +1,131 @@
1
+ // ========== Tasks ==========
2
+ (function(){
3
+ try {
4
+ // ========== Tasks ==========
5
+ async function renderTasks(){
6
+ // [v1.18.7] 同时获取实时 task plan 和持久化任务
7
+ const [planR,persistR]=await Promise.all([api('/api/task-plan/all').catch(()=>({plans:[],total_keys:0})),api('/api/tasks').catch(()=>({tasks:[],total:0}))]);
8
+ const plans=planR.plans||[];
9
+ const persistTasks=persistR.tasks||[];
10
+ // 统计
11
+ var totalPlans=0,totalDone=0,totalPending=0,totalRunning=0;
12
+ for(const p of plans){totalPlans+=p.total;totalDone+=p.done;totalPending+=p.pending;totalRunning+=p.running;}
13
+ // 持久化任务统计
14
+ var ptPending=0,ptRunning=0,ptCompleted=0,ptFailed=0;
15
+ 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++;}
16
+ let html=`<div class="card" style="margin-bottom:16px"><h3>📊 概览</h3>
17
+ <div class="grid grid-4" style="margin-top:12px">
18
+ <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>
19
+ <div class="stat"><div class="label">待执行</div><div class="value">${totalPending}</div><div style="font-size:11px;color:var(--text3)">pending</div></div>
20
+ <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>
21
+ <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>
22
+ </div>
23
+ ${persistTasks.length?`<div style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border);font-size:13px;color:var(--text2)">
24
+ 持久化任务: ${ptPending} 待处理 · ${ptRunning} 运行中 · <span style="color:var(--success)">${ptCompleted} 已完成</span> · <span style="color:var(--danger)">${ptFailed} 失败</span>
25
+ </div>`:''}
26
+ </div>`;
27
+ html+=`<div class="flex gap-8 mb-16 flex-wrap">
28
+ <button class="btn btn-sm btn-ghost" onclick="renderTasks()">🔄 刷新</button></div>`;
29
+ // 实时 Task Plan 列表
30
+ if(plans.length){
31
+ html+='<h3 style="margin-bottom:8px">📋 实时任务计划 ('+plans.length+' 个活跃)</h3>';
32
+ for(const p of plans){
33
+ html+=`<div class="card" style="margin-bottom:12px;padding:12px 16px">
34
+ <div class="flex justify-between items-center mb-8" style="flex-wrap:wrap;gap:8px">
35
+ <div><strong>${escHtml(p.label)}</strong> <span class="tag">${p.type==='session'?'会话':'Agent'}</span></div>
36
+ <div class="flex gap-4" style="font-size:12px">
37
+ <span class="badge badge-yellow">${p.pending} 待执行</span>
38
+ <span class="badge badge-blue">${p.running} 执行中</span>
39
+ <span class="badge badge-green">${p.done} 已完成</span>
40
+ </div>
41
+ </div>
42
+ <div style="margin-top:4px">`;
43
+ for(var i=0;i<p.tasks.length;i++){
44
+ var t=p.tasks[i];
45
+ var st=t.status||'pending';
46
+ var stBadge=st==='done'?'<span class="badge badge-green" style="font-size:10px">✓ done</span>':
47
+ st==='running'?'<span class="badge badge-blue" style="font-size:10px">⟳ running</span>':
48
+ '<span class="badge badge-yellow" style="font-size:10px">○ pending</span>';
49
+ var checked=st==='done'?'checked':'';
50
+ 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)':''}">
51
+ <input type="checkbox" ${checked} style="flex-shrink:0" onclick="toggleTaskPlanItem('${escHtml(p.key)}',${i},this.checked)">
52
+ <span style="flex:1">${escHtml(t.text||'')}</span>${stBadge}
53
+ </div>`;
54
+ }
55
+ html+=`</div></div>`;
56
+ }
57
+ }
58
+ if(!plans.length){
59
+ html+='<div class="empty" style="margin-bottom:16px">暂无活跃的实时任务计划</div>';
60
+ }
61
+ // 持久化任务列表
62
+ if(persistTasks.length){
63
+ html+=`<h3 style="margin-bottom:8px;margin-top:16px">💾 持久化任务 (${persistTasks.length} 条)</h3>`;
64
+ html+='<div class="table-wrap"><table><tr><th>任务ID</th><th>描述</th><th>来源</th><th>状态</th><th>时间</th><th>操作</th></tr>';
65
+ for(const t of persistTasks){
66
+ var statusBadge=t.status==='completed'?'<span class="badge badge-green">已完成</span>':
67
+ t.status==='running'?'<span class="badge badge-blue">运行中</span>':
68
+ t.status==='failed'?'<span class="badge badge-red">失败</span>':
69
+ '<span class="badge badge-yellow">待处理</span>';
70
+ var meta=t.metadata||{};
71
+ var source=meta.source==='group_chat'?'群聊':meta.source||'';
72
+ html+=`<tr data-status="${t.status}">
73
+ <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>
74
+ <td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtml(t.description)}">${escHtml((t.description||'').slice(0,100))}</td>
75
+ <td>${source}${meta.group_name?' / '+escHtml(meta.group_name):''}</td>
76
+ <td>${statusBadge}</td>
77
+ <td style="font-size:12px;white-space:nowrap">${fmtTimeAgo(t.updated_at)}</td>
78
+ <td class="flex gap-8">
79
+ ${t.status==='failed'||t.status==='pending'?`<button class="btn btn-sm btn-primary" onclick="retryTask('${escHtml(t.task_id)}')">重试</button>`:''}
80
+ <button class="btn btn-sm btn-danger" onclick="deleteTask('${escHtml(t.task_id)}')">删除</button>
81
+ </td></tr>`;
82
+ }
83
+ html+='</table></div>';
84
+ }
85
+ if(!plans.length&&!persistTasks.length){
86
+ html+='<div class="empty">暂无任务记录</div>';
87
+ }
88
+ $('content').innerHTML=html;
89
+ }
90
+
91
+ async function toggleTaskPlanItem(key,idx,checked){
92
+ await api('/api/task-plan/all');
93
+ // 直接调用 task-plan API 修改状态
94
+ const r=await api('/api/task-plan?agent='+encodeURIComponent(key));
95
+ if(!r||!r.tasks)return;
96
+ var tasks=r.tasks;
97
+ if(idx>=0&&idx<tasks.length){
98
+ tasks[idx].status=checked?'done':'pending';
99
+ await api('/api/task-plan',{method:'PUT',body:JSON.stringify({agent:key,session:r.session,tasks:tasks})});
100
+ renderTasks();
101
+ }
102
+ }
103
+
104
+ function filterTasks(){
105
+ const s=$('taskStatusFilter')?.value||'';
106
+ document.querySelectorAll('#taskList tr').forEach(tr=>{
107
+ if(!tr.dataset.status)return;
108
+ tr.style.display=(!s||tr.dataset.status===s)?'':'none';
109
+ });
110
+ }
111
+
112
+ async function retryTask(taskId){
113
+ showToast('正在重试...','info');
114
+ const r=await api(`/api/tasks/${encodeURIComponent(taskId)}/retry`,{method:'POST'});
115
+ if(r.error){showToast('重试失败: '+r.error,'danger');return;}
116
+ showToast('任务已重新提交','success');renderTasks();
117
+ }
118
+
119
+ async function deleteTask(taskId){
120
+ showConfirm('删除任务','确认删除此任务记录?此操作不可恢复。',async()=>{
121
+ const r=await api(`/api/tasks/${encodeURIComponent(taskId)}`,{method:'DELETE'});
122
+ if(r.error){showToast(r.error,'danger');closeModal();return;}
123
+ closeModal();showToast('已删除','success');renderTasks();
124
+ });
125
+ }
126
+
127
+
128
+ if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
129
+ window._adminRenderers['tasks'] = renderTasks;
130
+ } catch(e) { console.error('[admin-tasks] load error:', e); }
131
+ })();