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

1475 lines
66 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.0,maximum-scale=5.0,viewport-fit=cover">
<meta name="description" content="光湖智库 · 智能书库 · 团队成员阅读系统">
<meta name="theme-color" content="#050810">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>光湖智库 · 智能书库</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0%25' stop-color='%23a78bfa'/><stop offset='100%25' stop-color='%2322d3ee'/></linearGradient></defs><rect rx='20' width='100' height='100' fill='%23050810'/><text x='50' y='68' text-anchor='middle' font-size='48' font-weight='900' font-family='serif' fill='url(%23g)'>库</text></svg>">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#050810;--panel:rgba(12,18,40,0.94);
--card:rgba(14,20,48,0.6);--card-border:rgba(167,139,250,0.12);
--card-hover:rgba(167,139,250,0.22);
--purple:#a78bfa;--violet:#c4b5fd;--cyan:#22d3ee;--blue:#60a5fa;
--ok:#34d399;--warn:#fbbf24;--err:#f87171;
--text:#eaf0ff;--dim:#94a7d0;--muted:rgba(120,150,200,0.4);
--border:rgba(167,139,250,0.1);
--font:-apple-system,BlinkMacSystemFont,'PingFang SC','Helvetica Neue','Microsoft YaHei',sans-serif;
--mono:'SF Mono','JetBrains Mono','Fira Code',monospace
}
html{font-size:16px;scroll-behavior:smooth}
body{
font-family:var(--font);background:var(--bg);color:var(--text);
min-height:100vh;overflow-x:hidden;line-height:1.7;
-webkit-font-smoothing:antialiased
}
a{color:var(--cyan);text-decoration:none;transition:color .3s}
a:hover{color:var(--purple)}
::-webkit-scrollbar{width:5px}
::-webkit-scrollbar-thumb{background:rgba(167,139,250,0.15);border-radius:3px}
/* ═══ Background ═══ */
.cosmos{position:fixed;inset:0;z-index:0;pointer-events:none}
#starCanvas{position:absolute;inset:0;width:100%;height:100%}
.nebula{position:fixed;border-radius:50%;filter:blur(120px);opacity:.06;pointer-events:none;z-index:0}
.neb-1{width:55vw;height:50vh;background:radial-gradient(circle,var(--purple),transparent);top:-10%;right:-5%;animation:nebDrift 22s ease-in-out infinite alternate}
.neb-2{width:45vw;height:40vh;background:radial-gradient(circle,var(--cyan),transparent);bottom:5%;left:-8%;animation:nebDrift 28s ease-in-out infinite alternate-reverse}
.neb-3{width:35vw;height:30vh;background:radial-gradient(circle,var(--blue),transparent);top:40%;left:30%;animation:nebDrift 32s ease-in-out infinite alternate;opacity:.04}
@keyframes nebDrift{0%{transform:translate(0,0) scale(1)}100%{transform:translate(15px,-10px) scale(1.05)}}
.lake{position:fixed;bottom:0;left:0;right:0;height:18vh;z-index:0;pointer-events:none;
background:linear-gradient(to top,rgba(167,139,250,0.02),transparent);
animation:lakeB 6s ease-in-out infinite alternate}
.lake::before{content:'';position:absolute;bottom:0;left:8%;right:8%;height:1px;
background:linear-gradient(90deg,transparent,rgba(167,139,250,0.12),rgba(34,211,238,0.1),transparent)}
@keyframes lakeB{0%{opacity:.5}100%{opacity:1}}
/* ═══ Particles ═══ */
.particles{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden}
.particle{position:absolute;width:3px;height:3px;border-radius:50%;opacity:0;
animation:particleFloat linear infinite}
@keyframes particleFloat{
0%{opacity:0;transform:translateY(100vh) scale(0)}
10%{opacity:.6}90%{opacity:.6}
100%{opacity:0;transform:translateY(-20vh) scale(1)}
}
/* ═══ Layout ═══ */
.page{position:relative;z-index:1;min-height:100vh;display:flex;flex-direction:column}
/* ═══ Header ═══ */
.header{
padding:16px 28px;display:flex;align-items:center;justify-content:space-between;
border-bottom:1px solid var(--border);backdrop-filter:blur(16px);
background:rgba(5,8,16,0.85);position:sticky;top:0;z-index:50;transition:box-shadow .3s
}
.header.scrolled{box-shadow:0 4px 30px rgba(0,0,0,0.3)}
.header-left{display:flex;align-items:center;gap:12px}
.header-glyph{
width:38px;height:38px;border-radius:10px;
background:linear-gradient(135deg,rgba(167,139,250,0.12),rgba(34,211,238,0.08));
border:1px solid rgba(167,139,250,0.2);
display:flex;align-items:center;justify-content:center;
font-size:17px;font-weight:900;font-family:serif;color:var(--purple);
animation:glyphB 5s ease-in-out infinite;transition:transform .3s
}
.header-glyph:hover{transform:rotate(10deg) scale(1.1)}
@keyframes glyphB{0%,100%{box-shadow:0 0 10px rgba(167,139,250,0.1)}50%{box-shadow:0 0 28px rgba(167,139,250,0.25)}}
.header-title{font-size:16px;font-weight:700;letter-spacing:1px}
.header-sub{font-size:11px;color:var(--dim);display:block;margin-top:2px}
.header-right{display:flex;align-items:center;gap:12px}
.back-btn{
font-size:12px;color:var(--dim);display:flex;align-items:center;gap:5px;
padding:7px 14px;border-radius:8px;border:1px solid var(--border);
background:transparent;cursor:pointer;transition:all .25s;font-family:var(--font)
}
.back-btn:hover{color:var(--cyan);border-color:rgba(34,211,238,0.25);transform:translateY(-1px)}
/* ═══ Content ═══ */
.content{flex:1;padding:32px 28px 48px;max-width:1100px;margin:0 auto;width:100%}
/* ═══ Hero ═══ */
.hero{text-align:center;margin-bottom:40px;animation:heroIn 1s ease-out}
@keyframes heroIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
.hero-icon{
width:72px;height:72px;margin:0 auto 20px;border-radius:18px;
background:linear-gradient(135deg,rgba(167,139,250,0.15),rgba(34,211,238,0.08));
border:1px solid rgba(167,139,250,0.2);
display:flex;align-items:center;justify-content:center;
font-size:32px;font-weight:900;font-family:serif;color:var(--purple);
animation:glyphB 5s ease-in-out infinite;position:relative
}
.hero-icon::after{content:'';position:absolute;inset:-4px;border-radius:22px;
background:linear-gradient(135deg,rgba(167,139,250,0.1),rgba(34,211,238,0.05));
animation:ringPulse 4s ease-in-out infinite;z-index:-1}
@keyframes ringPulse{0%,100%{transform:scale(1);opacity:.5}50%{transform:scale(1.1);opacity:0}}
.hero h1{font-size:26px;font-weight:700;letter-spacing:2px;margin-bottom:8px;
background:linear-gradient(135deg,var(--text),var(--purple));
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.hero p{font-size:14px;color:var(--dim);line-height:1.8}
/* ═══ Status Bar ═══ */
.status-bar{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:32px;animation:fadeUp 1.2s ease-out}
@keyframes fadeUp{from{opacity:0;transform:translateY(15px)}to{opacity:1;transform:translateY(0)}}
.stat-card{
flex:1;min-width:140px;padding:18px 20px;border-radius:14px;
background:var(--card);border:1px solid var(--card-border);
backdrop-filter:blur(8px);transition:all .3s;position:relative;overflow:hidden
}
.stat-card:hover{border-color:var(--card-hover);transform:translateY(-2px)}
.stat-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;
background:linear-gradient(90deg,var(--purple),var(--cyan));opacity:0;transition:opacity .3s}
.stat-card:hover::before{opacity:.5}
.stat-num{font-size:28px;font-weight:700;font-family:var(--mono);color:var(--purple);margin-bottom:2px;transition:transform .3s}
.stat-card:hover .stat-num{transform:scale(1.05)}
.stat-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
/* ═══ Section ═══ */
.section-title{
font-size:12px;font-weight:700;color:var(--dim);text-transform:uppercase;
letter-spacing:1.5px;font-family:var(--mono);margin-bottom:16px;
display:flex;align-items:center;gap:8px
}
.section-title::after{content:'';flex:1;height:1px;background:var(--border)}
/* ═══ Tabs ═══ */
.tab-bar{display:flex;gap:4px;margin-bottom:20px;padding:4px;border-radius:12px;
background:rgba(14,20,48,0.4);border:1px solid var(--border);width:fit-content}
.tab-btn{padding:8px 18px;border-radius:8px;border:none;background:transparent;
color:var(--dim);font-size:13px;font-family:var(--font);cursor:pointer;
transition:all .25s;white-space:nowrap}
.tab-btn:hover{color:var(--text)}
.tab-btn.active{background:rgba(167,139,250,0.12);color:var(--purple);box-shadow:0 2px 8px rgba(167,139,250,0.1)}
/* ═══ Search Bar ═══ */
.search-wrap{margin-bottom:28px;animation:fadeUp 1.4s ease-out}
.search-bar{
display:flex;gap:10px;padding:6px;border-radius:14px;
background:var(--card);border:1px solid var(--card-border);backdrop-filter:blur(8px);
transition:border-color .3s,box-shadow .3s
}
.search-bar:focus-within{border-color:rgba(167,139,250,0.3);box-shadow:0 0 20px rgba(167,139,250,0.06)}
.search-input{
flex:1;background:transparent;border:none;padding:12px 16px;
color:var(--text);font-size:14px;font-family:var(--font);outline:none
}
.search-input::placeholder{color:var(--muted)}
.search-btn{
padding:10px 20px;border:1px solid rgba(167,139,250,0.2);border-radius:10px;
background:rgba(167,139,250,0.08);color:var(--purple);font-size:13px;font-weight:600;
cursor:pointer;transition:all .25s;font-family:var(--font);white-space:nowrap
}
.search-btn:hover{background:rgba(167,139,250,0.15);border-color:rgba(167,139,250,0.35);
transform:translateY(-1px);box-shadow:0 4px 12px rgba(167,139,250,0.1)}
.search-btn:active{transform:translateY(0)}
/* ═══ Search Results ═══ */
.search-results{margin-top:12px;display:none}
.search-result-card{
display:flex;align-items:center;justify-content:space-between;gap:12px;
padding:14px 18px;border-radius:12px;margin-bottom:8px;
background:var(--card);border:1px solid var(--card-border);
transition:all .3s;animation:slideIn .3s ease-out
}
.search-result-card:hover{border-color:var(--card-hover);transform:translateX(4px)}
@keyframes slideIn{from{opacity:0;transform:translateX(-10px)}to{opacity:1;transform:translateX(0)}}
.result-info{flex:1;min-width:0}
.result-title{font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.result-meta{font-size:11px;color:var(--dim);margin-top:2px}
.result-actions{display:flex;gap:6px;flex-shrink:0}
.action-btn{
padding:6px 12px;border-radius:8px;border:1px solid var(--border);
background:transparent;color:var(--dim);font-size:11px;cursor:pointer;
font-family:var(--font);transition:all .25s;white-space:nowrap
}
.action-btn:hover{transform:translateY(-1px)}
.action-btn.read{color:var(--cyan);border-color:rgba(34,211,238,0.25)}
.action-btn.read:hover{background:rgba(34,211,238,0.08)}
.action-btn.download{color:var(--ok);border-color:rgba(52,211,153,0.25)}
.action-btn.download:hover{background:rgba(52,211,153,0.08)}
/* ═══ Download Panel ═══ */
.dl-panel{
margin-top:16px;padding:16px 18px;border-radius:12px;
background:rgba(167,139,250,0.03);border:1px solid var(--card-border);
display:none;animation:fadeUp .4s ease-out
}
.dl-fields{display:flex;gap:10px;flex-wrap:wrap}
.dl-input{
flex:2;min-width:120px;background:rgba(5,8,16,0.4);border:1px solid var(--border);
border-radius:8px;padding:10px 14px;color:var(--text);font-size:13px;
font-family:var(--font);outline:none;transition:border-color .25s
}
.dl-input:focus{border-color:rgba(167,139,250,0.3)}
.dl-input::placeholder{color:var(--muted)}
.dl-select{
flex:1;min-width:110px;background:rgba(5,8,16,0.4);border:1px solid var(--border);
border-radius:8px;padding:10px;color:var(--text);font-size:13px;
font-family:var(--font);outline:none;cursor:pointer
}
.dl-select option{background:#0c1228}
/* ═══ Upload Zone ═══ */
.upload-zone{
margin-bottom:28px;padding:32px 24px;border-radius:14px;
background:var(--card);border:2px dashed var(--card-border);
text-align:center;cursor:pointer;transition:all .35s;position:relative;overflow:hidden
}
.upload-zone:hover,.upload-zone.dragover{
border-color:rgba(167,139,250,0.35);background:rgba(167,139,250,0.03);
transform:translateY(-2px);box-shadow:0 8px 30px rgba(167,139,250,0.06)
}
.upload-zone.dragover{border-color:rgba(34,211,238,0.4)}
.upload-icon{font-size:36px;margin-bottom:12px;opacity:.6;transition:transform .3s}
.upload-zone:hover .upload-icon{transform:translateY(-4px) scale(1.1)}
.upload-title{font-size:15px;font-weight:600;color:var(--text);margin-bottom:6px}
.upload-desc{font-size:12px;color:var(--dim);line-height:1.8}
.upload-formats{font-size:10px;color:var(--muted);margin-top:8px;font-family:var(--mono)}
.upload-progress{display:none;margin-top:12px}
.upload-bar{width:100%;height:4px;border-radius:2px;background:var(--border);overflow:hidden}
.upload-bar-fill{height:100%;border-radius:2px;background:linear-gradient(90deg,var(--purple),var(--cyan));
transition:width .3s ease-out;width:0}
.upload-status{font-size:11px;color:var(--dim);margin-top:6px}
input[type="file"]{display:none}
/* ═══ Book Grid ═══ */
.book-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px;margin-bottom:32px}
.book-card{
background:var(--card);border:1px solid var(--card-border);border-radius:14px;
padding:20px 22px;backdrop-filter:blur(8px);transition:all .35s;cursor:pointer;
position:relative;overflow:hidden
}
.book-card:hover{border-color:var(--card-hover);box-shadow:0 8px 32px rgba(167,139,250,0.06);transform:translateY(-2px)}
.book-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;
background:linear-gradient(90deg,var(--purple),var(--cyan));opacity:0;transition:opacity .3s}
.book-card:hover::before{opacity:.4}
.book-title{font-size:15px;font-weight:700;color:var(--text);margin-bottom:4px;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.book-author{font-size:12px;color:var(--dim);margin-bottom:8px}
.book-meta{display:flex;gap:6px;flex-wrap:wrap}
.book-tag{font-size:10px;padding:2px 8px;border-radius:6px;font-family:var(--mono);
background:rgba(167,139,250,0.08);color:var(--purple);border:1px solid rgba(167,139,250,0.15)}
.book-actions-hover{display:none;position:absolute;bottom:0;left:0;right:0;padding:12px 16px;
background:linear-gradient(to top,rgba(5,8,16,0.95),transparent);gap:8px;justify-content:center}
.book-card:hover .book-actions-hover{display:flex}
/* ═══ Module Cards ═══ */
.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin-bottom:32px}
.mod-card{
background:var(--card);border:1px solid var(--card-border);border-radius:14px;
padding:22px 24px;backdrop-filter:blur(8px);transition:all .35s;
position:relative;overflow:hidden;cursor:default
}
.mod-card:hover{border-color:var(--card-hover);box-shadow:0 8px 40px rgba(167,139,250,0.06);transform:translateY(-3px)}
.mod-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;
background:linear-gradient(90deg,var(--purple),var(--cyan));opacity:.2;transition:opacity .3s}
.mod-card:hover::before{opacity:.5}
.mod-icon{font-size:22px;margin-bottom:10px}
.mod-name{font-size:15px;font-weight:700;color:var(--text);margin-bottom:6px}
.mod-desc{font-size:13px;color:var(--dim);line-height:1.7;margin-bottom:12px}
.mod-status{font-size:11px;font-family:var(--mono);display:flex;align-items:center;gap:6px}
.mod-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.mod-dot.ok{background:var(--ok);box-shadow:0 0 6px rgba(52,211,153,0.4)}
.mod-dot.err{background:var(--err);box-shadow:0 0 6px rgba(248,113,113,0.4)}
.mod-dot.building{background:var(--warn);box-shadow:0 0 6px rgba(251,191,36,0.4);animation:pulse 1.5s infinite}
.mod-dot.plan{background:var(--muted)}
@keyframes pulse{0%,100%{opacity:.6}50%{opacity:1}}
/* ═══ Agent Chat ═══ */
.agent-chat{
background:var(--card);border:1px solid var(--card-border);border-radius:14px;
padding:0;backdrop-filter:blur(8px);overflow:hidden;transition:all .3s;margin-bottom:28px
}
.agent-header{
padding:14px 20px;border-bottom:1px solid var(--border);
display:flex;align-items:center;justify-content:space-between;background:rgba(167,139,250,0.03)
}
.agent-header-left{display:flex;align-items:center;gap:10px}
.agent-avatar{width:32px;height:32px;border-radius:10px;
background:linear-gradient(135deg,rgba(167,139,250,0.2),rgba(34,211,238,0.1));
display:flex;align-items:center;justify-content:center;font-size:16px;
animation:agentPulse 3s ease-in-out infinite}
@keyframes agentPulse{0%,100%{box-shadow:0 0 8px rgba(167,139,250,0.15)}50%{box-shadow:0 0 16px rgba(167,139,250,0.3)}}
.agent-name{font-size:13px;font-weight:700;color:var(--purple)}
.agent-status{font-size:10px;color:var(--ok);display:flex;align-items:center;gap:4px}
.agent-status-dot{width:5px;height:5px;border-radius:50%;background:var(--ok);animation:pulse 2s infinite}
.agent-model-badge{font-size:10px;padding:2px 8px;border-radius:6px;
background:rgba(34,211,238,0.08);color:var(--cyan);border:1px solid rgba(34,211,238,0.15);font-family:var(--mono)}
.chat-body{height:300px;overflow-y:auto;padding:16px 20px;scroll-behavior:smooth}
.chat-msg{margin-bottom:12px;animation:msgIn .3s ease-out}
@keyframes msgIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.chat-msg-user{display:flex;justify-content:flex-end}
.chat-msg-agent{display:flex;justify-content:flex-start}
.chat-bubble{max-width:80%;padding:10px 14px;border-radius:12px;font-size:13px;line-height:1.8;
white-space:pre-wrap;word-break:break-word}
.chat-bubble-user{background:rgba(167,139,250,0.12);border:1px solid rgba(167,139,250,0.15);
color:var(--text);border-bottom-right-radius:4px}
.chat-bubble-agent{background:rgba(34,211,238,0.06);border:1px solid rgba(34,211,238,0.1);
color:var(--text);border-bottom-left-radius:4px}
.chat-bubble-agent .model-tag{font-size:9px;color:var(--muted);font-family:var(--mono);display:block;margin-top:4px}
.chat-input-wrap{padding:12px 16px;border-top:1px solid var(--border);display:flex;gap:8px;align-items:center}
.chat-input{
flex:1;background:rgba(5,8,16,0.4);border:1px solid var(--border);
border-radius:10px;padding:10px 14px;color:var(--text);font-size:13px;
font-family:var(--font);outline:none;resize:none;min-height:40px;max-height:100px;transition:border-color .25s
}
.chat-input:focus{border-color:rgba(167,139,250,0.3)}
.chat-input::placeholder{color:var(--muted)}
.chat-send-btn{
width:40px;height:40px;border-radius:10px;border:none;
background:linear-gradient(135deg,rgba(167,139,250,0.15),rgba(34,211,238,0.1));
color:var(--purple);cursor:pointer;display:flex;align-items:center;justify-content:center;
transition:all .25s;flex-shrink:0
}
.chat-send-btn:hover{background:linear-gradient(135deg,rgba(167,139,250,0.25),rgba(34,211,238,0.15));transform:scale(1.05)}
.chat-send-btn:disabled{opacity:.3;cursor:not-allowed;transform:none}
.chat-quick-actions{padding:8px 16px 12px;display:flex;gap:6px;flex-wrap:wrap}
.quick-btn{
font-size:10px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);
background:transparent;color:var(--dim);cursor:pointer;font-family:var(--font);transition:all .2s;white-space:nowrap
}
.quick-btn:hover{color:var(--cyan);border-color:rgba(34,211,238,0.25);background:rgba(34,211,238,0.03)}
/* ═══ Typing Indicator ═══ */
.typing-indicator{display:flex;gap:4px;padding:4px 0}
.typing-dot{width:6px;height:6px;border-radius:50%;background:var(--dim);animation:typingBounce 1.4s infinite both}
.typing-dot:nth-child(2){animation-delay:.2s}
.typing-dot:nth-child(3){animation-delay:.4s}
@keyframes typingBounce{0%,80%,100%{transform:scale(0);opacity:.3}40%{transform:scale(1);opacity:1}}
/* ═══ Empty State ═══ */
.empty-state{text-align:center;padding:60px 24px;animation:fadeUp 1.6s ease-out}
.empty-icon{font-size:48px;margin-bottom:16px;opacity:.5}
.empty-title{font-size:18px;font-weight:600;color:var(--dim);margin-bottom:8px}
.empty-desc{font-size:13px;color:var(--muted);line-height:1.8;max-width:400px;margin:0 auto}
/* ═══ Reader Modal ═══ */
.reader-modal{display:none;position:fixed;inset:0;z-index:100;background:var(--bg);flex-direction:column;animation:readerIn .3s ease-out}
@keyframes readerIn{from{opacity:0}to{opacity:1}}
.reader-header{
display:flex;align-items:center;justify-content:space-between;padding:14px 20px;
border-bottom:1px solid var(--border);background:rgba(5,8,16,0.95);backdrop-filter:blur(12px);flex-shrink:0
}
.reader-body{flex:1;overflow-y:auto;padding:24px 20px 80px;max-width:720px;margin:0 auto;width:100%;font-size:16px;line-height:1.9;color:var(--text)}
.reader-nav{
position:fixed;bottom:0;left:0;right:0;display:flex;justify-content:space-between;
padding:12px 20px;background:rgba(5,8,16,0.95);border-top:1px solid var(--border);backdrop-filter:blur(12px)
}
/* ═══ Footer ═══ */
.footer{
text-align:center;padding:32px 24px;font-size:11px;color:var(--muted);
border-top:1px solid var(--border);letter-spacing:.5px
}
.footer-line{width:100px;height:1px;margin:0 auto 12px;
background:linear-gradient(90deg,transparent,rgba(167,139,250,0.12),transparent)}
/* ═══ Responsive ═══ */
@media(max-width:600px){
.content{padding:20px 16px 36px}
.hero h1{font-size:22px}
.module-grid,.book-grid{grid-template-columns:1fr}
.status-bar{flex-direction:column}
.header{padding:12px 16px}
.tab-bar{width:100%;overflow-x:auto}
.search-result-card{flex-direction:column;align-items:flex-start;gap:8px}
.result-actions{width:100%;justify-content:flex-end}
.chat-body{height:240px}
.dl-fields{flex-direction:column}
}
/* ═══ Toast ═══ */
.toast{
position:fixed;bottom:32px;left:50%;transform:translateX(-50%) translateY(100px);
padding:14px 24px;border-radius:12px;background:rgba(14,20,48,0.95);
border:1px solid rgba(167,139,250,0.2);color:var(--text);font-size:13px;
z-index:9999;backdrop-filter:blur(12px);max-width:90%;text-align:center;
opacity:0;transition:all .4s cubic-bezier(.175,.885,.32,1.275)
}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
@media(prefers-reduced-motion:reduce){
*,*::before,*::after{animation-duration:0.01ms!important;transition-duration:0.01ms!important}
}
</style>
</head>
<body>
<div class="cosmos">
<canvas id="starCanvas"></canvas>
<div class="nebula neb-1"></div>
<div class="nebula neb-2"></div>
<div class="nebula neb-3"></div>
</div>
<div class="lake"></div>
<div class="particles" id="particlesContainer"></div>
<div class="page">
<!-- Header -->
<header class="header" id="mainHeader">
<div class="header-left">
<div class="header-glyph"></div>
<div>
<span class="header-title">光湖智库</span>
<span class="header-sub">智能书库 · 内部成员系统</span>
</div>
</div>
<div class="header-right">
<button class="back-btn" onclick="window.location.href='/'">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
返回首页
</button>
</div>
</header>
<main class="content">
<!-- Hero -->
<div class="hero">
<div class="hero-icon"></div>
<h1>光湖智库 · 智能书库</h1>
<p>
光湖语言世界 · 团队内部智能阅读系统<br>
书库搜索 · 智能分章 · 在线阅读 · AI辅助 · 文稿上传
</p>
</div>
<!-- Status -->
<div class="status-bar" id="statusBar">
<div class="stat-card">
<div class="stat-num" id="statBooks"></div>
<div class="stat-label">书库总量</div>
</div>
<div class="stat-card">
<div class="stat-num" id="statMembers"></div>
<div class="stat-label">团队成员</div>
</div>
<div class="stat-card">
<div class="stat-num" id="statToday"></div>
<div class="stat-label">今日活跃</div>
</div>
<div class="stat-card">
<div class="stat-num" id="statChapters"></div>
<div class="stat-label">已分章书籍</div>
</div>
</div>
<!-- ═══ 搜索书 (统一入口) ═══ -->
<div class="section-title">🔍 搜索书</div>
<div class="search-wrap">
<div class="search-bar" id="mainSearchBar">
<input class="search-input" id="searchInput" type="text" placeholder="输入书名、作者搜索书库...">
<button class="search-btn" onclick="searchBooks()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle;margin-right:4px"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
搜索
</button>
</div>
<!-- Search Results -->
<div class="search-results" id="searchResults"></div>
<!-- Download toggle -->
<div style="margin-top:12px;text-align:center">
<button class="quick-btn" onclick="toggleDownloadPanel()" style="font-size:11px">📥 从平台下载新书</button>
</div>
<div class="dl-panel" id="downloadPanel">
<div style="font-size:11px;color:var(--dim);margin-bottom:8px;line-height:1.6">
💡 系统会先匹配 <span id="dlSourceCount" style="color:var(--cyan)">--</span> 个开源书源数据库,找到则直接使用;未找到才从选定平台下载。
</div>
<div class="dl-fields">
<input class="dl-input" id="dlBookName" type="text" placeholder="书名" style="flex:2;min-width:120px">
<input class="dl-input" id="dlAuthor" type="text" placeholder="作者(选填)" style="flex:1;min-width:100px">
<select class="dl-select" id="dlPlatform">
<option value="qimao">七猫</option>
<option value="fanqie">番茄</option>
<option value="yuewen">阅文</option>
<option value="jjwxc">晋江</option>
</select>
<button class="search-btn" onclick="downloadBook()">搜索并下载</button>
</div>
</div>
</div>
<!-- ═══ 上传文稿 ═══ -->
<div class="section-title">📤 上传文稿</div>
<div class="upload-zone" id="uploadZone" onclick="document.getElementById('fileInput').click()">
<input type="file" id="fileInput" accept=".txt,.epub,.html,.htm,.md,.docx">
<div class="upload-icon">📄</div>
<div class="upload-title">点击选择文件 或 拖拽文件到这里</div>
<div class="upload-desc">支持上传小说文稿,系统自动检测格式并转换<br>可在线阅读、自动分章整理</div>
<div class="upload-formats">支持格式: TXT · EPUB · HTML · Markdown · DOCX | 最大 50MB</div>
<div class="upload-progress" id="uploadProgress">
<div class="upload-bar"><div class="upload-bar-fill" id="uploadBarFill"></div></div>
<div class="upload-status" id="uploadStatus">准备上传...</div>
</div>
</div>
<!-- ═══ 书库 ═══ -->
<div class="section-title">📚 书库</div>
<div class="tab-bar">
<button class="tab-btn active" onclick="switchBookTab('all',this)">全部</button>
<button class="tab-btn" onclick="switchBookTab('downloaded',this)">已下载</button>
<button class="tab-btn" onclick="switchBookTab('uploaded',this)">已上传</button>
</div>
<div id="bookArea">
<div class="empty-state" id="emptyState">
<div class="empty-icon">📚</div>
<div class="empty-title">书库为空</div>
<div class="empty-desc">
搜索并下载书籍,或上传你的小说文稿。<br>
系统会自动分章,支持在线阅读。
</div>
</div>
<div class="book-grid" id="bookGrid" style="display:none"></div>
</div>
<!-- ═══ Reader Modal ═══ -->
<div class="reader-modal" id="readerModal">
<div class="reader-header">
<div style="display:flex;align-items:center;gap:10px">
<button class="back-btn" onclick="closeReader()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
返回
</button>
<span id="readerTitle" style="font-size:14px;font-weight:600;color:var(--text)"></span>
</div>
<div style="display:flex;gap:8px;align-items:center">
<button class="back-btn" onclick="changeFontSize(-1)" title="减小字号">A-</button>
<button class="back-btn" onclick="changeFontSize(1)" title="增大字号">A+</button>
<button class="back-btn" id="themeToggle" onclick="toggleReaderTheme()" title="切换主题">☀️</button>
</div>
</div>
<div class="reader-body" id="readerBody"></div>
<div class="reader-nav">
<button class="search-btn" id="prevChBtn" onclick="prevChapter()">上一章</button>
<span id="chapterInfo" style="font-size:12px;color:var(--dim);align-self:center;font-family:var(--mono)"></span>
<button class="search-btn" id="nextChBtn" onclick="nextChapter()">下一章</button>
</div>
</div>
<!-- ═══ 团队书库 ═══ -->
<div class="section-title" style="margin-top:20px">📦 团队书库</div>
<div id="librarySection" style="margin-bottom:28px">
<div class="tab-bar" style="margin-bottom:12px">
<button class="tab-btn active" onclick="loadLibrary(null,this)">全部</button>
<button class="tab-btn" onclick="loadLibrary('男频',this)">男频</button>
<button class="tab-btn" onclick="loadLibrary('女频',this)">女频</button>
<button class="tab-btn" onclick="loadLibrary('无CP',this)">无CP</button>
<button class="tab-btn" onclick="loadLibrary('百合',this)">百合</button>
<button class="tab-btn" onclick="loadLibrary('耽美',this)">耽美</button>
<button class="tab-btn" onclick="loadLibrary('其他',this)">其他</button>
</div>
<div class="book-grid" id="libraryGrid"></div>
<div id="libraryEmpty" class="empty-state" style="padding:30px">
<div class="empty-icon">📦</div>
<div class="empty-title">团队书库为空</div>
<div class="empty-desc">下载的书籍会自动进入团队书库,也可以手动添加。</div>
</div>
</div>
<!-- ═══ AI Agent 对话 ═══ -->
<div class="section-title">🤖 AI 对话</div>
<div class="agent-chat" id="agentSection">
<div class="agent-header">
<div class="agent-header-left">
<div class="agent-avatar">🤖</div>
<div>
<div class="agent-name">智库 Agent</div>
<div class="agent-status" id="agentStatusWrap"><span class="agent-status-dot" id="agentStatusDot" style="background:var(--warn);animation:none"></span> <span id="agentStatusText">连接中...</span></div>
</div>
</div>
<div id="agentModelBadge" class="agent-model-badge" style="display:none">AI</div>
</div>
<div class="chat-body" id="agentChatBox"></div>
<div class="chat-input-wrap">
<textarea class="chat-input" id="agentInput" placeholder="和AI对话...(支持 DeepSeek / Kimi / 通义千问)" rows="1"></textarea>
<button class="chat-send-btn" id="agentSendBtn" onclick="sendAgentChat()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m22 2-7 20-4-9-9-4z"/><path d="m22 2-10 10"/></svg>
</button>
</div>
<div class="chat-quick-actions">
<button class="quick-btn" onclick="sendAgentChat('帮助')">💡 帮助</button>
<button class="quick-btn" onclick="sendAgentChat('记忆')">🧠 我的记忆</button>
<button class="quick-btn" onclick="sendAgentChat('帮我分析一下最近在读的书')">📖 阅读分析</button>
<button class="quick-btn" onclick="sendAgentChat('给我推荐一些好书')">📚 推荐书籍</button>
<button class="quick-btn" onclick="window.open('/novel.html','_blank')">✍️ 打开创作台</button>
</div>
</div>
<!-- ═══ 系统模块 ═══ -->
<div class="section-title" style="margin-top:20px">🛠️ 系统模块</div>
<div class="module-grid">
<div class="mod-card">
<div class="mod-icon">🛡️</div>
<div class="mod-name">语言保护罩</div>
<div class="mod-desc">Nginx蜜罐(L2) + fail2ban自动封禁(L3)。服务器永远不暴露真实IP。</div>
<div class="mod-status" id="modShield"><span class="mod-dot" id="modShieldDot"></span><span id="modShieldText" style="color:var(--dim)">检测中...</span></div>
</div>
<div class="mod-card">
<div class="mod-icon">📥</div>
<div class="mod-name">下载引擎</div>
<div class="mod-desc">整合开源数据库 + 七猫/番茄/阅文/晋江。优先匹配已有资源。</div>
<div class="mod-status" id="modDownload"><span class="mod-dot" id="modDownloadDot"></span><span id="modDownloadText" style="color:var(--dim)">检测中...</span></div>
</div>
<div class="mod-card">
<div class="mod-icon">✂️</div>
<div class="mod-name">智能分章 Agent</div>
<div class="mod-desc">AI识别章节规律无章节标识时按内容结构分段。</div>
<div class="mod-status" id="modChapter"><span class="mod-dot" id="modChapterDot"></span><span id="modChapterText" style="color:var(--dim)">检测中...</span></div>
</div>
<div class="mod-card">
<div class="mod-icon">📖</div>
<div class="mod-name">在线阅读器</div>
<div class="mod-desc">自动格式检测。翻页、字体、深/亮色。进度自动存档。</div>
<div class="mod-status" id="modReader"><span class="mod-dot" id="modReaderDot"></span><span id="modReaderText" style="color:var(--dim)">检测中...</span></div>
</div>
<div class="mod-card">
<div class="mod-icon">📤</div>
<div class="mod-name">文稿上传</div>
<div class="mod-desc">支持 TXT/EPUB/HTML/MD/DOCX自动格式检测转换拖拽上传。</div>
<div class="mod-status" id="modUpload"><span class="mod-dot" id="modUploadDot"></span><span id="modUploadText" style="color:var(--dim)">检测中...</span></div>
</div>
<div class="mod-card">
<div class="mod-icon">🤖</div>
<div class="mod-name">AI 对话引擎</div>
<div class="mod-desc">接入 DeepSeek/Kimi/通义千问 + 第三方 API。自动降级多模型备用。</div>
<div class="mod-status" id="modAI"><span class="mod-dot" id="modAIDot"></span><span id="modAIText" style="color:var(--dim)">检测中...</span></div>
</div>
<div class="mod-card">
<div class="mod-icon">🔗</div>
<div class="mod-name">开源书源</div>
<div class="mod-desc">已注册外部开源小说下载器数据库,优先匹配已有书源。</div>
<div class="mod-status" id="modSources"><span class="mod-dot" id="modSourcesDot"></span><span id="modSourcesText" style="color:var(--dim)">检测中...</span></div>
</div>
<div class="mod-card">
<div class="mod-icon">✍️</div>
<div class="mod-name">智能小说系统</div>
<div class="mod-desc">小说创作工作台。大纲/人物卡/章节管理。AI续写/改写。</div>
<div class="mod-status" id="modNovel"><span class="mod-dot" id="modNovelDot"></span><span id="modNovelText" style="color:var(--dim)">检测中...</span></div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="footer">
<div class="footer-line"></div>
光湖智库 · 光湖语言世界 · 内部成员系统<br>
版权所有 © 冰朔 · TCS-0002∞ · 国作登字-2026-A-00037559
</footer>
</div>
<script>
/* ═══ API Base ═══ */
var API = (location.hostname === 'guanghuyaoming.com')
? ''
: (location.hostname === 'novel.guanghuyaoming.com')
? ''
: 'http://127.0.0.1:4000';
var MEMBER_ID = 'member-default';
var currentBookTab = 'all';
var systemStatus = { api: false, ai: false, agent: false, sources: 0 };
/* ═══ Star Field ═══ */
(function(){
var c=document.getElementById('starCanvas');
if(!c)return;
var ctx=c.getContext('2d'),stars=[],w,h;
function resize(){w=c.width=window.innerWidth;h=c.height=window.innerHeight;initS();}
function initS(){
stars=[];
var n=Math.min(Math.floor(w*h/4000),200);
for(var i=0;i<n;i++)stars.push({x:Math.random()*w,y:Math.random()*h,r:Math.random()*1.1+0.2,a:Math.random()*0.5+0.2,speed:Math.random()*0.0008+0.0002,phase:Math.random()*Math.PI*2});
}
function draw(t){
ctx.clearRect(0,0,w,h);
for(var i=0;i<stars.length;i++){var s=stars[i];var alpha=s.a*(0.5+0.5*Math.sin(t*s.speed+s.phase));ctx.beginPath();ctx.arc(s.x,s.y,s.r,0,Math.PI*2);ctx.fillStyle='rgba(200,210,240,'+alpha+')';ctx.fill();}
requestAnimationFrame(draw);
}
window.addEventListener('resize',resize);resize();requestAnimationFrame(draw);
})();
/* ═══ Floating Particles ═══ */
(function(){
var container=document.getElementById('particlesContainer');
if(!container)return;
var colors=['rgba(167,139,250,0.4)','rgba(34,211,238,0.3)','rgba(96,165,250,0.3)'];
for(var i=0;i<15;i++){
var p=document.createElement('div');
p.className='particle';
p.style.left=Math.random()*100+'%';
p.style.width=p.style.height=(Math.random()*3+1)+'px';
p.style.background=colors[Math.floor(Math.random()*colors.length)];
p.style.animationDuration=(Math.random()*12+8)+'s';
p.style.animationDelay=(Math.random()*10)+'s';
container.appendChild(p);
}
})();
/* ═══ Header scroll effect ═══ */
window.addEventListener('scroll',function(){
var h=document.getElementById('mainHeader');
if(h)h.classList.toggle('scrolled',window.scrollY>20);
});
/* ═══ Toast ═══ */
function showToast(msg,type){
var existing=document.querySelector('.toast');
if(existing)existing.remove();
var t=document.createElement('div');
t.className='toast';
var borderColor=type==='error'?'rgba(248,113,113,0.3)':type==='ok'?'rgba(52,211,153,0.3)':'rgba(167,139,250,0.2)';
t.style.borderColor=borderColor;
t.textContent=msg;
document.body.appendChild(t);
requestAnimationFrame(function(){requestAnimationFrame(function(){t.classList.add('show');});});
setTimeout(function(){t.classList.remove('show');setTimeout(function(){t.remove()},400)},3000);
}
/* ═══ Fetch Helper ═══ */
function api(method,path,body){
var opts={method:method,headers:{'Content-Type':'application/json'}};
if(body)opts.body=JSON.stringify(body);
return fetch(API+path,opts).then(function(r){
if(!r.ok&&r.status===502)return{error:true,message:'智库节点服务未响应(502),请确认 novel-db 进程已启动'};
if(!r.ok&&r.status===504)return{error:true,message:'智库节点连接超时(504),服务器间网络可能不通'};
return r.json();
}).catch(function(e){return{error:true,message:'网络错误: '+(e.message||'无法连接到服务器')}});
}
function escHtml(s){return s?s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/'/g,'&#x27;').replace(/"/g,'&quot;'):'';}
/* ═══ Load Stats ═══ */
function loadStats(){
api('GET','/api/zhiku/stats').then(function(d){
if(d.error){
// API不可达
updateSystemHealth(false, null);
return;
}
document.getElementById('statBooks').textContent=(d.download?d.download.total_books:0)+(d.upload?d.upload.total_uploads:0);
document.getElementById('statMembers').textContent=d.reader?d.reader.total_members:0;
document.getElementById('statToday').textContent=d.download?d.download.completed_tasks:0;
document.getElementById('statChapters').textContent=d.chapter?d.chapter.total_chaptered_books:0;
if(d.ai&&d.ai.available_models&&d.ai.available_models.length>0){
var badge=document.getElementById('agentModelBadge');
badge.textContent=d.ai.available_models[0].name;
badge.style.display='';
}
updateSystemHealth(true, d);
});
}
/* ═══ System Health Check ═══ */
function setModStatus(id, ok, text) {
var dot = document.getElementById(id + 'Dot');
var txt = document.getElementById(id + 'Text');
if (dot) {
dot.className = 'mod-dot ' + (ok === true ? 'ok' : ok === false ? 'err' : '');
dot.style.background = ok === true ? 'var(--ok)' : ok === false ? 'var(--err)' : 'var(--warn)';
}
if (txt) {
txt.style.color = ok === true ? 'var(--ok)' : ok === false ? 'var(--err)' : 'var(--warn)';
txt.textContent = text;
}
}
function updateSystemHealth(apiOk, statsData) {
systemStatus.api = apiOk;
if (!apiOk) {
// API不可达 - 全部标记为离线
setModStatus('modShield', false, '无法连接');
setModStatus('modDownload', false, '无法连接');
setModStatus('modChapter', false, '无法连接');
setModStatus('modReader', false, '无法连接');
setModStatus('modUpload', false, '无法连接');
setModStatus('modAI', false, '无法连接');
setModStatus('modSources', false, '无法连接');
setModStatus('modNovel', false, '无法连接');
updateAgentStatus(false, 'API离线');
return;
}
// API可达 — 根据各模块统计更新状态
setModStatus('modShield', true, '已激活');
// 下载引擎
if (statsData && statsData.download) {
var dl = statsData.download;
var srcCount = dl.registered_sources || 0;
setModStatus('modDownload', true, '在线 · ' + dl.total_books + ' 本');
setModStatus('modSources', true, srcCount + ' 个数据源 · 索引 ' + (dl.local_index_count || 0));
systemStatus.sources = srcCount;
// 更新下载面板的数据源数量显示
var dlSrcEl = document.getElementById('dlSourceCount');
if (dlSrcEl) dlSrcEl.textContent = srcCount;
} else {
setModStatus('modDownload', null, '未知');
setModStatus('modSources', null, '未知');
}
// 分章引擎
if (statsData && statsData.chapter) {
setModStatus('modChapter', true, '在线 · ' + statsData.chapter.total_chaptered_books + ' 已分章');
} else {
setModStatus('modChapter', null, '未知');
}
// 阅读器
if (statsData && statsData.reader) {
setModStatus('modReader', true, '在线 · ' + statsData.reader.total_members + ' 成员');
} else {
setModStatus('modReader', null, '未知');
}
// 上传
if (statsData && statsData.upload) {
setModStatus('modUpload', true, '在线 · ' + statsData.upload.total_uploads + ' 已上传');
} else {
setModStatus('modUpload', null, '未知');
}
// AI引擎
if (statsData && statsData.ai) {
var aiModels = statsData.ai.available_models || [];
systemStatus.ai = aiModels.length > 0;
if (aiModels.length > 0) {
setModStatus('modAI', true, '在线 · ' + aiModels.map(function(m){return m.name}).join(', '));
} else {
setModStatus('modAI', false, '无可用模型 · 需配置 API Key');
}
} else {
setModStatus('modAI', null, '未知');
systemStatus.ai = false;
}
// 小说系统
if (statsData && statsData.novel) {
setModStatus('modNovel', true, '在线 · ' + statsData.novel.total_novels + ' 部作品');
} else {
setModStatus('modNovel', null, '未知');
}
// Agent状态
if (systemStatus.api) {
if (systemStatus.ai) {
updateAgentStatus(true, 'AI在线');
} else {
updateAgentStatus('partial', '本地模式无AI');
}
}
}
function updateAgentStatus(ok, text) {
var dot = document.getElementById('agentStatusDot');
var txt = document.getElementById('agentStatusText');
systemStatus.agent = ok;
if (dot) {
if (ok === true) {
dot.style.background = 'var(--ok)';
dot.style.animation = 'pulse 2s infinite';
} else if (ok === 'partial') {
dot.style.background = 'var(--warn)';
dot.style.animation = 'pulse 3s infinite';
} else {
dot.style.background = 'var(--err)';
dot.style.animation = 'none';
}
}
if (txt) txt.textContent = text || (ok ? '在线' : '离线');
}
/* ═══ Load Books ═══ */
var allDownloadedBooks=[];
var allUploadedBooks=[];
function loadBooks(){
api('GET','/api/zhiku/download/books').then(function(d){
allDownloadedBooks=(d.data||[]);
renderBookGrid();
});
api('GET','/api/zhiku/upload/list').then(function(d){
allUploadedBooks=(d.data||[]);
renderBookGrid();
});
}
function renderBookGrid(){
var books=[];
if(currentBookTab==='all'||currentBookTab==='downloaded'){
allDownloadedBooks.forEach(function(b){
books.push({type:'downloaded',book_name:b.book_name,author:b.author,platform:b.platform,
size:b.size,filename:b.filename,tags:[]});
});
}
if(currentBookTab==='all'||currentBookTab==='uploaded'){
allUploadedBooks.forEach(function(b){
books.push({type:'uploaded',book_name:b.title,author:b.author,platform:'upload',
size:0,filename:b.converted_file,tags:[b.original_format],word_count:b.word_count});
});
}
var grid=document.getElementById('bookGrid');
var empty=document.getElementById('emptyState');
if(books.length===0){
empty.style.display='';grid.style.display='none';return;
}
empty.style.display='none';grid.style.display='';
grid.innerHTML='';
var platformNames={qimao:'七猫',fanqie:'番茄',yuewen:'阅文',jjwxc:'晋江',upload:'上传'};
books.forEach(function(book,idx){
var card=document.createElement('div');
card.className='book-card';
card.style.animationDelay=(idx*0.05)+'s';
card.onclick=function(){openBook(book.filename)};
var sizeStr=book.size?(book.size/1024).toFixed(1)+' KB':'';
var wordStr=book.word_count?(book.word_count/10000).toFixed(1)+'万字':'';
var metaStr=sizeStr||wordStr;
var tagsHtml='<span class="book-tag">'+(platformNames[book.platform]||book.platform)+'</span>';
if(metaStr)tagsHtml+='<span class="book-tag">'+escHtml(metaStr)+'</span>';
if(book.type==='uploaded')tagsHtml+='<span class="book-tag" style="color:var(--cyan);border-color:rgba(34,211,238,0.15)">已上传</span>';
(book.tags||[]).forEach(function(t){tagsHtml+='<span class="book-tag">'+escHtml(t)+'</span>';});
card.innerHTML='<div class="book-title">'+escHtml(book.book_name)+'</div>'
+'<div class="book-author">'+escHtml(book.author)+'</div>'
+'<div class="book-meta">'+tagsHtml+'</div>'
+'<div class="book-actions-hover">'
+'<button class="action-btn read" data-filename="'+escHtml(book.filename)+'">📖 阅读</button>'
+'</div>';
card.querySelector('.action-btn.read').addEventListener('click',function(e){e.stopPropagation();openBook(book.filename);});
grid.appendChild(card);
});
}
function switchBookTab(tab,btn){
currentBookTab=tab;
var btns=btn.parentElement.querySelectorAll('.tab-btn');
btns.forEach(function(b){b.classList.remove('active')});
btn.classList.add('active');
renderBookGrid();
}
/* ═══ Search (统一搜索) ═══ */
function searchBooks(){
var q=document.getElementById('searchInput').value.trim().toLowerCase();
var resultsDiv=document.getElementById('searchResults');
if(!q){
resultsDiv.style.display='none';
loadBooks();
return;
}
var results=[];
allDownloadedBooks.forEach(function(b){
if(b.book_name.toLowerCase().includes(q)||b.author.toLowerCase().includes(q)){
results.push({type:'downloaded',book_name:b.book_name,author:b.author,platform:b.platform,filename:b.filename,size:b.size});
}
});
allUploadedBooks.forEach(function(b){
if((b.title||'').toLowerCase().includes(q)||(b.author||'').toLowerCase().includes(q)){
results.push({type:'uploaded',book_name:b.title,author:b.author,platform:'upload',filename:b.converted_file,word_count:b.word_count});
}
});
api('GET','/api/zhiku/library').then(function(d){
if(!d.error&&d.data){
d.data.forEach(function(b){
if(b.book_name.toLowerCase().includes(q)||b.author.toLowerCase().includes(q)){
var exists=results.find(function(r){return r.book_name===b.book_name&&r.author===b.author});
if(!exists)results.push({type:'library',book_name:b.book_name,author:b.author,platform:'library',category:b.category});
}
});
}
renderSearchResults(results,q);
});
}
function renderSearchResults(results,query){
var div=document.getElementById('searchResults');
if(results.length===0){
div.innerHTML='<div style="text-align:center;padding:20px;color:var(--dim);font-size:13px">未找到匹配 "'+escHtml(query)+'" 的书籍</div>';
div.style.display='block';
return;
}
div.style.display='block';
div.innerHTML='<div style="font-size:11px;color:var(--muted);margin-bottom:8px;font-family:var(--mono)">找到 '+results.length+' 个结果</div>';
var platformNames={qimao:'七猫',fanqie:'番茄',yuewen:'阅文',jjwxc:'晋江',upload:'上传',library:'团队书库'};
results.forEach(function(book,idx){
var card=document.createElement('div');
card.className='search-result-card';
card.style.animationDelay=(idx*0.05)+'s';
var metaStr=platformNames[book.platform]||book.platform;
if(book.size)metaStr+=' · '+(book.size/1024).toFixed(1)+' KB';
if(book.word_count)metaStr+=' · '+(book.word_count/10000).toFixed(1)+'万字';
if(book.category)metaStr+=' · '+book.category;
var actionsHtml='';
if(book.filename){
actionsHtml+='<button class="action-btn read" data-filename="'+escHtml(book.filename)+'">📖 阅读</button>';
}
card.innerHTML='<div class="result-info">'
+'<div class="result-title">'+escHtml(book.book_name)+'</div>'
+'<div class="result-meta">'+escHtml(book.author)+' · '+escHtml(metaStr)+'</div>'
+'</div>'
+'<div class="result-actions">'+actionsHtml+'</div>';
if(book.filename){
card.querySelector('.action-btn.read').addEventListener('click',function(){openBook(book.filename);});
}
div.appendChild(card);
});
}
/* ═══ Download Panel ═══ */
function toggleDownloadPanel(){
var panel=document.getElementById('downloadPanel');
panel.style.display=panel.style.display==='block'?'none':'block';
}
function downloadBook(){
var bookName=document.getElementById('dlBookName').value.trim();
var author=document.getElementById('dlAuthor').value.trim();
var platform=document.getElementById('dlPlatform').value;
if(!bookName){showToast('请输入书名','error');return;}
// Step 1: 先搜索开源数据库匹配
showToast('正在匹配开源书源数据库...');
api('GET','/api/zhiku/download/search?book_name='+encodeURIComponent(bookName)+'&author='+encodeURIComponent(author)).then(function(d){
if(!d.error && d.data && d.data.local_match_count > 0){
// 本地已有匹配
var match = d.data.local_matches[0];
showToast('✅ 书源匹配成功! 「'+match.book_name+'」已在 '+match.source_name+' 中找到','ok');
document.getElementById('dlBookName').value='';
document.getElementById('dlAuthor').value='';
if(match.file_path){
// 直接打开已有的书
setTimeout(function(){ openBook(match.file_path); }, 800);
}
loadBooks();
return;
}
// 本地无匹配,显示数据源信息后尝试平台下载
var srcCount = (d.data && d.data.registered_source_count) || 0;
showToast('📥 已搜索 '+srcCount+' 个书源数据库,未找到匹配。正在从 '+platform+' 下载...');
// Step 2: 无本地匹配,从平台下载
api('POST','/api/zhiku/download',{book_name:bookName,author:author||'未知',platform:platform,requested_by:MEMBER_ID}).then(function(dd){
if(dd.error){showToast(dd.message||'下载失败','error');return;}
showToast('下载任务已创建','ok');
document.getElementById('dlBookName').value='';
document.getElementById('dlAuthor').value='';
setTimeout(function(){
var filename=bookName.replace(/[<>:"/\\|?*]/g,'_')+'_'+(author||'未知').replace(/[<>:"/\\|?*]/g,'_')+'_'+platform+'.txt';
api('POST','/api/zhiku/chapter/split',{filename:filename}).then(function(r){
if(!r.error)showToast('自动分章完成: '+r.data.total_chapters+' 章','ok');
loadBooks();loadStats();
});
},1500);
});
});
}
/* ═══ File Upload ═══ */
(function(){
var zone=document.getElementById('uploadZone');
var input=document.getElementById('fileInput');
if(!zone||!input)return;
['dragenter','dragover'].forEach(function(evt){
zone.addEventListener(evt,function(e){e.preventDefault();e.stopPropagation();zone.classList.add('dragover');});
});
['dragleave','drop'].forEach(function(evt){
zone.addEventListener(evt,function(e){e.preventDefault();e.stopPropagation();zone.classList.remove('dragover');});
});
zone.addEventListener('drop',function(e){
var files=e.dataTransfer.files;
if(files.length>0)handleFileUpload(files[0]);
});
input.addEventListener('change',function(){
if(input.files.length>0)handleFileUpload(input.files[0]);
input.value='';
});
})();
function handleFileUpload(file){
var progress=document.getElementById('uploadProgress');
var barFill=document.getElementById('uploadBarFill');
var status=document.getElementById('uploadStatus');
progress.style.display='block';
barFill.style.width='10%';
status.textContent='正在上传: '+file.name+' ('+(file.size/1024).toFixed(1)+' KB)...';
var formData=new FormData();
formData.append('file',file);
formData.append('title',file.name.replace(/\.[^.]+$/,''));
formData.append('uploader',MEMBER_ID);
barFill.style.width='40%';
fetch(API+'/api/zhiku/upload',{method:'POST',body:formData})
.then(function(r){return r.json()})
.then(function(d){
barFill.style.width='80%';
if(d.error){
status.textContent='❌ '+d.message;
showToast(d.message,'error');
setTimeout(function(){progress.style.display='none';barFill.style.width='0'},3000);
return;
}
barFill.style.width='100%';
status.textContent='✅ 上传成功 · '+d.data.original_format+' → TXT · '+d.data.word_count+'字';
showToast('上传成功: '+d.data.title+' ('+d.data.word_count+'字)','ok');
if(d.data.converted_file){
setTimeout(function(){
api('POST','/api/zhiku/chapter/split',{filename:d.data.converted_file}).then(function(r){
if(!r.error)showToast('自动分章完成: '+r.data.total_chapters+' 章','ok');
loadBooks();loadStats();
});
},500);
}else{
loadBooks();loadStats();
}
setTimeout(function(){progress.style.display='none';barFill.style.width='0'},4000);
})
.catch(function(err){
status.textContent='❌ 上传失败: '+err.message;
showToast('上传失败','error');
setTimeout(function(){progress.style.display='none';barFill.style.width='0'},3000);
});
}
/* ═══ Open Book ═══ */
function openBook(filename){
var bookId=filename.replace('.txt','');
api('GET','/api/zhiku/chapter/'+encodeURIComponent(bookId)).then(function(d){
if(d.error){
showToast('正在分章...');
api('POST','/api/zhiku/chapter/split',{filename:filename}).then(function(r){
if(r.error){showToast('分章失败: '+r.message,'error');return;}
showToast('分章完成: '+r.data.total_chapters+' 章','ok');
loadStats();
openReader(bookId,0);
});
return;
}
api('GET','/api/zhiku/reader/progress/'+MEMBER_ID+'/'+encodeURIComponent(bookId)).then(function(p){
var startCh=(p&&p.data&&p.data.chapter_index)?p.data.chapter_index:0;
openReader(bookId,startCh);
});
});
}
/* ═══ Reader State ═══ */
var readerState={bookId:'',currentCh:0,totalCh:0,fontSize:16,theme:'dark'};
function openReader(bookId,chapterIndex){
readerState.bookId=bookId;
readerState.currentCh=chapterIndex;
document.getElementById('readerModal').style.display='flex';
loadChapter(bookId,chapterIndex);
}
function closeReader(){
document.getElementById('readerModal').style.display='none';
if(readerState.bookId){
api('POST','/api/zhiku/reader/progress/'+MEMBER_ID+'/'+encodeURIComponent(readerState.bookId),{
chapter_index:readerState.currentCh,
scroll_position:document.getElementById('readerBody').scrollTop,
percent:readerState.totalCh>0?Math.round((readerState.currentCh+1)/readerState.totalCh*100):0
});
}
}
function loadChapter(bookId,idx){
api('GET','/api/zhiku/chapter/'+encodeURIComponent(bookId)+'/'+idx).then(function(d){
if(d.error){showToast('加载章节失败','error');return;}
var ch=d.data;
readerState.totalCh=ch.total_chapters;
readerState.currentCh=idx;
var rendered=autoRenderContent(ch.content);
document.getElementById('readerTitle').textContent=ch.title;
document.getElementById('readerBody').innerHTML='<h2 style="text-align:center;margin-bottom:20px;font-size:1.1em;color:var(--purple)">'+escHtml(ch.title)+'</h2>'
+'<div style="word-break:break-word">'+rendered+'</div>';
document.getElementById('readerBody').scrollTop=0;
document.getElementById('chapterInfo').textContent=(idx+1)+' / '+ch.total_chapters;
document.getElementById('prevChBtn').disabled=!ch.has_prev;
document.getElementById('nextChBtn').disabled=!ch.has_next;
document.getElementById('prevChBtn').style.opacity=ch.has_prev?'1':'0.3';
document.getElementById('nextChBtn').style.opacity=ch.has_next?'1':'0.3';
});
}
/**
* 自动检测内容格式并渲染
* 安全: 所有内容先 escape再应用安全格式化
* 支持: 纯文本、Markdown-like
*/
function autoRenderContent(content){
if(!content)return '';
// SECURITY: Always escape HTML first, then apply safe formatting
var escaped=escHtml(content);
// Check if original content looks like Markdown (headers, bold, etc.)
if(/^#{1,3}\s/m.test(content)||/\*\*[^*]+\*\*/m.test(content)){
var html=escaped
.replace(/^### (.+)$/gm,'<h3 style="font-size:1.1em;margin:16px 0 8px;color:var(--purple)">$1</h3>')
.replace(/^## (.+)$/gm,'<h2 style="font-size:1.2em;margin:20px 0 10px;color:var(--purple)">$1</h2>')
.replace(/^# (.+)$/gm,'<h1 style="font-size:1.3em;margin:24px 0 12px;color:var(--purple)">$1</h1>')
.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
.replace(/\*(.+?)\*/g,'<em>$1</em>')
.replace(/\n\n/g,'</p><p style="text-indent:2em;margin:0.5em 0">')
.replace(/\n/g,'<br>');
return '<p style="text-indent:2em;margin:0.5em 0">'+html+'</p>';
}
// Plain text: format with indentation
var paragraphs=escaped.split(/\n\s*\n/).filter(function(p){return p.trim()});
if(paragraphs.length>1){
return paragraphs.map(function(p){
return '<p style="text-indent:2em;margin:0.8em 0;line-height:1.9">'+p.trim().replace(/\n/g,'<br>')+'</p>';
}).join('');
}
return '<div style="white-space:pre-wrap;text-indent:2em;line-height:1.9">'+escaped+'</div>';
}
function prevChapter(){if(readerState.currentCh>0)loadChapter(readerState.bookId,readerState.currentCh-1);}
function nextChapter(){if(readerState.currentCh<readerState.totalCh-1)loadChapter(readerState.bookId,readerState.currentCh+1);}
function changeFontSize(delta){
readerState.fontSize=Math.min(28,Math.max(12,readerState.fontSize+delta*2));
document.getElementById('readerBody').style.fontSize=readerState.fontSize+'px';
}
function toggleReaderTheme(){
var body=document.getElementById('readerBody');
var modal=document.getElementById('readerModal');
var btn=document.getElementById('themeToggle');
if(readerState.theme==='dark'){
readerState.theme='light';
modal.style.background='#f5f5f0';
body.style.color='#1a1a1a';
btn.textContent='🌙';
}else{
readerState.theme='dark';
modal.style.background='var(--bg)';
body.style.color='var(--text)';
btn.textContent='☀️';
}
}
/* ═══ Team Library ═══ */
function loadLibrary(category,btn){
if(btn){
var btns=btn.parentElement.querySelectorAll('.tab-btn');
btns.forEach(function(b){b.classList.remove('active')});
btn.classList.add('active');
}
var url='/api/zhiku/library';
if(category)url+='?category='+encodeURIComponent(category);
api('GET',url).then(function(d){
var grid=document.getElementById('libraryGrid');
var empty=document.getElementById('libraryEmpty');
if(d.error||!d.data||d.data.length===0){
grid.style.display='none';empty.style.display='';return;
}
grid.style.display='';empty.style.display='none';
grid.innerHTML='';
d.data.forEach(function(book,idx){
var card=document.createElement('div');card.className='book-card';
card.style.animationDelay=(idx*0.05)+'s';
var stars='';for(var i=0;i<5;i++)stars+=i<Math.round(book.rating)?'★':'☆';
card.innerHTML='<div class="book-title">'+escHtml(book.book_name)+'</div>'
+'<div class="book-author">'+escHtml(book.author)+'</div>'
+'<div class="book-meta">'
+'<span class="book-tag">'+escHtml(book.category)+'</span>'
+(book.tags||[]).map(function(t){return '<span class="book-tag">'+escHtml(t)+'</span>'}).join('')
+'<span class="book-tag" style="color:var(--warn)">'+stars+' '+book.rating+'</span>'
+'<span class="book-tag">引用 '+book.ref_count+'</span>'
+'</div>';
grid.appendChild(card);
});
});
}
/* ═══ Agent Chat (AI Powered) ═══ */
var agentReady=false;
var agentSending=false;
function initAgent(){
// 先尝试注册Agent如果失败说明API不可达
api('POST','/api/zhiku/agent/'+MEMBER_ID,{nickname:'成员'}).then(function(d){
if(d.error){
agentReady=false;
updateAgentStatus(false, 'API连接失败');
appendChat('agent','⚠️ 无法连接到后端服务。请检查网络连接或联系管理员。\n\n错误: '+(d.message||'网络错误'));
return;
}
agentReady=true;
// 检查AI是否真正可用
api('GET','/api/zhiku/ai/health').then(function(aiData){
if(!aiData.error && aiData.data && aiData.data.available_models && aiData.data.available_models.length > 0){
updateAgentStatus(true, 'AI在线 · '+aiData.data.available_models[0].name);
appendChat('agent','🤖 AI Agent已就绪当前使用: '+aiData.data.available_models.map(function(m){return m.name}).join(' / ')+'\n\n📖 分析小说内容\n✂ 拆文整理\n📝 生成摘要\n💡 创作建议\n\n直接聊天即可有什么我可以帮你的');
} else {
updateAgentStatus('partial', '本地模式无AI API');
appendChat('agent','🤖 Agent已连接本地模式。AI大模型尚未配置 API Key。\n\n当前可用功能:\n💬 基础对话\n📝 记忆/笔记\n💡 帮助指令\n\n⚠ 如需AI辅助摘要/改写/续写),请配置 DeepSeek/Kimi/通义千问 API Key。');
}
});
});
}
function appendChat(role,text,model){
var box=document.getElementById('agentChatBox');
var div=document.createElement('div');
div.className='chat-msg '+(role==='user'?'chat-msg-user':'chat-msg-agent');
var bubble=document.createElement('div');
bubble.className='chat-bubble '+(role==='user'?'chat-bubble-user':'chat-bubble-agent');
var rendered=escHtml(text).replace(/\n/g,'<br>');
bubble.innerHTML=rendered;
if(role==='agent'&&model&&model!=='local'&&model!=='none'){
var tag=document.createElement('span');
tag.className='model-tag';
tag.textContent='via '+model;
bubble.appendChild(tag);
}
div.appendChild(bubble);
box.appendChild(div);
box.scrollTop=box.scrollHeight;
}
function showTypingIndicator(){
var box=document.getElementById('agentChatBox');
var div=document.createElement('div');
div.className='chat-msg chat-msg-agent';
div.id='typingIndicator';
div.innerHTML='<div class="chat-bubble chat-bubble-agent"><div class="typing-indicator"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div></div>';
box.appendChild(div);
box.scrollTop=box.scrollHeight;
}
function removeTypingIndicator(){
var el=document.getElementById('typingIndicator');
if(el)el.remove();
}
function sendAgentChat(preset){
if(agentSending)return;
var msg=preset||document.getElementById('agentInput').value.trim();
if(!msg)return;
if(!preset)document.getElementById('agentInput').value='';
if(!agentReady && !systemStatus.api){
appendChat('user',msg);
appendChat('agent','⚠️ 无法发送消息:后端服务未连接。\n\n请检查\n1. 服务器是否运行中\n2. API地址是否正确\n3. 网络连接是否正常');
return;
}
appendChat('user',msg);
agentSending=true;
document.getElementById('agentSendBtn').disabled=true;
showTypingIndicator();
api('POST','/api/zhiku/agent/'+MEMBER_ID+'/chat',{message:msg}).then(function(d){
removeTypingIndicator();
agentSending=false;
document.getElementById('agentSendBtn').disabled=false;
if(d.error){
appendChat('agent','⚠️ '+(d.message||'网络错误 · 无法连接到服务器'));
updateAgentStatus(false, '连接中断');
return;
}
var model=d.data.model||'';
appendChat('agent',d.data.reply,model);
if(model&&model!=='local'&&model!=='none'){
var badge=document.getElementById('agentModelBadge');
badge.textContent=model;
badge.style.display='';
updateAgentStatus(true, 'AI在线 · '+model);
}
});
}
/* ═══ Auto-resize chat input ═══ */
(function(){
var input=document.getElementById('agentInput');
if(!input)return;
input.addEventListener('input',function(){
this.style.height='auto';
this.style.height=Math.min(this.scrollHeight,100)+'px';
});
})();
/* ═══ Keyboard shortcuts ═══ */
document.getElementById('searchInput').addEventListener('keydown',function(e){if(e.key==='Enter')searchBooks();});
document.getElementById('agentInput').addEventListener('keydown',function(e){
if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendAgentChat();}
});
/* ═══ Init with retry ═══ */
var statsRetryCount=0;
var statsRetryMax=3;
var statsLoading=false;
function loadStatsWithRetry(){
if(statsLoading)return;
statsLoading=true;
var origCb=systemStatus.api;
loadStats();
// loadStats是异步的通过轮询检测结果
setTimeout(function checkResult(){
statsLoading=false;
if(!systemStatus.api && statsRetryCount<statsRetryMax){
statsRetryCount++;
console.log('[智库] API重试 '+statsRetryCount+'/'+statsRetryMax);
setTimeout(loadStatsWithRetry,(statsRetryCount)*5000);
} else if(systemStatus.api){
statsRetryCount=0;
}
},3000);
}
loadStatsWithRetry();
loadBooks();
loadLibrary();
initAgent();
// 定期刷新状态每60秒
(function scheduleRefresh(){setTimeout(function(){loadStats();scheduleRefresh();},60000)})();
</script>
</body>
</html>