Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

494 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>语料采集 · Corpus Agent</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--deep:#0b1424;--mid:#141e30;--shallow:#1a2a44;
--cyan:#2a9dbf;--glow:#5ad0e0;--light:#d4eaf0;
--violet:#7b5dbf;--heart:#e04a74;--green:#40b87a;
--text:#e4ecf5;--text-soft:#8aa0b8;--text-faint:#5a7088;
--glass:rgba(11,20,36,.7);--glass-edge:rgba(90,208,224,.08);
--card-bg:rgba(11,20,36,.4);--border:rgba(90,208,224,.10);
--shadow:0 8px 32px rgba(0,0,0,.3);
}
@keyframes breathe{0%,100%{box-shadow:0 0 4px rgba(42,157,191,.08),0 0 12px rgba(42,157,191,.02)}50%{box-shadow:0 0 10px rgba(42,157,191,.28),0 0 28px rgba(42,157,191,.1)}}
@keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
@keyframes typing{0%,80%,100%{transform:scale(.6)}40%{transform:scale(1)}}
html,body{height:100%;background:var(--deep);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
/* Login */
.login-copyright{color:var(--text-faint);font-size:11px;margin-top:24px}
/* Main layout */
.app{display:none;height:100vh;flex-direction:column}
/* Header */
.header{background:var(--glass);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
.header-left{display:flex;align-items:center;gap:12px}
.header-left .logo{width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,var(--cyan),var(--violet));opacity:.8}
.header-left h2{font-size:16px;font-weight:500}
.header-left .user-badge{background:rgba(42,157,191,.12);color:var(--cyan);padding:2px 10px;border-radius:12px;font-size:12px}
.header-right{display:flex;align-items:center;gap:8px}
.btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:6px;font-size:12px;cursor:pointer;transition:.2s;border:none}
.btn-ghost{background:transparent;color:var(--text-soft);border:1px solid var(--border)}
.btn-ghost:hover{background:rgba(255,255,255,.04);color:var(--text)}
.btn-primary{background:linear-gradient(135deg,var(--cyan),#3ab0c8);color:var(--deep);font-weight:500}
.btn-primary:hover{box-shadow:0 2px 12px rgba(42,157,191,.25)}
.btn-sm{padding:4px 10px;font-size:11px}
/* Content */
.content{flex:1;display:flex;overflow:hidden}
/* Chat area */
.chat-area{flex:1;display:flex;flex-direction:column;min-width:0}
.chat-messages{flex:1;overflow-y:auto;padding:20px 24px}
.chat-messages::-webkit-scrollbar{width:4px}
.chat-messages::-webkit-scrollbar-track{background:transparent}
.chat-messages::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
.msg{max-width:75%;margin-bottom:16px;animation:fadeIn .3s ease;line-height:1.7}
.msg.user{margin-left:auto}
.msg.assistant{margin-right:auto}
.msg.system{max-width:90%;margin:12px auto;text-align:center}
.msg .label{font-size:11px;color:var(--text-soft);margin-bottom:4px;font-weight:500}
.msg.user .label{text-align:right}
.msg .bubble{padding:10px 16px;border-radius:10px;font-size:14px}
.msg.user .bubble{background:linear-gradient(135deg,var(--cyan),#3ab0c8);color:var(--deep);border-bottom-right-radius:4px}
.msg.assistant .bubble{background:var(--card-bg);border:1px solid var(--border);border-bottom-left-radius:4px;color:var(--text)}
.msg.system .bubble{background:rgba(224,74,116,.08);border:1px solid rgba(224,74,116,.15);color:var(--heart);font-size:13px}
.msg .bubble code{background:rgba(255,255,255,.06);padding:1px 5px;border-radius:3px;font-size:13px}
.msg .bubble pre{background:rgba(0,0,0,.25);padding:10px;border-radius:6px;margin:8px 0;overflow-x:auto;font-size:13px}
.msg .bubble strong{color:var(--glow)}
/* Typing indicator */
.typing{display:flex;gap:5px;padding:4px 0}
.typing span{width:7px;height:7px;background:var(--text-soft);border-radius:50%;animation:typing 1.2s infinite ease-in-out}
.typing span:nth-child(2){animation-delay:.2s}
.typing span:nth-child(3){animation-delay:.4s}
/* Chat input */
.chat-input{background:var(--glass);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-top:1px solid var(--border);padding:14px 24px;display:flex;gap:10px;align-items:center;flex-shrink:0}
.chat-input input{flex:1;padding:10px 16px;background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px;outline:none;transition:.2s}
.chat-input input:focus{border-color:var(--cyan)}
.chat-input input::placeholder{color:var(--text-faint)}
/* Sidebar */
.sidebar{width:320px;background:var(--glass);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-left:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}
.sidebar-tabs{display:flex;border-bottom:1px solid var(--border);flex-shrink:0}
.sidebar-tab{padding:10px 14px;font-size:12px;color:var(--text-soft);cursor:pointer;border-bottom:2px solid transparent;transition:.2s;background:none;border-top:none;border-left:none;border-right:none}
.sidebar-tab.active{color:var(--cyan);border-bottom-color:var(--cyan)}
.sidebar-tab:hover{color:var(--text)}
.sidebar-content{flex:1;overflow-y:auto;padding:16px}
.sidebar-content::-webkit-scrollbar{width:4px}
.sidebar-content::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
.sidebar-section{margin-bottom:20px}
.sidebar-section h3{font-size:12px;color:var(--text-soft);font-weight:500;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}
.sidebar-section .desc{font-size:12px;color:var(--text-faint);margin-bottom:8px;line-height:1.6}
.code-block{background:rgba(0,0,0,.3);border:1px solid var(--border);border-radius:6px;padding:10px;font-family:'SFMono-Regular',Consolas,Monaco,monospace;font-size:11px;color:var(--glow);overflow-x:auto;white-space:pre-wrap;word-break:break-all}
.copy-btn{display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--cyan);cursor:pointer;margin-top:6px;background:none;border:none;padding:4px 8px;border-radius:4px}
.copy-btn:hover{background:rgba(42,157,191,.1)}
/* Stats */
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
.stat-card{background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:12px;text-align:center}
.stat-card .num{font-size:18px;font-weight:600;color:var(--glow)}
.stat-card .label{font-size:11px;color:var(--text-soft);margin-top:2px}
/* Samples */
.sample-item{background:var(--card-bg);border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;cursor:pointer;transition:.2s}
.sample-item:hover{border-color:rgba(42,157,191,.2)}
.sample-item .preview{font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:4px}
.sample-item .meta{font-size:11px;color:var(--text-faint)}
.sample-item .tag{display:inline-block;background:rgba(42,157,191,.1);color:var(--cyan);padding:1px 6px;border-radius:4px;font-size:10px;margin-right:4px}
.empty-state{padding:40px 20px;text-align:center;color:var(--text-faint);font-size:13px;line-height:1.8}
/* Progress bar */
.progress-bar{height:3px;background:var(--border);border-radius:2px;overflow:hidden;margin:8px 0}
.progress-bar .fill{height:100%;background:linear-gradient(90deg,var(--cyan),var(--violet));border-radius:2px;transition:width .3s ease;width:0%}
/* Responsive */
@media(max-width:768px){
.sidebar{display:none}
.msg{max-width:90%}
}
</style>
</head>
<body>
<!-- Login -->
<!-- App -->
<div class="app" id="app">
<div class="header">
<div class="header-left">
<div class="logo"></div>
<h2>Corpus Agent</h2>
<span class="user-badge" id="user-badge"></span>
</div>
<div class="header-right">
<span style="font-size:11px;color:var(--text-faint)" id="info-version">v1.2.0</span>
<button class="btn btn-ghost btn-sm" onclick="exportAll()">📥 导出</button>
<button class="btn btn-ghost btn-sm" onclick="doLogout()">退出</button>
</div>
</div>
<div class="content">
<!-- Chat -->
<div class="chat-area">
<div class="chat-messages" id="chat-messages">
<div class="msg system">
<div class="bubble">🧠 语料助手已就绪。告诉我你要整理哪里的对话,我来帮你采集、判断、脱敏、格式化。</div>
</div>
</div>
<div class="chat-input">
<input type="text" id="chat-input" placeholder="告诉我要整理什么对话..." autocomplete="off">
<button class="btn btn-primary" id="send-btn" onclick="sendMessage()">发送</button>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-tabs">
<button class="sidebar-tab active" onclick="switchSidebar('api',this)">API</button>
<button class="sidebar-tab" onclick="switchSidebar('corpus',this)">语料库</button>
</div>
<div class="sidebar-content" id="sidebar-api">
<div class="sidebar-section">
<h3>WorkBuddy MCP</h3>
<div class="desc">在 WorkBuddy 中连接此 MCP Server 即可远程调用语料采集能力</div>
<div class="code-block" id="mcp-config">正在加载...</div>
<button class="copy-btn" onclick="copyText('mcp-config')">📋 复制配置</button>
</div>
<div class="sidebar-section">
<h3>REST API</h3>
<div class="desc">直接调用 API 采集语料</div>
<div class="code-block" id="api-curl">正在加载...</div>
<button class="copy-btn" onclick="copyText('api-curl')">📋 复制命令</button>
</div>
<div class="sidebar-section">
<h3>统计数据</h3>
<div class="stats-grid" id="stats-grid">
<div class="stat-card"><div class="num" id="stat-total">0</div><div class="label">语料总数</div></div>
<div class="stat-card"><div class="num" id="stat-chars">0</div><div class="label">总字符数</div></div>
</div>
</div>
<div class="sidebar-section">
<h3>关于</h3>
<div class="desc">
Corpus Agent 是铸渊的语料采集分身。<br>
采集的对话会经过 AI 判断 → 自动脱敏 → 格式化为 SFT 训练语料。<br><br>
所有采集的语料按代码仓库账号分开存储。<br>
导出格式JSONL (ChatML)<br><br>
需要帮助在对话区输入问题Agent 会引导你完成采集。
</div>
</div>
</div>
<div class="sidebar-content" id="sidebar-corpus" style="display:none">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h3 style="font-size:12px;color:var(--text-soft);font-weight:500">已采集的语料</h3>
<button class="btn btn-ghost btn-sm" onclick="loadSamples()">🔄 刷新</button>
</div>
<div id="sample-list">
<div class="empty-state">还没有语料<br>在对话区告诉 Agent 你想整理什么</div>
</div>
</div>
</div>
</div>
</div>
<script>
let token = localStorage.getItem('corpus_token') || '';
let username = '';
let chatHistory = [];
// ─── 检测首页登录状态 ────────────────────────────────────────────
(function() {
var u = localStorage.getItem('current_user');
if (!u || localStorage.getItem('logged_in') !== 'true') {
window.location.href = '/';
return;
}
username = u;
document.getElementById('user-badge').textContent = username;
fetch('/api/auth/quick-login?username=' + encodeURIComponent(u))
.then(function(r) { return r.json(); })
.then(function(d) { if (d.ok) { token = d.token; localStorage.setItem('corpus_token', token); document.getElementById('app').style.display = 'flex'; updateApiPanel(); loadStats(); loadSamples(); addSystemMsg('✅ 欢迎回来,有什么语料需要整理的?'); } })
.catch(function() {});
})();
function doLogout() {
localStorage.removeItem('corpus_token');
token = ''; username = '';
document.getElementById('app').style.display = 'none';
}
// 自动恢复页面刷新后从localStorage恢复session
if (token) {
(async () => {
try {
const r = await fetch('/api/auth/check?token=' + token);
const d = await r.json();
if (d.ok) {
username = d.username;
document.getElementById('app').style.display = 'flex';
document.getElementById('user-badge').textContent = username;
updateApiPanel();
loadStats();
loadSamples();
} else {
localStorage.removeItem('corpus_token');
// fallback: re-login via homepage
var u = localStorage.getItem('current_user');
if (u) { window.location.href = '/corpus/'; }
}
} catch(e) { localStorage.removeItem('corpus_token'); }
})();
}
// ─── Enter key ──────────────────────────────────────────────────────
document.getElementById('chat-input').addEventListener('keydown', e => { if(e.key==='Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
// ─── Chat ───────────────────────────────────────────────────────────
function addSystemMsg(text) {
const c = document.getElementById('chat-messages');
const d = document.createElement('div'); d.className = 'msg system';
d.innerHTML = '<div class="bubble">' + text + '</div>';
c.appendChild(d); c.scrollTop = c.scrollHeight;
}
function addMsg(role, text, keepHistory) {
const c = document.getElementById('chat-messages');
const d = document.createElement('div'); d.className = 'msg ' + role;
const label = role === 'user' ? '你' : role === 'assistant' ? '助手' : '系统';
let content = escHtml(text).replace(/\n/g,'<br>').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/`([^`]+)`/g,'<code>$1</code>');
d.innerHTML = '<div class="label">' + label + '</div><div class="bubble">' + content + '</div>';
c.appendChild(d); c.scrollTop = c.scrollHeight;
if (keepHistory !== false) {
chatHistory.push({role, content: text});
}
}
function showTyping() {
const c = document.getElementById('chat-messages');
const d = document.createElement('div'); d.className = 'msg assistant'; d.id = 'typing-msg';
d.innerHTML = '<div class="label">助手</div><div class="bubble"><div class="typing"><span></span><span></span><span></span></div></div>';
c.appendChild(d); c.scrollTop = c.scrollHeight;
}
function hideTyping() {
const el = document.getElementById('typing-msg');
if (el) el.remove();
}
async function sendMessage() {
const input = document.getElementById('chat-input');
const msg = input.value.trim();
if (!msg || !token) return;
input.value = '';
addMsg('user', msg);
showTyping();
// Strip typing indicator from history for chat-v2
try {
const r = await fetch('/api/corpus/chat?token=' + token, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
message: msg,
persona: 'zhuyuan',
engine: 'deepseek'
})
});
if (!r.ok) {
hideTyping();
addSystemMsg('❌ 请求失败 (' + r.status + ')');
return;
}
// Read SSE stream
const reader = r.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullText = '';
let streamBubble = null;
while (true) {
const {done, value} = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true});
while (buffer.includes('\n\n')) {
const idx = buffer.indexOf('\n\n');
const event = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
if (!event.startsWith('data: ')) continue;
const dataStr = event.slice(6).trim();
if (dataStr === '[DONE]') continue;
try {
const data = JSON.parse(dataStr);
const type = data.type || '';
if (type === 'persona-loaded') {
hideTyping();
const c = document.getElementById('chat-messages');
const d = document.createElement('div'); d.className = 'msg assistant';
d.innerHTML = '<div class="label">助手</div><div class="bubble" id="stream-bubble"></div>';
c.appendChild(d);
streamBubble = document.getElementById('stream-bubble');
} else if (type === 'token') {
fullText += (data.token || '');
if (!streamBubble) {
hideTyping();
const c = document.getElementById('chat-messages');
const d = document.createElement('div'); d.className = 'msg assistant';
d.innerHTML = '<div class="label">助手</div><div class="bubble" id="stream-bubble"></div>';
c.appendChild(d);
streamBubble = document.getElementById('stream-bubble');
}
streamBubble.innerHTML = escHtml(fullText).replace(/\n/g,'<br>').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\`([^\`]+)\`/g,'<code>$1</code>');
document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;
} else if (type === 'error') {
hideTyping();
addSystemMsg('⚠️ ' + (data.message || '错误'));
}
} catch(e) {}
}
}
hideTyping();
if (fullText && streamBubble) {
streamBubble.innerHTML = escHtml(fullText).replace(/\n/g,'<br>').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\`([^\`]+)\`/g,'<code>$1</code>');
} else if (fullText) {
addMsg('assistant', fullText);
}
} catch(e) {
hideTyping();
addSystemMsg('❌ 连接错误: ' + e.message);
}
}
function scrollToBottom() {
const c = document.getElementById('chat-messages');
c.scrollTop = c.scrollHeight;
}
// ─── Sidebar ────────────────────────────────────────────────────────
function switchSidebar(name, btn) {
document.querySelectorAll('.sidebar-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.sidebar-content').forEach(t => t.style.display = 'none');
if (btn) btn.classList.add('active');
document.getElementById('sidebar-' + name).style.display = 'block';
}
function updateApiPanel() {
document.getElementById('mcp-config').textContent =
`{
"mcpServers": {
"corpus-agent": {
"url": "https://guanghulab.com/mcp/corpus",
"token": "${token.substring(0,8)}..."
}
}
}`;
document.getElementById('api-curl').textContent =
`# 快速采集一段文本
curl -X POST https://guanghulab.com/api/corpus/collect?token=${token.substring(0,8)}... \\
-H 'Content-Type: application/json' \\
-d '{"text":"...","source":"manual"}'
# 查看语料统计
curl -s https://guanghulab.com/api/corpus/stats?token=${token.substring(0,8)}...`;
}
function copyText(id) {
const el = document.getElementById(id);
navigator.clipboard.writeText(el.textContent).then(() => {
const btn = el.nextElementSibling;
const orig = btn.textContent;
btn.textContent = '✅ 已复制';
setTimeout(() => btn.textContent = orig, 1500);
});
}
// ─── Data ───────────────────────────────────────────────────────────
async function loadStats() {
if (!token) return;
try {
const r = await fetch('/api/corpus/stats?token=' + token);
const d = await r.json();
if (d.ok) {
document.getElementById('stat-total').textContent = d.total_samples || 0;
document.getElementById('stat-chars').textContent = (d.total_chars || 0).toLocaleString();
}
} catch(e) {}
}
async function loadSamples() {
if (!token) return;
const el = document.getElementById('sample-list');
try {
const r = await fetch('/api/corpus/list?token=' + token + '&page=1&size=20');
const d = await r.json();
if (!d.ok || !d.samples || d.samples.length === 0) {
el.innerHTML = '<div class="empty-state">还没有语料<br>在对话区告诉 Agent 你想整理什么</div>';
return;
}
let html = '';
for (const s of d.samples) {
const msgs = s.messages || [];
const first = msgs[0]?.content?.slice(0, 60) || '(空)';
const tags = (s.tags || []).map(t => '<span class="tag">' + t + '</span>').join('');
html += '<div class="sample-item"><div class="preview">' + escHtml(first) + '</div><div class="meta">' + tags + ' ' + (s.source || '') + '</div></div>';
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="empty-state">加载失败</div>';
}
}
async function exportAll() {
if (!token) return;
const r = await fetch('/api/corpus/export?token=' + token + '&format=jsonl');
if (r.ok) {
const blob = await r.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'corpus_' + username + '_' + new Date().toISOString().slice(0,10) + '.jsonl';
a.click();
addSystemMsg('✅ 语料已导出:' + a.download);
}
}
// ─── Utilities ──────────────────────────────────────────────────────
function escHtml(s) {
if (typeof s !== 'string') s = String(s || '');
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
</script>
</body>
</html>