token-consumer / token-consumer.html
qwen2api's picture
Upload 11 files
8a81249 verified
Raw
History Blame Contribute Delete
70.7 kB
<!doctype html>
<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>