Spaces:
Sleeping
Sleeping
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>OpenAI Token 消耗器</title> | |
| <style> | |
| *{box-sizing:border-box} | |
| body{margin:0;background:#0f172a;font:14px/1.5 "Segoe UI","Microsoft YaHei",sans-serif;color:#e2e8f0} | |
| .wrap{max-width:1400px;margin:0 auto;padding:18px 16px} | |
| h1{margin:0 0 16px;font-size:26px;background:linear-gradient(135deg,#60a5fa,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent} | |
| .card{background:linear-gradient(145deg,#1e293b,#0f172a);border:1px solid #334155;border-radius:12px;padding:20px;margin-bottom:16px;box-shadow:0 4px 20px rgba(0,0,0,0.3)} | |
| .g2,.g3,.stats,.logs{display:grid;gap:16px} | |
| .g2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))} | |
| .g3{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))} | |
| label{display:flex;flex-direction:column;gap:6px;font-size:12px;color:#94a3b8} | |
| input,textarea,select,button{font:inherit} | |
| input,textarea,select{margin-top:0;border:1px solid #475569;border-radius:8px;padding:10px 12px;background:#1e293b;color:#e2e8f0;transition:border-color 0.2s,box-shadow 0.2s} | |
| input:focus,textarea:focus,select:focus{outline:none;border-color:#60a5fa;box-shadow:0 0 0 3px rgba(96,165,250,0.2)} | |
| input::placeholder,textarea::placeholder{color:#64748b} | |
| input[type=checkbox]{width:18px;height:18px;accent-color:#60a5fa} | |
| .toggle{display:flex;flex-direction:row;align-items:center;gap:8px;font-size:13px;color:#cbd5e1} | |
| .toggle span{line-height:1.35} | |
| textarea{min-height:80px;resize:vertical} | |
| .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:12px} | |
| button{padding:10px 18px;border:0;border-radius:8px;cursor:pointer;font-weight:500;transition:all 0.2s} | |
| button:hover{transform:translateY(-1px)} | |
| button:active{transform:translateY(0)} | |
| .p{background:linear-gradient(135deg,#3b82f6,#2563eb);color:#fff;box-shadow:0 2px 10px rgba(59,130,246,0.4)} | |
| .p:hover{box-shadow:0 4px 15px rgba(59,130,246,0.5)} | |
| .d{background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff;box-shadow:0 2px 10px rgba(239,68,68,0.4)} | |
| .s{background:#1e293b;border:1px solid #475569;color:#e2e8f0} | |
| .s:hover{border-color:#60a5fa} | |
| button:disabled{opacity:.5;cursor:not-allowed;transform:none} | |
| #ms,#rs{font-size:12px;color:#64748b} | |
| .stats{grid-template-columns:repeat(auto-fit,minmax(120px,1fr))} | |
| .box{border:1px solid #334155;border-radius:10px;background:linear-gradient(145deg,#1e293b,#0f172a);padding:12px;text-align:center} | |
| .box label{font-size:11px;color:#64748b;margin-bottom:4px} | |
| .box b{display:block;font-size:22px;color:#60a5fa;margin-top:4px} | |
| .logs{grid-template-columns:repeat(auto-fill,minmax(280px,1fr))} | |
| .log{border:1px solid #334155;border-radius:10px;background:linear-gradient(145deg,#1e293b,#0f172a);padding:10px;transition:all 0.2s} | |
| .log.waiting{border-color:#475569} | |
| .log.running{border-color:#3b82f6;border-width:2px;box-shadow:0 0 15px rgba(59,130,246,0.3)} | |
| .log.success{border-color:#22c55e;box-shadow:0 0 10px rgba(34,197,94,0.2)} | |
| .log.done{border-color:#22c55e;border-style:dashed} | |
| .log.error{border-color:#ef4444;box-shadow:0 0 10px rgba(239,68,68,0.2)} | |
| .log.stopped{border-color:#f59e0b;box-shadow:0 0 10px rgba(245,158,11,0.2)} | |
| .log.paused{border-color:#f59e0b;box-shadow:0 0 10px rgba(245,158,11,0.2)} | |
| .t{display:flex;justify-content:space-between;font-size:13px;font-weight:600;margin-bottom:6px;color:#e2e8f0} | |
| .m{font-size:11px;color:#64748b;margin-bottom:6px} | |
| pre{margin:0;background:#0f172a;color:#93c5fd;border-radius:6px;padding:8px;min-height:80px;max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:11px} | |
| pre::-webkit-scrollbar{width:5px;height:5px} | |
| pre::-webkit-scrollbar-track{background:#1e293b;border-radius:3px} | |
| pre::-webkit-scrollbar-thumb{background:#475569;border-radius:3px} | |
| /* Config Editor Scrollbar */ | |
| #configEditor::-webkit-scrollbar{width:10px;height:10px} | |
| #configEditor::-webkit-scrollbar-track{background:#0f172a;border-radius:5px} | |
| #configEditor::-webkit-scrollbar-thumb{background:linear-gradient(180deg,#475569,#334155);border-radius:5px;border:2px solid #0f172a} | |
| #configEditor::-webkit-scrollbar-thumb:hover{background:linear-gradient(180deg,#60a5fa,#3b82f6)} | |
| #configEditor::-webkit-scrollbar-corner{background:#0f172a} | |
| /* Task list styles */ | |
| .task-list{display:flex;flex-direction:column;gap:12px} | |
| .task-item{display:flex;flex-direction:column;padding:16px 18px;border:1px solid #334155;border-radius:12px;background:linear-gradient(145deg,#1e293b,#0f172a);cursor:pointer;transition:all 0.2s;position:relative;overflow:hidden} | |
| .task-item:hover{transform:translateX(4px)} | |
| .task-item.running{border-color:#3b82f6;box-shadow:0 0 20px rgba(59,130,246,0.15)} | |
| .task-item.running::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,#3b82f6,#60a5fa,#3b82f6);animation:shimmer 2s infinite} | |
| .task-item.paused{border-color:#f59e0b;box-shadow:0 0 20px rgba(245,158,11,0.15)} | |
| .task-item.paused::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:#f59e0b} | |
| .task-item.selected{transform:translateX(4px)} | |
| @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}} | |
| .task-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px} | |
| .task-title{display:flex;align-items:center;gap:10px} | |
| .task-label{font-weight:600;font-size:15px;color:#e2e8f0} | |
| .status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0} | |
| .status-dot.running{background:#3b82f6;box-shadow:0 0 8px #3b82f6;animation:pulse 1.5s infinite} | |
| .status-dot.paused{background:#f59e0b;box-shadow:0 0 8px #f59e0b} | |
| .status-dot.completed{background:#22c55e} | |
| .status-dot.stopped{background:#ef4444} | |
| .status-dot.idle{background:#64748b} | |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}} | |
| .task-badge{padding:3px 10px;border-radius:6px;font-size:11px;font-weight:600} | |
| .task-badge.running{background:rgba(59,130,246,0.15);color:#60a5fa;border:1px solid rgba(59,130,246,0.3)} | |
| .task-badge.paused{background:rgba(245,158,11,0.15);color:#fbbf24;border:1px solid rgba(245,158,11,0.3)} | |
| .task-badge.completed{background:rgba(34,197,94,0.15);color:#4ade80;border:1px solid rgba(34,197,94,0.3)} | |
| .task-badge.stopped{background:rgba(239,68,68,0.15);color:#f87171;border:1px solid rgba(239,68,68,0.3)} | |
| .task-badge.idle{background:rgba(100,116,139,0.15);color:#94a3b8;border:1px solid rgba(100,116,139,0.3)} | |
| .task-progress{display:flex;flex-direction:column;gap:8px} | |
| .progress-row{display:flex;align-items:center;gap:10px} | |
| .progress-label{font-size:11px;color:#64748b;width:50px;flex-shrink:0} | |
| .progress-bar{flex:1;height:6px;background:#1e293b;border-radius:3px;overflow:hidden} | |
| .progress-fill{height:100%;border-radius:3px;transition:width 0.3s ease} | |
| .progress-fill.task{background:linear-gradient(90deg,#3b82f6,#60a5fa)} | |
| .progress-fill.token{background:linear-gradient(90deg,#8b5cf6,#a78bfa)} | |
| .progress-text{font-size:11px;color:#94a3b8;width:80px;text-align:right;flex-shrink:0} | |
| .task-meta{display:flex;align-items:center;gap:16px;margin-top:12px;padding-top:12px;border-top:1px solid #334155;flex-wrap:wrap} | |
| /* Task Actions Bar */ | |
| .task-actions-bar{display:flex;gap:8px;margin-left:auto;flex-shrink:0;padding-top:2px} | |
| .action-btn{padding:6px 16px;border-radius:6px;font-size:12px;font-weight:500;cursor:pointer;transition:all 0.2s;border:1px solid transparent} | |
| .action-btn.disabled{opacity:0.4;cursor:not-allowed} | |
| .start-btn{background:linear-gradient(135deg,#22c55e,#16a34a);color:#fff} | |
| .start-btn:hover:not(.disabled){transform:translateY(-1px);box-shadow:0 2px 8px rgba(34,197,94,0.4)} | |
| .pause-btn{background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff} | |
| .pause-btn:hover:not(.disabled){transform:translateY(-1px);box-shadow:0 2px 8px rgba(245,158,11,0.4)} | |
| .stop-btn{background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff} | |
| .stop-btn:hover:not(.disabled){transform:translateY(-1px);box-shadow:0 2px 8px rgba(239,68,68,0.4)} | |
| .meta-item{display:flex;align-items:center;gap:4px;font-size:11px;color:#64748b} | |
| .meta-item svg{width:12px;height:12px;opacity:0.7} | |
| .task-actions{display:flex;gap:8px} | |
| .task-actions button{padding:6px 12px;font-size:12px} | |
| .badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:10px;font-weight:600} | |
| .badge.running{background:rgba(59,130,246,0.2);color:#60a5fa} | |
| .badge.idle{background:rgba(100,116,139,0.3);color:#94a3b8} | |
| .badge.completed{background:rgba(34,197,94,0.2);color:#4ade80} | |
| .badge.stopped{background:rgba(245,158,11,0.2);color:#fbbf24} | |
| .badge.paused{background:rgba(251,191,36,0.2);color:#fbbf24} | |
| /* Navigation */ | |
| .nav{display:flex;gap:10px;margin-bottom:16px} | |
| .nav-btn{padding:10px 20px;background:#1e293b;border:1px solid #475569;color:#94a3b8;border-radius:8px;cursor:pointer;transition:all 0.2s} | |
| .nav-btn:hover{border-color:#60a5fa;color:#e2e8f0} | |
| .nav-btn.active{background:linear-gradient(135deg,#3b82f6,#2563eb);border-color:#3b82f6;color:#fff} | |
| .header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px} | |
| .header h1{margin:0} | |
| /* Empty state */ | |
| .empty{text-align:center;padding:40px 20px;color:#64748b} | |
| .empty-icon{font-size:48px;margin-bottom:12px;opacity:0.5} | |
| /* Responsive */ | |
| @media(max-width:768px){ | |
| .stats{grid-template-columns:repeat(2,1fr)} | |
| .g3{grid-template-columns:1fr} | |
| .task-actions{flex-direction:column} | |
| } | |
| /* Status Card for Detail Page */ | |
| .status-card{display:flex;flex-direction:column;gap:0;padding:16px 18px;border-radius:12px;transition:all 0.3s;position:relative;overflow:hidden;background:linear-gradient(145deg,#1e293b,#0f172a)} | |
| .status-card.idle{border-color:#334155} | |
| .status-card.running{border-color:#3b82f6;box-shadow:0 0 20px rgba(59,130,246,0.15)} | |
| .status-card.running::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,#3b82f6,#60a5fa,#3b82f6);animation:shimmer 2s infinite;background-size:200% 100%} | |
| .status-card.paused{border-color:#f59e0b;box-shadow:0 0 20px rgba(245,158,11,0.15)} | |
| .status-card.paused::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:#f59e0b} | |
| .status-card.completed{border-color:#22c55e} | |
| .status-card.stopped{border-color:#ef4444} | |
| /* Status Bar */ | |
| .status-bar{display:flex;align-items:center;gap:24px;padding:10px 18px;background:linear-gradient(145deg,#1e293b,#0f172a);border:1px solid #334155;border-radius:10px;margin-bottom:16px;font-size:12px} | |
| .status-item{display:flex;align-items:center;gap:6px;color:#94a3b8} | |
| .status-label{color:#64748b} | |
| .status-item span:last-child{color:#60a5fa;font-weight:500} | |
| /* Config Editor */ | |
| #configEditor{background:#0f172a;color:#e2e8f0;border:1px solid #475569;border-radius:8px;resize:vertical} | |
| #configEditor:focus{outline:none;border-color:#60a5fa;box-shadow:0 0 0 3px rgba(96,165,250,0.2)} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <div class="header"> | |
| <h1>OpenAI Token 消耗器</h1> | |
| <div class="nav"> | |
| <button class="nav-btn active" id="navList">任务列表</button> | |
| <button class="nav-btn" id="navNew">新建任务</button> | |
| <button class="nav-btn" id="navHttp">HTTP请求</button> | |
| <button class="nav-btn" id="navConfig">配置编辑</button> | |
| </div> | |
| </div> | |
| <!-- Status Bar --> | |
| <div class="status-bar" id="statusBar"> | |
| <div class="status-item"> | |
| <span class="status-label">进程内存:</span> | |
| <span id="processMem">--</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">系统内存:</span> | |
| <span id="systemMem">--</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">系统使用率:</span> | |
| <span id="systemMemPercent">--</span> | |
| </div> | |
| </div> | |
| <!-- Task List View --> | |
| <div id="listView"> | |
| <div class="card"> | |
| <div class="row" style="justify-content:space-between;margin-top:0"> | |
| <span style="color:#94a3b8;font-size:13px">点击任务进入详情,或创建新任务</span> | |
| <button class="p" id="refreshBtn">刷新列表</button> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="task-list" id="taskList"></div> | |
| </div> | |
| </div> | |
| <!-- Task Edit View --> | |
| <div id="editView" style="display:none"> | |
| <div class="card"> | |
| <div class="row" style="justify-content:space-between;margin-top:0"> | |
| <button class="s" id="backBtn">← 返回列表</button> | |
| <div id="taskHeader"> | |
| <span id="currentTaskLabel" style="font-weight:600;font-size:16px"></span> | |
| <span id="currentTaskStatus" class="badge idle" style="margin-left:8px">空闲</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card status-card idle" id="statusCard"> | |
| <div class="task-header"> | |
| <div class="task-title"> | |
| <span class="status-dot idle" id="detailStatusDot"></span> | |
| <span class="task-label" id="bannerLabel">空闲</span> | |
| </div> | |
| <span class="task-badge idle" id="detailBadge">空闲</span> | |
| </div> | |
| <div id="configInfo" style="background:rgba(100,116,139,0.1);border:1px solid rgba(100,116,139,0.2);border-radius:8px;padding:10px;margin:10px 0;font-size:12px;color:#94a3b8"> | |
| <div style="display:flex;flex-wrap:wrap;gap:8px 16px"> | |
| <span>模型: <b id="infoModel" style="color:#60a5fa">-</b></span> | |
| <span>max_tokens: <b id="infoMaxTokens" style="color:#60a5fa">-</b></span> | |
| <span>累计上限: <b id="infoMaxLimit" style="color:#60a5fa">-</b></span> | |
| <span>线程: <b id="infoThreads" style="color:#60a5fa">-</b></span> | |
| <span>循环: <b id="infoLoop" style="color:#60a5fa">-</b></span> | |
| <span>温度: <b id="infoTemp" style="color:#60a5fa">-</b></span> | |
| </div> | |
| </div> | |
| <div class="task-progress"> | |
| <div class="progress-row"> | |
| <span class="progress-label">任务</span> | |
| <div class="progress-bar"> | |
| <div class="progress-fill task" id="detailTaskProgress" style="width:0%"></div> | |
| </div> | |
| <span class="progress-text" id="bannerProgress">0/0</span> | |
| </div> | |
| <div class="progress-row"> | |
| <span class="progress-label">Tokens</span> | |
| <div class="progress-bar"> | |
| <div class="progress-fill token" id="detailTokenProgress" style="width:0%"></div> | |
| </div> | |
| <span class="progress-text" id="bannerTokens">0/0</span> | |
| </div> | |
| </div> | |
| <div class="task-meta"> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> | |
| <span id="bannerTime">0s</span> | |
| </div> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg> | |
| <span>成功 <b id="detailSuccess" style="color:#4ade80">0</b></span> | |
| </div> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg> | |
| <span>失败 <b id="detailFailed" style="color:#f87171">0</b></span> | |
| </div> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg> | |
| <span>中止 <b id="detailAborted">0</b></span> | |
| </div> | |
| <div class="task-actions-bar" id="detailActions"> | |
| <button class="action-btn start-btn" id="detailStartBtn">启动</button> | |
| <button class="action-btn pause-btn disabled" id="detailPauseBtn" disabled>暂停</button> | |
| <button class="action-btn stop-btn disabled" id="detailStopBtn" disabled>停止</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="g2"> | |
| <label>任务标签<input id="label" placeholder="给任务起个名字"></label> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="g2"> | |
| <label>Base URL<input id="base" value="https://api.openai.com" placeholder="https://api.openai.com 或代理地址/v1"></label> | |
| <label>Token<input id="token" type="password" placeholder="sk-..."></label> | |
| <label>Model<input id="modelAuto" list="modelList" placeholder="Load models first"><datalist id="modelList"></datalist></label> | |
| </div> | |
| <div class="row"> | |
| <label class="toggle" style="margin-top:0"><input id="tokenShow" type="checkbox"><span>Show Token</span></label> | |
| <button class="s" id="load">Load Models</button> | |
| <span id="ms">Enter baseurl + token to auto load models.</span> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <label>System Prompt(可选)<textarea id="sys">You are a concise assistant.</textarea></label> | |
| <label style="margin-top:8px">User Prompt<textarea id="usr" placeholder="输入要重复调用的内容">写一个1000字的小说</textarea></label> | |
| <div class="row"> | |
| <label class="toggle"><input id="randOn" type="checkbox"><span>添加随机标记</span></label> | |
| <label class="toggle"><input id="streamOn" type="checkbox" checked><span>流式输出</span></label> | |
| </div> | |
| <div class="g3" style="margin-top:8px"> | |
| <label>每线程循环次数<input id="loop" type="number" min="1" value="10"></label> | |
| <label>并发线程数<input id="thr" type="number" min="1" value="3"></label> | |
| <label>max_tokens<input id="max" type="number" min="1" value="1024"></label> | |
| <label>temperature<input id="temp" type="number" min="0" max="2" step="0.1" value="1"></label> | |
| <label>单次超时秒数<input id="to" type="number" min="5" value="600"></label> | |
| <label>循环间隔毫秒<input id="waitBetween" type="number" min="0" value="1000"></label> | |
| <label>累计tokens上限<input id="maxTokens" type="number" min="1000" value="1000000000"></label> | |
| <label>目标输入/输出比例<input id="ratioTarget" type="number" min="0.1" max="20" step="0.1" value="1.0"></label> | |
| </div> | |
| <div class="row"> | |
| <button class="p" id="saveTask">保存任务</button> | |
| <button class="p" id="start">启动任务</button> | |
| <button class="s" id="pause" disabled style="background:#f59e0b;color:#fff">暂停</button> | |
| <button class="d" id="stop" disabled>停止任务</button> | |
| <button class="d" id="deleteTask">删除任务</button> | |
| <span id="rs">尚未开始</span> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="stats"> | |
| <div class="box"><label>目标请求</label><b id="stt">0</b></div> | |
| <div class="box"><label>已完成</label><b id="std">0</b></div> | |
| <div class="box"><label>成功/失败/中止</label><b id="str">0/0/0</b></div> | |
| <div class="box"><label>累计 tokens</label><b id="stk">0</b></div> | |
| <div class="box"><label>输入/输出</label><b id="stp">0/0</b></div> | |
| <div class="box"><label>耗时</label><b id="sec">0s</b></div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="logs" id="logs"><div style="color:#64748b;text-align:center">启动后显示线程日志</div></div> | |
| </div> | |
| </div> | |
| <!-- Config Edit View --> | |
| <div id="configView" style="display:none"> | |
| <div class="card" id="configWarning" style="display:none;background:linear-gradient(145deg,#422006,#1e293b);border-color:#f59e0b"> | |
| <div style="display:flex;align-items:center;gap:10px"> | |
| <span style="font-size:18px">⚠️</span> | |
| <span style="color:#fbbf24">有 <b id="runningTaskCount">0</b> 个任务正在运行,保存配置被禁用。请先暂停或停止运行中的任务(暂停状态可保存)。</span> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="row" style="justify-content:space-between;margin-top:0"> | |
| <span style="color:#94a3b8;font-size:13px">编辑 data/tasks.json 文件内容,或上传文件覆盖</span> | |
| <div style="display:flex;gap:10px"> | |
| <label class="s" style="padding:10px 18px;border-radius:8px;cursor:pointer;display:flex;align-items:center;gap:6px"> | |
| <input type="file" id="configFileInput" accept=".json" style="display:none"> | |
| 上传文件 | |
| </label> | |
| <button class="p" id="saveConfigBtn">保存配置</button> | |
| <button class="s" id="reloadConfigBtn">重新加载</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <textarea id="configEditor" style="width:100%;min-height:500px;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.5;tab-size:2" placeholder="加载中..."></textarea> | |
| </div> | |
| </div> | |
| <!-- HTTP Request View --> | |
| <div id="httpView" style="display:none"> | |
| <div class="card"> | |
| <div class="row" style="justify-content:space-between;margin-top:0"> | |
| <button class="s" id="httpBackBtn">← 返回列表</button> | |
| <div id="httpTaskHeader"> | |
| <span id="httpCurrentTaskLabel" style="font-weight:600;font-size:16px"></span> | |
| <span id="httpCurrentTaskStatus" class="badge idle" style="margin-left:8px">空闲</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card status-card idle" id="httpStatusCard"> | |
| <div class="task-header"> | |
| <div class="task-title"> | |
| <span class="status-dot idle" id="httpDetailStatusDot"></span> | |
| <span class="task-label" id="httpBannerLabel">空闲</span> | |
| </div> | |
| <span class="task-badge idle" id="httpDetailBadge">空闲</span> | |
| </div> | |
| <div id="httpConfigInfo" style="background:rgba(100,116,139,0.1);border:1px solid rgba(100,116,139,0.2);border-radius:8px;padding:10px;margin:10px 0;font-size:12px;color:#94a3b8"> | |
| <div style="display:flex;flex-wrap:wrap;gap:8px 16px"> | |
| <span>URL: <b id="httpInfoUrl" style="color:#60a5fa">-</b></span> | |
| <span>方法: <b id="httpInfoMethod" style="color:#60a5fa">-</b></span> | |
| <span>线程: <b id="httpInfoThreads" style="color:#60a5fa">-</b></span> | |
| <span>循环: <b id="httpInfoLoop" style="color:#60a5fa">-</b></span> | |
| <span>超时: <b id="httpInfoTimeout" style="color:#60a5fa">-</b></span> | |
| </div> | |
| </div> | |
| <div class="task-progress"> | |
| <div class="progress-row"> | |
| <span class="progress-label">任务</span> | |
| <div class="progress-bar"> | |
| <div class="progress-fill task" id="httpDetailTaskProgress" style="width:0%"></div> | |
| </div> | |
| <span class="progress-text" id="httpBannerProgress">0/0</span> | |
| </div> | |
| </div> | |
| <div class="task-meta"> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> | |
| <span id="httpBannerTime">0s</span> | |
| </div> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg> | |
| <span>成功 <b id="httpDetailSuccess" style="color:#4ade80">0</b></span> | |
| </div> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg> | |
| <span>失败 <b id="httpDetailFailed" style="color:#f87171">0</b></span> | |
| </div> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg> | |
| <span>中止 <b id="httpDetailAborted">0</b></span> | |
| </div> | |
| <div class="task-actions-bar" id="httpDetailActions"> | |
| <button class="action-btn start-btn" id="httpDetailStartBtn">启动</button> | |
| <button class="action-btn pause-btn disabled" id="httpDetailPauseBtn" disabled>暂停</button> | |
| <button class="action-btn stop-btn disabled" id="httpDetailStopBtn" disabled>停止</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="g2"> | |
| <label>任务标签<input id="httpLabel" placeholder="给HTTP任务起个名字"></label> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="g2"> | |
| <label>请求方法<select id="httpMethod"> | |
| <option value="GET">GET</option> | |
| <option value="POST">POST</option> | |
| <option value="PUT">PUT</option> | |
| <option value="DELETE">DELETE</option> | |
| <option value="PATCH">PATCH</option> | |
| <option value="HEAD">HEAD</option> | |
| <option value="OPTIONS">OPTIONS</option> | |
| </select></label> | |
| <label>请求URL<input id="httpUrl" placeholder="https://example.com/api" value="https://httpbin.org/get"></label> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <label>请求头 (JSON格式)<textarea id="httpHeaders" placeholder='{"Content-Type": "application/json"}'>{"Content-Type": "application/json"}</textarea></label> | |
| <label style="margin-top:8px">请求体 (POST/PUT时使用)<textarea id="httpBody" placeholder='{"key": "value"}'>{"test": "data"}</textarea></label> | |
| </div> | |
| <div class="card"> | |
| <div class="g3"> | |
| <label>每线程循环次数<input id="httpLoop" type="number" min="1" value="5"></label> | |
| <label>并发线程数<input id="httpThreads" type="number" min="1" value="3"></label> | |
| <label>请求超时秒数<input id="httpTimeout" type="number" min="5" value="30"></label> | |
| <label>循环间隔毫秒<input id="httpWaitBetween" type="number" min="0" value="500"></label> | |
| </div> | |
| <div class="row"> | |
| <button class="p" id="httpSaveTask">保存任务</button> | |
| <button class="p" id="httpStart">启动任务</button> | |
| <button class="s" id="httpPause" disabled style="background:#f59e0b;color:#fff">暂停</button> | |
| <button class="d" id="httpStop" disabled>停止任务</button> | |
| <button class="d" id="httpDeleteTask">删除任务</button> | |
| <span id="httpRs">尚未开始</span> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="stats"> | |
| <div class="box"><label>目标请求</label><b id="httpStt">0</b></div> | |
| <div class="box"><label>已完成</label><b id="httpStd">0</b></div> | |
| <div class="box"><label>成功/失败/中止</label><b id="httpStr">0/0/0</b></div> | |
| <div class="box"><label>总请求数</label><b id="httpStk">0</b></div> | |
| <div class="box"><label>耗时</label><b id="httpSec">0s</b></div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="logs" id="httpLogs"><div style="color:#64748b;text-align:center">启动后显示线程日志</div></div> | |
| </div> | |
| <div class="card"> | |
| <div class="row" style="justify-content:space-between;margin-top:0"> | |
| <span style="color:#94a3b8;font-size:13px">HTTP任务列表</span> | |
| <button class="p" id="httpRefreshBtn">刷新列表</button> | |
| </div> | |
| <div class="task-list" id="httpTaskList"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const $=id=>document.getElementById(id); | |
| const API=location.origin; | |
| let currentTaskId=null; | |
| let pollTimer=null; | |
| let listPollTimer=null; | |
| let tasks=[]; | |
| // Utility functions | |
| const n=(v,d,min,max)=>{v=Number(v);if(!Number.isFinite(v))return d;return Math.min(max,Math.max(min,v))}; | |
| const fmt=n=>{if(n>=1e9)return(n/1e9).toFixed(2)+'B';if(n>=1e6)return(n/1e6).toFixed(2)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'K';return String(n)}; | |
| const fmtTime=ms=>{ | |
| const s=Math.floor(ms/1000); | |
| const h=Math.floor(s/3600); | |
| const m=Math.floor((s%3600)/60); | |
| const sec=s%60; | |
| if(h>0)return`${h}h${m}m${sec}s`; | |
| if(m>0)return`${m}m${sec}s`; | |
| return`${sec}s`; | |
| }; | |
| const ms=(t,c='')=>{$('ms').style.color=c||'#64748b';$('ms').textContent=t}; | |
| const rs=(t,c='')=>{$('rs').style.color=c||'#64748b';$('rs').textContent=t}; | |
| // Local storage for config | |
| const LOCAL_KEY='cpac.tokenConsumer.v2'; | |
| const getLocal=()=>{try{return JSON.parse(localStorage.getItem(LOCAL_KEY)||'{}')}catch{return{}}}; | |
| const saveLocal=d=>{try{localStorage.setItem(LOCAL_KEY,JSON.stringify(d))}catch{}}; | |
| // API functions | |
| async function apiGet(url){const r=await fetch(API+url);return r.json()} | |
| async function apiPost(url,data){const r=await fetch(API+url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});return r.json()} | |
| async function apiDelete(url){const r=await fetch(API+url,{method:'DELETE'});return r.json()} | |
| // Load tasks | |
| async function loadTasks(){ | |
| try{ | |
| tasks=await apiGet('/api/tasks'); | |
| renderTaskList(); | |
| // Start list polling if any task is running | |
| const hasRunning=tasks.some(t=>t.running); | |
| if(hasRunning && !listPollTimer){ | |
| startListPolling(); | |
| }else if(!hasRunning && listPollTimer){ | |
| stopListPolling(); | |
| } | |
| }catch(e){ | |
| console.error('加载任务失败:',e); | |
| renderTaskList(); | |
| } | |
| } | |
| // List polling for task status updates | |
| function startListPolling(){ | |
| if(listPollTimer)return; | |
| listPollTimer=setInterval(loadTasks,2000); | |
| } | |
| function stopListPolling(){ | |
| if(listPollTimer){ | |
| clearInterval(listPollTimer); | |
| listPollTimer=null; | |
| } | |
| } | |
| // Render task list | |
| function renderTaskList(){ | |
| const box=$('taskList'); | |
| if(!tasks.length){ | |
| box.innerHTML='<div class="empty"><div class="empty-icon">📋</div><div>暂无任务,点击"新建任务"创建</div></div>'; | |
| return; | |
| } | |
| box.innerHTML=tasks.map(t=>{ | |
| // currentStats has nested stats, flatten it | |
| const currentStats=t.currentStats||{}; | |
| const stats=currentStats.stats||t.stats||{}; | |
| const taskProgress=stats.total>0?Math.round((stats.completed||0)/stats.total*100):0; | |
| const tokenProgress=t.config?.maxTokens>0?Math.round((stats.totalTokens||0)/t.config.maxTokens*100):0; | |
| const elapsed=currentStats.elapsed||0; | |
| const elapsedStr=elapsed>0?fmtTime(elapsed):''; | |
| let status, statusText; | |
| const isPaused=t.running && t.currentStats?.paused; | |
| if(isPaused){ | |
| status='paused'; | |
| statusText='已暂停'; | |
| }else if(t.running){ | |
| status='running'; | |
| statusText='运行中'; | |
| }else if(t.status==='completed'){ | |
| status='completed'; | |
| statusText='已完成'; | |
| }else if(t.status==='stopped'){ | |
| status='stopped'; | |
| statusText='已停止'; | |
| }else{ | |
| status='idle'; | |
| statusText='空闲'; | |
| } | |
| // Button states (unified logic) | |
| const btnStates=getButtonStates(t.running, isPaused); | |
| return` | |
| <div class="task-item ${t.id===currentTaskId?'selected':''} ${t.running?'running':''} ${isPaused?'paused':''}" data-id="${t.id}"> | |
| <div class="task-header"> | |
| <div class="task-title"> | |
| <span class="status-dot ${status}"></span> | |
| <span class="task-label">${t.label||'未命名任务'}</span> | |
| </div> | |
| <span class="task-badge ${status}">${statusText}</span> | |
| </div> | |
| <div class="task-progress"> | |
| <div class="progress-row"> | |
| <span class="progress-label">任务</span> | |
| <div class="progress-bar"> | |
| <div class="progress-fill task" style="width:${taskProgress}%"></div> | |
| </div> | |
| <span class="progress-text">${stats.completed||0}/${stats.total||0}</span> | |
| </div> | |
| <div class="progress-row"> | |
| <span class="progress-label">Tokens</span> | |
| <div class="progress-bar"> | |
| <div class="progress-fill token" style="width:${Math.min(tokenProgress,100)}%"></div> | |
| </div> | |
| <span class="progress-text">${fmt(stats.totalTokens||0)}/${fmt(t.config?.maxTokens||0)}</span> | |
| </div> | |
| </div> | |
| <div class="task-meta"> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> | |
| <span>${elapsedStr||'-'}</span> | |
| </div> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg> | |
| <span>成功 <b style="color:#4ade80">${stats.success||0}</b></span> | |
| </div> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg> | |
| <span>失败 <b style="color:#f87171">${stats.failed||0}</b></span> | |
| </div> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg> | |
| <span>中止 ${stats.aborted||0}</span> | |
| </div> | |
| <div class="task-actions-bar" data-task-id="${t.id}"> | |
| <button class="action-btn start-btn ${btnStates.canStart?'':'disabled'}" data-action="start" ${btnStates.canStart?'':'disabled'}>${btnStates.isPaused?'继续':'启动'}</button> | |
| <button class="action-btn pause-btn ${btnStates.canPause?'':'disabled'}" data-action="pause" ${btnStates.canPause?'':'disabled'}>暂停</button> | |
| <button class="action-btn stop-btn ${btnStates.canStop?'':'disabled'}" data-action="stop" ${btnStates.canStop?'':'disabled'}>停止</button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| // Add click handlers | |
| box.querySelectorAll('.task-item').forEach(el=>{ | |
| el.addEventListener('click',()=>openTask(el.dataset.id)); | |
| }); | |
| // Add action button handlers | |
| box.querySelectorAll('.action-btn').forEach(btn=>{ | |
| btn.addEventListener('click',async(e)=>{ | |
| e.stopPropagation(); // Prevent card click | |
| const action=btn.dataset.action; | |
| const taskId=btn.closest('.task-actions-bar').dataset.taskId; | |
| if(action==='start'){ | |
| await apiPost('/api/tasks/'+taskId+'/start',{}); | |
| }else if(action==='pause'){ | |
| await apiPost('/api/tasks/'+taskId+'/pause',{}); | |
| }else if(action==='stop'){ | |
| await apiPost('/api/tasks/'+taskId+'/stop',{}); | |
| } | |
| await loadTasks(); | |
| }); | |
| }); | |
| } | |
| // Open task for editing | |
| async function openTask(id){ | |
| currentTaskId=id; | |
| const task=await apiGet('/api/tasks/'+id); | |
| if(task.error){ | |
| alert('加载任务失败:'+task.error); | |
| return; | |
| } | |
| // Load local config first, then merge with server | |
| const local=getLocal(); | |
| const merged={...local,...task.config,id:task.id,label:task.label}; | |
| fillForm(merged); | |
| updateTaskHeader(task); | |
| showEditView(); | |
| startPolling(id); | |
| } | |
| // Fill form with task data | |
| function fillForm(d){ | |
| $('label').value=d.label||''; | |
| $('base').value=d.base||'https://api.openai.com'; | |
| $('token').value=d.token||''; | |
| $('modelAuto').value=d.model||''; | |
| $('sys').value=d.sys||'You are a concise assistant.'; | |
| $('usr').value=d.usr||''; | |
| $('loop').value=d.loop||10; | |
| $('thr').value=d.threads||3; | |
| $('max').value=d.max||1024; | |
| $('temp').value=d.temp||1; | |
| $('to').value=(d.timeout||60000)/1000; | |
| $('waitBetween').value=d.waitBetween||1000; | |
| $('maxTokens').value=d.maxTokens||1000000000; | |
| $('randOn').checked=!!d.randOn; | |
| $('streamOn').checked=d.streamOn!==false; | |
| $('ratioTarget').value=d.ratioTarget||1; | |
| updateConfigInfo(); | |
| } | |
| // Get form data | |
| function getFormData(){ | |
| return{ | |
| id:currentTaskId, | |
| label:$('label').value.trim()||`任务-${Date.now().toString(36)}`, | |
| config:{ | |
| base:$('base').value.trim(), | |
| token:$('token').value.trim(), | |
| model:$('modelAuto').value.trim(), | |
| sys:$('sys').value.trim(), | |
| usr:$('usr').value.trim(), | |
| loop:n($('loop').value,10,1,10000), | |
| threads:n($('thr').value,3,1,200), | |
| max:n($('max').value,1024,1,32768), | |
| temp:n($('temp').value,1,0,2), | |
| timeout:n($('to').value,600,5,3600)*1000, | |
| waitBetween:n($('waitBetween').value,1000,0,60000), | |
| maxTokens:n($('maxTokens').value,1e9,1000,1e12), | |
| randOn:$('randOn').checked, | |
| streamOn:$('streamOn').checked, | |
| ratioTarget:n($('ratioTarget').value,1,0.1,20) | |
| } | |
| }; | |
| } | |
| // Update config info display | |
| function updateConfigInfo(){ | |
| $('infoModel').textContent=$('modelAuto').value||'-'; | |
| $('infoMaxTokens').textContent=$('max').value||1024; | |
| $('infoMaxLimit').textContent=fmt(Number($('maxTokens').value)||1000000000); | |
| $('infoThreads').textContent=$('thr').value||3; | |
| $('infoLoop').textContent=$('loop').value||10; | |
| $('infoTemp').textContent=$('temp').value||1; | |
| } | |
| // Save task | |
| async function saveTask(){ | |
| const data=getFormData(); | |
| try{ | |
| const result=await apiPost('/api/tasks',data); | |
| if(result.error){ | |
| alert('保存失败:'+result.error); | |
| return; | |
| } | |
| currentTaskId=result.id; | |
| // Also save to local | |
| saveLocal({...data.config,id:result.id,label:data.label}); | |
| rs('已保存','#22c55e'); | |
| await loadTasks(); | |
| }catch(e){ | |
| alert('保存失败:'+e.message); | |
| } | |
| } | |
| // Start task | |
| async function startTask(){ | |
| await saveTask(); | |
| try{ | |
| const result=await apiPost('/api/tasks/'+currentTaskId+'/start',{}); | |
| if(result.error){ | |
| alert('启动失败:'+result.error); | |
| return; | |
| } | |
| rs(result.message||'任务已启动','#3b82f6'); | |
| startPolling(currentTaskId); | |
| await loadTasks(); | |
| }catch(e){ | |
| alert('启动失败:'+e.message); | |
| } | |
| } | |
| // Pause task | |
| async function pauseTask(){ | |
| try{ | |
| const result=await apiPost('/api/tasks/'+currentTaskId+'/pause',{}); | |
| if(result.error){ | |
| alert('暂停失败:'+result.error); | |
| return; | |
| } | |
| rs('任务已暂停','#f59e0b'); | |
| updateButtons(true, false, true); // running, paused | |
| await loadTasks(); | |
| }catch(e){ | |
| alert('暂停失败:'+e.message); | |
| } | |
| } | |
| // Stop task | |
| async function stopTask(){ | |
| try{ | |
| const result=await apiPost('/api/tasks/'+currentTaskId+'/stop',{}); | |
| if(result.error){ | |
| alert('停止失败:'+result.error); | |
| return; | |
| } | |
| rs('任务已停止','#f59e0b'); | |
| await loadTasks(); | |
| }catch(e){ | |
| alert('停止失败:'+e.message); | |
| } | |
| } | |
| // Update button states | |
| // Calculate button states (unified logic) | |
| function getButtonStates(running, paused){ | |
| const isRunning=running && !paused; | |
| return{ | |
| canStart:!running || paused, // can start if idle or paused | |
| canPause:isRunning, // can pause if running (not paused) | |
| canStop:running, // can stop if running or paused | |
| isPaused:paused | |
| }; | |
| } | |
| // Update button states (used by both list cards and detail page) | |
| function applyButtonStates(btns, states){ | |
| if(btns.start){ | |
| btns.start.disabled=!states.canStart; | |
| btns.start.textContent=states.isPaused?'继续':'启动'; | |
| btns.start.classList.toggle('disabled', !states.canStart); | |
| } | |
| if(btns.pause){ | |
| btns.pause.disabled=!states.canPause; | |
| btns.pause.classList.toggle('disabled', !states.canPause); | |
| } | |
| if(btns.stop){ | |
| btns.stop.disabled=!states.canStop; | |
| btns.stop.classList.toggle('disabled', !states.canStop); | |
| } | |
| } | |
| function updateButtons(running, paused, stopped){ | |
| const states=getButtonStates(running, paused); | |
| // Bottom config buttons | |
| $('start').disabled=!states.canStart; | |
| $('start').textContent=states.isPaused?'继续':'启动任务'; | |
| $('pause').disabled=!states.canPause; | |
| $('stop').disabled=!states.canStop; | |
| // Detail page action buttons | |
| applyButtonStates({ | |
| start:$('detailStartBtn'), | |
| pause:$('detailPauseBtn'), | |
| stop:$('detailStopBtn') | |
| }, states); | |
| } | |
| // Delete task | |
| async function deleteTask(){ | |
| if(!confirm('确定删除此任务?'))return; | |
| try{ | |
| await apiDelete('/api/tasks/'+currentTaskId); | |
| currentTaskId=null; | |
| stopPolling(); | |
| showListView(); | |
| await loadTasks(); | |
| }catch(e){ | |
| alert('删除失败:'+e.message); | |
| } | |
| } | |
| // Load models | |
| async function loadModels(){ | |
| const base=$('base').value.trim(); | |
| const token=$('token').value.trim(); | |
| if(!base||!token){ | |
| ms('请输入 baseurl + token','#f59e0b'); | |
| return; | |
| } | |
| ms('正在加载模型...','#3b82f6'); | |
| try{ | |
| const result=await apiPost('/api/models',{base,token}); | |
| if(result.error){ | |
| ms('加载失败:'+result.error,'#ef4444'); | |
| return; | |
| } | |
| const models=result.models||[]; | |
| $('modelList').innerHTML=models.map(m=>`<option value="${m}">`).join(''); | |
| if(models.length&&!$('modelAuto').value)$('modelAuto').value=models[0]; | |
| ms('已加载 '+models.length+' 个模型','#22c55e'); | |
| }catch(e){ | |
| ms('加载失败:'+e.message,'#ef4444'); | |
| } | |
| } | |
| // Update stats display | |
| function updateStats(s){ | |
| $('stt').textContent=s.stats?.total||0; | |
| $('std').textContent=s.stats?.completed||0; | |
| $('str').textContent=`${s.stats?.success||0}/${s.stats?.failed||0}/${s.stats?.aborted||0}`; | |
| $('stk').textContent=fmt(s.stats?.totalTokens||0); | |
| const pct=s.stats?.completionTokens>0?(s.stats?.promptTokens/s.stats?.completionTokens*100).toFixed(1):'0'; | |
| $('stp').textContent=`${fmt(s.stats?.promptTokens||0)}/${fmt(s.stats?.completionTokens||0)} (${pct}%)`; | |
| $('sec').textContent=fmtTime(s.elapsed||0); | |
| // Update status card | |
| const total=s.stats?.total||0; | |
| const completed=s.stats?.completed||0; | |
| const totalTokens=s.stats?.totalTokens||0; | |
| const maxTokens=Number($('maxTokens').value)||1000000000; | |
| const taskProgress=total>0?Math.round(completed/total*100):0; | |
| const tokenProgress=maxTokens>0?Math.min(Math.round(totalTokens/maxTokens*100),100):0; | |
| $('detailTaskProgress').style.width=taskProgress+'%'; | |
| $('detailTokenProgress').style.width=tokenProgress+'%'; | |
| $('bannerProgress').textContent=`${completed}/${total}`; | |
| $('bannerTokens').textContent=`${fmt(totalTokens)}/${fmt(maxTokens)}`; | |
| $('bannerTime').textContent=fmtTime(s.elapsed||0); | |
| $('detailSuccess').textContent=s.stats?.success||0; | |
| $('detailFailed').textContent=s.stats?.failed||0; | |
| $('detailAborted').textContent=s.stats?.aborted||0; | |
| } | |
| // Update thread logs | |
| function updateLogs(threadLogs){ | |
| const box=$('logs'); | |
| if(!threadLogs||!Object.keys(threadLogs).length){ | |
| box.innerHTML='<div style="color:#64748b;text-align:center">启动后显示线程日志</div>'; | |
| return; | |
| } | |
| box.innerHTML=Object.entries(threadLogs).map(([id,log])=>{ | |
| const statusClass=log.status==='running'||log.status==='streaming'?'running': | |
| log.status==='success'?'success': | |
| log.status==='error'?'error': | |
| log.status==='aborted'?'stopped': | |
| log.status==='paused'?'paused':'waiting'; | |
| // Display error first if status is error, otherwise show content/message | |
| let displayContent='等待中...'; | |
| if(log.status==='error'){ | |
| displayContent='❌ '+(log.error||'请求失败'); | |
| }else if(log.status==='paused'){ | |
| displayContent='⏸️ '+(log.message||'已暂停'); | |
| }else if(log.status==='aborted'){ | |
| displayContent='⏹️ '+(log.message||'已中止'); | |
| }else if(log.content){ | |
| displayContent=log.content; | |
| }else if(log.message){ | |
| displayContent=log.message; | |
| } | |
| return` | |
| <div class="log ${statusClass}"> | |
| <div class="t"><span>线程 #${id}</span><span>${log.status}</span></div> | |
| <div class="m">循环:${log.loop||'-'}</div> | |
| <pre>${displayContent.slice(-500)}</pre> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| // Update task header | |
| function updateTaskHeader(task){ | |
| $('currentTaskLabel').textContent=task.label||'未命名任务'; | |
| let status, statusText; | |
| if(task.running && task.currentStats?.paused){ | |
| status='paused'; | |
| statusText='已暂停'; | |
| }else if(task.running){ | |
| status='running'; | |
| statusText='运行中'; | |
| }else if(task.status==='completed'){ | |
| status='completed'; | |
| statusText='已完成'; | |
| }else if(task.status==='stopped'){ | |
| status='stopped'; | |
| statusText='已停止'; | |
| }else{ | |
| status='idle'; | |
| statusText='空闲'; | |
| } | |
| const badge=$('currentTaskStatus'); | |
| badge.textContent=statusText; | |
| badge.className='badge '+status; | |
| const statusCard=$('statusCard'); | |
| statusCard.className='card status-card '+status; | |
| $('detailStatusDot').className='status-dot '+status; | |
| $('detailBadge').className='task-badge '+status; | |
| $('detailBadge').textContent=statusText; | |
| $('bannerLabel').textContent=statusText; | |
| if(task.config){ | |
| $('infoModel').textContent=task.config.model||'-'; | |
| $('infoMaxTokens').textContent=task.config.max||'-'; | |
| $('infoMaxLimit').textContent=fmt(task.config.maxTokens||0); | |
| $('infoThreads').textContent=task.config.threads||'-'; | |
| $('infoLoop').textContent=task.config.loop||'-'; | |
| $('infoTemp').textContent=task.config.temp||'-'; | |
| } | |
| updateButtons(task.running, task.currentStats?.paused, task.stopped); | |
| } | |
| // Start polling for task status | |
| function startPolling(taskId){ | |
| stopPolling(); | |
| pollTimer=setInterval(async()=>{ | |
| try{ | |
| const s=await apiGet('/api/tasks/'+taskId+'/status'); | |
| updateStats(s); | |
| updateLogs(s.threadLogs); | |
| updateButtons(s.running, s.paused, s.stopped); | |
| let status='idle', statusText='空闲'; | |
| if(s.running && s.paused){ | |
| status='paused'; | |
| statusText='已暂停'; | |
| }else if(s.running){ | |
| status='running'; | |
| statusText='运行中'; | |
| }else if(s.stopped){ | |
| status='stopped'; | |
| statusText='已停止'; | |
| } | |
| const statusCard=$('statusCard'); | |
| statusCard.className='card status-card '+status; | |
| $('detailStatusDot').className='status-dot '+status; | |
| $('detailBadge').className='task-badge '+status; | |
| $('detailBadge').textContent=statusText; | |
| $('bannerLabel').textContent=statusText; | |
| const task=tasks.find(t=>t.id===taskId); | |
| if(task?.config){ | |
| $('infoModel').textContent=task.config.model||'-'; | |
| $('infoMaxTokens').textContent=task.config.max||'-'; | |
| $('infoMaxLimit').textContent=fmt(task.config.maxTokens||0); | |
| $('infoThreads').textContent=task.config.threads||'-'; | |
| $('infoLoop').textContent=task.config.loop||'-'; | |
| $('infoTemp').textContent=task.config.temp||'-'; | |
| } | |
| if(!s.running){ | |
| $('start').textContent='启动任务'; | |
| await loadTasks(); | |
| }else{ | |
| $('start').textContent=s.paused?'继续':'启动任务'; | |
| } | |
| }catch(e){ | |
| console.error('轮询失败:',e); | |
| } | |
| },1000); | |
| } | |
| // Stop polling | |
| function stopPolling(){ | |
| if(pollTimer){ | |
| clearInterval(pollTimer); | |
| pollTimer=null; | |
| } | |
| } | |
| // View navigation | |
| function showListView(){ | |
| stopPolling(); | |
| currentTaskId=null; | |
| $('listView').style.display='block'; | |
| $('editView').style.display='none'; | |
| $('configView').style.display='none'; | |
| $('navList').classList.add('active'); | |
| $('navNew').classList.remove('active'); | |
| $('navConfig').classList.remove('active'); | |
| loadTasks(); // This will start list polling if needed | |
| } | |
| function showEditView(){ | |
| stopListPolling(); | |
| $('listView').style.display='none'; | |
| $('editView').style.display='block'; | |
| $('configView').style.display='none'; | |
| $('navList').classList.remove('active'); | |
| $('navNew').classList.add('active'); | |
| $('navConfig').classList.remove('active'); | |
| } | |
| function showConfigView(){ | |
| stopPolling(); | |
| stopListPolling(); | |
| $('listView').style.display='none'; | |
| $('editView').style.display='none'; | |
| $('configView').style.display='block'; | |
| $('navList').classList.remove('active'); | |
| $('navNew').classList.remove('active'); | |
| $('navConfig').classList.add('active'); | |
| // Load tasks first to check running status, then load config | |
| loadTasks().then(()=>{ | |
| updateConfigWarning(); | |
| loadConfigRaw(); | |
| }); | |
| } | |
| // Config Editor functions | |
| async function loadConfigRaw(){ | |
| try{ | |
| const result=await apiGet('/api/tasks-raw'); | |
| if(result.error){ | |
| alert('加载失败: '+result.error); | |
| return; | |
| } | |
| // Format JSON | |
| const parsed=JSON.parse(result.content); | |
| $('configEditor').value=JSON.stringify(parsed, null, 2); | |
| // Update running status | |
| updateConfigWarning(); | |
| }catch(e){ | |
| alert('加载失败: '+e.message); | |
| } | |
| } | |
| function updateConfigWarning(){ | |
| // Only count tasks that are running AND not paused (paused tasks don't write to file) | |
| const runningCount=tasks.filter(t=>t.running && !t.currentStats?.paused).length; | |
| const warning=$('configWarning'); | |
| const count=$('runningTaskCount'); | |
| const saveBtn=$('saveConfigBtn'); | |
| count.textContent=runningCount; | |
| if(runningCount>0){ | |
| warning.style.display='block'; | |
| saveBtn.disabled=true; | |
| saveBtn.classList.remove('p'); | |
| saveBtn.classList.add('s'); | |
| saveBtn.style.opacity='0.5'; | |
| }else{ | |
| warning.style.display='none'; | |
| saveBtn.disabled=false; | |
| saveBtn.classList.remove('s'); | |
| saveBtn.classList.add('p'); | |
| saveBtn.style.opacity='1'; | |
| } | |
| } | |
| async function saveConfigRaw(){ | |
| const content=$('configEditor').value; | |
| if(!content.trim()){ | |
| alert('内容不能为空'); | |
| return; | |
| } | |
| // Validate JSON first | |
| try{ | |
| JSON.parse(content); | |
| }catch(e){ | |
| alert('JSON 格式无效: '+e.message); | |
| return; | |
| } | |
| if(!confirm('确定要保存配置吗?这将覆盖现有的 tasks.json 文件。'))return; | |
| try{ | |
| const result=await apiPost('/api/tasks-raw', {content}); | |
| if(result.error){ | |
| // Check if it's a conflict error (task running) | |
| if(result.error.includes('正在运行')){ | |
| alert('无法保存:' + result.error + '\n\n提示:暂停的任务可以保存配置,请先暂停或停止运行中的任务。'); | |
| }else{ | |
| alert('保存失败: '+result.error); | |
| } | |
| return; | |
| } | |
| alert('保存成功'); | |
| await loadTasks(); | |
| }catch(e){ | |
| alert('保存失败: '+e.message); | |
| } | |
| } | |
| function handleConfigFileUpload(e){ | |
| const file=e.target.files[0]; | |
| if(!file)return; | |
| const reader=new FileReader(); | |
| reader.onload=(ev)=>{ | |
| try{ | |
| const content=ev.target.result; | |
| const parsed=JSON.parse(content); | |
| $('configEditor').value=JSON.stringify(parsed, null, 2); | |
| alert('文件已加载,点击"保存配置"按钮保存'); | |
| }catch(err){ | |
| alert('文件不是有效的 JSON 格式: '+err.message); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| e.target.value=''; // Reset input | |
| } | |
| // Create new task | |
| function newTask(){ | |
| currentTaskId=null; | |
| const local=getLocal(); | |
| fillForm(local); | |
| $('currentTaskLabel').textContent='新任务'; | |
| $('currentTaskStatus').textContent='新建'; | |
| $('currentTaskStatus').className='badge idle'; | |
| // Reset status card | |
| const statusCard=$('statusCard'); | |
| statusCard.className='card status-card idle'; | |
| $('detailStatusDot').className='status-dot idle'; | |
| $('detailBadge').className='task-badge idle'; | |
| $('detailBadge').textContent='新建'; | |
| $('bannerLabel').textContent='新建'; | |
| $('detailTaskProgress').style.width='0%'; | |
| $('detailTokenProgress').style.width='0%'; | |
| $('bannerProgress').textContent='0/0'; | |
| $('bannerTokens').textContent='0/0'; | |
| $('bannerTime').textContent='0s'; | |
| $('detailSuccess').textContent='0'; | |
| $('detailFailed').textContent='0'; | |
| $('detailAborted').textContent='0'; | |
| // Update config info | |
| $('infoModel').textContent=local.model||'-'; | |
| $('infoMaxTokens').textContent=local.max||1024; | |
| $('infoMaxLimit').textContent=fmt(local.maxTokens||1000000000); | |
| $('infoThreads').textContent=local.threads||3; | |
| $('infoLoop').textContent=local.loop||10; | |
| $('infoTemp').textContent=local.temp||1; | |
| updateButtons(false, false, false); | |
| showEditView(); | |
| stopPolling(); | |
| } | |
| // Event handlers | |
| $('navList').onclick=showListView; | |
| $('navNew').onclick=newTask; | |
| $('navConfig').onclick=showConfigView; | |
| $('backBtn').onclick=showListView; | |
| $('refreshBtn').onclick=loadTasks; | |
| $('load').onclick=loadModels; | |
| $('saveTask').onclick=saveTask; | |
| $('start').onclick=startTask; | |
| $('pause').onclick=pauseTask; | |
| $('stop').onclick=stopTask; | |
| $('deleteTask').onclick=deleteTask; | |
| $('tokenShow').onchange=()=>$('token').type=$('tokenShow').checked?'text':'password'; | |
| $('saveConfigBtn').onclick=saveConfigRaw; | |
| $('reloadConfigBtn').onclick=loadConfigRaw; | |
| $('configFileInput').onchange=handleConfigFileUpload; | |
| // Detail page action buttons | |
| $('detailStartBtn').onclick=startTask; | |
| $('detailPauseBtn').onclick=pauseTask; | |
| $('detailStopBtn').onclick=stopTask; | |
| // Config info update on input change | |
| ['modelAuto','max','maxTokens','thr','loop','temp'].forEach(id=>{ | |
| $(id).addEventListener('input',updateConfigInfo); | |
| }); | |
| // Memory status update | |
| function formatBytes(bytes){ | |
| if(bytes>=1e9)return(bytes/1e9).toFixed(1)+' GB'; | |
| if(bytes>=1e6)return(bytes/1e6).toFixed(0)+' MB'; | |
| if(bytes>=1e3)return(bytes/1e3).toFixed(0)+' KB'; | |
| return bytes+' B'; | |
| } | |
| async function updateMemoryStatus(){ | |
| try{ | |
| const mem=await apiGet('/api/memory'); | |
| $('processMem').textContent=formatBytes(mem.process.rss); | |
| $('systemMem').textContent=formatBytes(mem.system.used)+' / '+formatBytes(mem.system.total); | |
| $('systemMemPercent').textContent=mem.system.usagePercent+'%'; | |
| // Color based on usage | |
| const percent=mem.system.usagePercent; | |
| const el=$('systemMemPercent'); | |
| if(percent>90)el.style.color='#ef4444'; | |
| else if(percent>70)el.style.color='#f59e0b'; | |
| else el.style.color='#60a5fa'; | |
| }catch(e){ | |
| console.error('Memory status error:',e); | |
| } | |
| } | |
| // Update memory every 5 seconds | |
| setInterval(updateMemoryStatus,5000); | |
| updateMemoryStatus(); | |
| // Auto-load models on base/token change | |
| let loadTimer=null; | |
| const autoLoad=()=>{ | |
| if(loadTimer)clearTimeout(loadTimer); | |
| loadTimer=setTimeout(()=>{ | |
| if($('base').value.trim()&&$('token').value.trim())loadModels(); | |
| },500); | |
| }; | |
| $('base').oninput=autoLoad; | |
| $('token').oninput=autoLoad; | |
| // HTTP Task variables | |
| let httpCurrentTaskId = null; | |
| let httpPollTimer = null; | |
| let httpTasks = []; | |
| // HTTP API functions | |
| async function httpApiGet(url) { const r = await fetch(API + url); return r.json(); } | |
| async function httpApiPost(url, data) { const r = await fetch(API + url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return r.json(); } | |
| async function httpApiDelete(url) { const r = await fetch(API + url, { method: 'DELETE' }); return r.json(); } | |
| // Load HTTP tasks | |
| async function loadHttpTasks() { | |
| try { | |
| httpTasks = await httpApiGet('/api/http-tasks'); | |
| renderHttpTaskList(); | |
| } catch (e) { | |
| console.error('加载HTTP任务失败:', e); | |
| renderHttpTaskList(); | |
| } | |
| } | |
| // Render HTTP task list | |
| function renderHttpTaskList() { | |
| const box = $('httpTaskList'); | |
| if (!httpTasks.length) { | |
| box.innerHTML = '<div class="empty"><div class="empty-icon">🌐</div><div>暂无HTTP任务</div></div>'; | |
| return; | |
| } | |
| box.innerHTML = httpTasks.map(t => { | |
| const stats = t.currentStats?.stats || t.stats || {}; | |
| const taskProgress = stats.total > 0 ? Math.round((stats.completed || 0) / stats.total * 100) : 0; | |
| const elapsed = t.currentStats?.elapsed || 0; | |
| const elapsedStr = elapsed > 0 ? fmtTime(elapsed) : ''; | |
| let status, statusText; | |
| const isPaused = t.running && t.currentStats?.paused; | |
| if (isPaused) { | |
| status = 'paused'; | |
| statusText = '已暂停'; | |
| } else if (t.running) { | |
| status = 'running'; | |
| statusText = '运行中'; | |
| } else if (t.status === 'completed') { | |
| status = 'completed'; | |
| statusText = '已完成'; | |
| } else if (t.status === 'stopped') { | |
| status = 'stopped'; | |
| statusText = '已停止'; | |
| } else { | |
| status = 'idle'; | |
| statusText = '空闲'; | |
| } | |
| return ` | |
| <div class="task-item ${t.id === httpCurrentTaskId ? 'selected' : ''} ${t.running ? 'running' : ''} ${isPaused ? 'paused' : ''}" data-id="${t.id}"> | |
| <div class="task-header"> | |
| <div class="task-title"> | |
| <span class="status-dot ${status}"></span> | |
| <span class="task-label">${t.label || '未命名HTTP任务'}</span> | |
| </div> | |
| <span class="task-badge ${status}">${statusText}</span> | |
| </div> | |
| <div class="task-progress"> | |
| <div class="progress-row"> | |
| <span class="progress-label">任务</span> | |
| <div class="progress-bar"> | |
| <div class="progress-fill task" style="width:${taskProgress}%"></div> | |
| </div> | |
| <span class="progress-text">${stats.completed || 0}/${stats.total || 0}</span> | |
| </div> | |
| </div> | |
| <div class="task-meta"> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> | |
| <span>${elapsedStr || '-'}</span> | |
| </div> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg> | |
| <span>成功 <b style="color:#4ade80">${stats.success || 0}</b></span> | |
| </div> | |
| <div class="meta-item"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg> | |
| <span>失败 <b style="color:#f87171">${stats.failed || 0}</b></span> | |
| </div> | |
| <div class="task-actions-bar" data-task-id="${t.id}"> | |
| <button class="action-btn start-btn ${t.running && !isPaused ? 'disabled' : ''}" data-action="start" ${t.running && !isPaused ? 'disabled' : ''}>${isPaused ? '继续' : '启动'}</button> | |
| <button class="action-btn pause-btn ${!t.running || isPaused ? 'disabled' : ''}" data-action="pause" ${!t.running || isPaused ? 'disabled' : ''}>暂停</button> | |
| <button class="action-btn stop-btn ${!t.running ? 'disabled' : ''}" data-action="stop" ${!t.running ? 'disabled' : ''}>停止</button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| box.querySelectorAll('.task-item').forEach(el => { | |
| el.addEventListener('click', () => openHttpTask(el.dataset.id)); | |
| }); | |
| box.querySelectorAll('.action-btn').forEach(btn => { | |
| btn.addEventListener('click', async(e) => { | |
| e.stopPropagation(); | |
| const action = btn.dataset.action; | |
| const taskId = btn.closest('.task-actions-bar').dataset.taskId; | |
| if (action === 'start') { | |
| await httpApiPost('/api/http-tasks/' + taskId + '/start', {}); | |
| } else if (action === 'pause') { | |
| await httpApiPost('/api/http-tasks/' + taskId + '/pause', {}); | |
| } else if (action === 'stop') { | |
| await httpApiPost('/api/http-tasks/' + taskId + '/stop', {}); | |
| } | |
| await loadHttpTasks(); | |
| }); | |
| }); | |
| } | |
| // Open HTTP task | |
| async function openHttpTask(id) { | |
| httpCurrentTaskId = id; | |
| const task = await httpApiGet('/api/http-tasks/' + id); | |
| if (task.error) { | |
| alert('加载HTTP任务失败:' + task.error); | |
| return; | |
| } | |
| fillHttpForm(task); | |
| updateHttpTaskHeader(task); | |
| startHttpPolling(id); | |
| } | |
| // Fill HTTP form | |
| function fillHttpForm(d) { | |
| $('httpLabel').value = d.label || ''; | |
| $('httpMethod').value = d.config?.method || 'GET'; | |
| $('httpUrl').value = d.config?.url || ''; | |
| $('httpHeaders').value = d.config?.headers || '{}'; | |
| $('httpBody').value = d.config?.body || ''; | |
| $('httpLoop').value = d.config?.loop || 5; | |
| $('httpThreads').value = d.config?.threads || 3; | |
| $('httpTimeout').value = (d.config?.timeout || 30000) / 1000; | |
| $('httpWaitBetween').value = d.config?.waitBetween || 500; | |
| updateHttpConfigInfo(); | |
| } | |
| // Get HTTP form data | |
| function getHttpFormData() { | |
| return { | |
| id: httpCurrentTaskId, | |
| label: $('httpLabel').value.trim() || `HTTP任务-${Date.now().toString(36)}`, | |
| config: { | |
| url: $('httpUrl').value.trim(), | |
| method: $('httpMethod').value, | |
| headers: $('httpHeaders').value.trim() || '{}', | |
| body: $('httpBody').value.trim(), | |
| loop: n($('httpLoop').value, 5, 1, 10000), | |
| threads: n($('httpThreads').value, 3, 1, 200), | |
| timeout: n($('httpTimeout').value, 30, 5, 3600) * 1000, | |
| waitBetween: n($('httpWaitBetween').value, 500, 0, 60000) | |
| } | |
| }; | |
| } | |
| // Update HTTP config info | |
| function updateHttpConfigInfo() { | |
| $('httpInfoUrl').textContent = $('httpUrl').value || '-'; | |
| $('httpInfoMethod').textContent = $('httpMethod').value || '-'; | |
| $('httpInfoThreads').textContent = $('httpThreads').value || 3; | |
| $('httpInfoLoop').textContent = $('httpLoop').value || 5; | |
| $('httpInfoTimeout').textContent = $('httpTimeout').value + 's'; | |
| } | |
| // Save HTTP task | |
| async function saveHttpTask() { | |
| const data = getHttpFormData(); | |
| try { | |
| const result = await httpApiPost('/api/http-tasks', data); | |
| if (result.error) { | |
| alert('保存失败:' + result.error); | |
| return; | |
| } | |
| httpCurrentTaskId = result.id; | |
| $('httpRs').textContent = '已保存'; | |
| $('httpRs').style.color = '#22c55e'; | |
| await loadHttpTasks(); | |
| } catch (e) { | |
| alert('保存失败:' + e.message); | |
| } | |
| } | |
| // Start HTTP task | |
| async function startHttpTask() { | |
| await saveHttpTask(); | |
| try { | |
| const result = await httpApiPost('/api/http-tasks/' + httpCurrentTaskId + '/start', {}); | |
| if (result.error) { | |
| alert('启动失败:' + result.error); | |
| return; | |
| } | |
| $('httpRs').textContent = result.message || '任务已启动'; | |
| $('httpRs').style.color = '#3b82f6'; | |
| startHttpPolling(httpCurrentTaskId); | |
| await loadHttpTasks(); | |
| } catch (e) { | |
| alert('启动失败:' + e.message); | |
| } | |
| } | |
| // Pause HTTP task | |
| async function pauseHttpTask() { | |
| try { | |
| const result = await httpApiPost('/api/http-tasks/' + httpCurrentTaskId + '/pause', {}); | |
| if (result.error) { | |
| alert('暂停失败:' + result.error); | |
| return; | |
| } | |
| $('httpRs').textContent = '任务已暂停'; | |
| $('httpRs').style.color = '#f59e0b'; | |
| await loadHttpTasks(); | |
| } catch (e) { | |
| alert('暂停失败:' + e.message); | |
| } | |
| } | |
| // Stop HTTP task | |
| async function stopHttpTask() { | |
| try { | |
| const result = await httpApiPost('/api/http-tasks/' + httpCurrentTaskId + '/stop', {}); | |
| if (result.error) { | |
| alert('停止失败:' + result.error); | |
| return; | |
| } | |
| $('httpRs').textContent = '任务已停止'; | |
| $('httpRs').style.color = '#f59e0b'; | |
| await loadHttpTasks(); | |
| } catch (e) { | |
| alert('停止失败:' + e.message); | |
| } | |
| } | |
| // Delete HTTP task | |
| async function deleteHttpTask() { | |
| if (!confirm('确定删除此HTTP任务?')) return; | |
| try { | |
| await httpApiDelete('/api/http-tasks/' + httpCurrentTaskId); | |
| httpCurrentTaskId = null; | |
| stopHttpPolling(); | |
| fillHttpForm({}); | |
| updateHttpTaskHeader({ label: '新任务', status: 'idle', config: {} }); | |
| await loadHttpTasks(); | |
| } catch (e) { | |
| alert('删除失败:' + e.message); | |
| } | |
| } | |
| // Update HTTP stats | |
| function updateHttpStats(s) { | |
| $('httpStt').textContent = s.stats?.total || 0; | |
| $('httpStd').textContent = s.stats?.completed || 0; | |
| $('httpStr').textContent = `${s.stats?.success || 0}/${s.stats?.failed || 0}/${s.stats?.aborted || 0}`; | |
| $('httpStk').textContent = s.stats?.totalRequests || 0; | |
| $('httpSec').textContent = fmtTime(s.elapsed || 0); | |
| const total = s.stats?.total || 0; | |
| const completed = s.stats?.completed || 0; | |
| const taskProgress = total > 0 ? Math.round(completed / total * 100) : 0; | |
| $('httpDetailTaskProgress').style.width = taskProgress + '%'; | |
| $('httpBannerProgress').textContent = `${completed}/${total}`; | |
| $('httpBannerTime').textContent = fmtTime(s.elapsed || 0); | |
| $('httpDetailSuccess').textContent = s.stats?.success || 0; | |
| $('httpDetailFailed').textContent = s.stats?.failed || 0; | |
| $('httpDetailAborted').textContent = s.stats?.aborted || 0; | |
| } | |
| // Update HTTP logs | |
| function updateHttpLogs(threadLogs) { | |
| const box = $('httpLogs'); | |
| if (!threadLogs || !Object.keys(threadLogs).length) { | |
| box.innerHTML = '<div style="color:#64748b;text-align:center">启动后显示线程日志</div>'; | |
| return; | |
| } | |
| box.innerHTML = Object.entries(threadLogs).map(([id, log]) => { | |
| const statusClass = log.status === 'running' ? 'running' : | |
| log.status === 'success' ? 'success' : | |
| log.status === 'error' ? 'error' : | |
| log.status === 'aborted' ? 'stopped' : | |
| log.status === 'paused' ? 'paused' : 'waiting'; | |
| let displayContent = '等待中...'; | |
| if (log.status === 'error') { | |
| displayContent = `❌ 错误 [${log.errorCode || '未知'}]: ${log.errorMessage || log.message || '请求失败'}`; | |
| } else if (log.status === 'paused') { | |
| displayContent = '⏸️ ' + (log.message || '已暂停'); | |
| } else if (log.status === 'aborted') { | |
| displayContent = '⏹️ ' + (log.message || '已中止'); | |
| } else if (log.responseData) { | |
| displayContent = log.responseData.slice(-1000); | |
| } else if (log.message) { | |
| displayContent = log.message; | |
| } | |
| return ` | |
| <div class="log ${statusClass}"> | |
| <div class="t"><span>线程 #${id}</span><span>${log.status}</span></div> | |
| <div class="m">循环:${log.loop || '-'}</div> | |
| <pre>${displayContent}</pre> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| // Update HTTP task header | |
| function updateHttpTaskHeader(task) { | |
| $('httpCurrentTaskLabel').textContent = task.label || '新任务'; | |
| let status, statusText; | |
| if (task.running && task.currentStats?.paused) { | |
| status = 'paused'; | |
| statusText = '已暂停'; | |
| } else if (task.running) { | |
| status = 'running'; | |
| statusText = '运行中'; | |
| } else if (task.status === 'completed') { | |
| status = 'completed'; | |
| statusText = '已完成'; | |
| } else if (task.status === 'stopped') { | |
| status = 'stopped'; | |
| statusText = '已停止'; | |
| } else { | |
| status = 'idle'; | |
| statusText = '空闲'; | |
| } | |
| const badge = $('httpCurrentTaskStatus'); | |
| badge.textContent = statusText; | |
| badge.className = 'badge ' + status; | |
| const statusCard = $('httpStatusCard'); | |
| statusCard.className = 'card status-card ' + status; | |
| $('httpDetailStatusDot').className = 'status-dot ' + status; | |
| $('httpDetailBadge').className = 'task-badge ' + status; | |
| $('httpDetailBadge').textContent = statusText; | |
| $('httpBannerLabel').textContent = statusText; | |
| if (task.config) { | |
| $('httpInfoUrl').textContent = task.config.url || '-'; | |
| $('httpInfoMethod').textContent = task.config.method || '-'; | |
| $('httpInfoThreads').textContent = task.config.threads || '-'; | |
| $('httpInfoLoop').textContent = task.config.loop || '-'; | |
| $('httpInfoTimeout').textContent = (task.config.timeout ? task.config.timeout / 1000 : 30) + 's'; | |
| } | |
| updateHttpButtons(task.running, task.currentStats?.paused); | |
| } | |
| // Update HTTP buttons | |
| function updateHttpButtons(running, paused) { | |
| const isRunning = running && !paused; | |
| $('httpStart').disabled = running && !paused; | |
| $('httpStart').textContent = paused ? '继续' : '启动任务'; | |
| $('httpPause').disabled = !isRunning; | |
| $('httpStop').disabled = !running; | |
| $('httpDetailStartBtn').disabled = running && !paused; | |
| $('httpDetailStartBtn').textContent = paused ? '继续' : '启动'; | |
| $('httpDetailStartBtn').classList.toggle('disabled', running && !paused); | |
| $('httpDetailPauseBtn').disabled = !isRunning; | |
| $('httpDetailPauseBtn').classList.toggle('disabled', !isRunning); | |
| $('httpDetailStopBtn').disabled = !running; | |
| $('httpDetailStopBtn').classList.toggle('disabled', !running); | |
| } | |
| // Start HTTP polling | |
| function startHttpPolling(taskId) { | |
| stopHttpPolling(); | |
| httpPollTimer = setInterval(async () => { | |
| try { | |
| const s = await httpApiGet('/api/http-tasks/' + taskId + '/status'); | |
| updateHttpStats(s); | |
| updateHttpLogs(s.threadLogs); | |
| updateHttpButtons(s.running, s.paused); | |
| let status = 'idle', statusText = '空闲'; | |
| if (s.running && s.paused) { | |
| status = 'paused'; | |
| statusText = '已暂停'; | |
| } else if (s.running) { | |
| status = 'running'; | |
| statusText = '运行中'; | |
| } else if (s.stopped) { | |
| status = 'stopped'; | |
| statusText = '已停止'; | |
| } | |
| $('httpStatusCard').className = 'card status-card ' + status; | |
| $('httpDetailStatusDot').className = 'status-dot ' + status; | |
| $('httpDetailBadge').className = 'task-badge ' + status; | |
| $('httpDetailBadge').textContent = statusText; | |
| $('httpBannerLabel').textContent = statusText; | |
| if (!s.running) { | |
| $('httpStart').textContent = '启动任务'; | |
| await loadHttpTasks(); | |
| } else { | |
| $('httpStart').textContent = s.paused ? '继续' : '启动任务'; | |
| } | |
| } catch (e) { | |
| console.error('HTTP轮询失败:', e); | |
| } | |
| }, 1000); | |
| } | |
| // Stop HTTP polling | |
| function stopHttpPolling() { | |
| if (httpPollTimer) { | |
| clearInterval(httpPollTimer); | |
| httpPollTimer = null; | |
| } | |
| } | |
| // Show HTTP view | |
| function showHttpView() { | |
| stopPolling(); | |
| stopHttpPolling(); | |
| httpCurrentTaskId = null; | |
| $('listView').style.display = 'none'; | |
| $('editView').style.display = 'none'; | |
| $('configView').style.display = 'none'; | |
| $('httpView').style.display = 'block'; | |
| $('navList').classList.remove('active'); | |
| $('navNew').classList.remove('active'); | |
| $('navHttp').classList.add('active'); | |
| $('navConfig').classList.remove('active'); | |
| fillHttpForm({}); | |
| updateHttpTaskHeader({ label: '新任务', status: 'idle', config: {} }); | |
| loadHttpTasks(); | |
| } | |
| // HTTP event handlers | |
| $('navHttp').onclick = showHttpView; | |
| $('httpBackBtn').onclick = showListView; | |
| $('httpRefreshBtn').onclick = loadHttpTasks; | |
| $('httpSaveTask').onclick = saveHttpTask; | |
| $('httpStart').onclick = startHttpTask; | |
| $('httpPause').onclick = pauseHttpTask; | |
| $('httpStop').onclick = stopHttpTask; | |
| $('httpDeleteTask').onclick = deleteHttpTask; | |
| $('httpDetailStartBtn').onclick = startHttpTask; | |
| $('httpDetailPauseBtn').onclick = pauseHttpTask; | |
| $('httpDetailStopBtn').onclick = stopHttpTask; | |
| // HTTP config info update on input change | |
| ['httpUrl', 'httpMethod', 'httpThreads', 'httpLoop', 'httpTimeout'].forEach(id => { | |
| $(id).addEventListener('input', updateHttpConfigInfo); | |
| }); | |
| // Init | |
| showListView(); | |
| loadTasks(); | |
| // Load local config and try to merge with server | |
| (async()=>{ | |
| const local=getLocal(); | |
| if(local.base||local.token){ | |
| $('base').value=local.base||'https://api.openai.com'; | |
| $('token').value=local.token||''; | |
| } | |
| if(local.model)$('modelAuto').value=local.model; | |
| if(local.sys)$('sys').value=local.sys; | |
| if(local.usr)$('usr').value=local.usr; | |
| if(local.loop)$('loop').value=local.loop; | |
| if(local.threads)$('thr').value=local.threads; | |
| if(local.max)$('max').value=local.max; | |
| if(local.temp)$('temp').value=local.temp; | |
| if(local.timeout)$('to').value=local.timeout/1000; | |
| if(local.waitBetween)$('waitBetween').value=local.waitBetween; | |
| if(local.maxTokens)$('maxTokens').value=local.maxTokens; | |
| if(local.ratioTarget)$('ratioTarget').value=local.ratioTarget; | |
| if(local.randOn!==undefined)$('randOn').checked=local.randOn; | |
| if(local.streamOn!==undefined)$('streamOn').checked=local.streamOn; | |
| // Load server config | |
| try{ | |
| const serverConfig=await apiGet('/api/config'); | |
| if(Object.keys(serverConfig).length){ | |
| saveLocal({...local,...serverConfig}); | |
| if(serverConfig.base)$('base').value=serverConfig.base; | |
| if(serverConfig.token)$('token').value=serverConfig.token; | |
| if(serverConfig.model)$('modelAuto').value=serverConfig.model; | |
| if(serverConfig.sys)$('sys').value=serverConfig.sys; | |
| if(serverConfig.usr)$('usr').value=serverConfig.usr; | |
| if(serverConfig.loop)$('loop').value=serverConfig.loop; | |
| if(serverConfig.threads)$('thr').value=serverConfig.threads; | |
| if(serverConfig.max)$('max').value=serverConfig.max; | |
| if(serverConfig.temp)$('temp').value=serverConfig.temp; | |
| if(serverConfig.timeout)$('to').value=serverConfig.timeout/1000; | |
| if(serverConfig.waitBetween)$('waitBetween').value=serverConfig.waitBetween; | |
| if(serverConfig.maxTokens)$('maxTokens').value=serverConfig.maxTokens; | |
| if(serverConfig.ratioTarget)$('ratioTarget').value=serverConfig.ratioTarget; | |
| if(serverConfig.randOn!==undefined)$('randOn').checked=serverConfig.randOn; | |
| if(serverConfig.streamOn!==undefined)$('streamOn').checked=serverConfig.streamOn; | |
| } | |
| }catch(e){} | |
| if($('base').value.trim()&&$('token').value.trim()){ | |
| loadModels(); | |
| } | |
| })(); | |
| </script> | |
| </body> | |
| </html> |