zhizhi/docs/index.html

1492 lines
74 KiB
HTML
Raw 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.0,maximum-scale=5.0,viewport-fit=cover">
<meta name="description" content="光湖灯塔 · 语言驱动操作系统 AGE OS">
<meta name="theme-color" content="#080c18">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>光湖灯塔 · AGE OS</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>🏛️</text></svg>">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
/* ═══ Themes ═══ */
:root{
--bg:#060a14;--bg1:#0c1220;--bg2:#111827;--bg3:#1a2332;
--border:#1e2d44;--accent:#4f8ef7;--accent2:#7c6ef7;--accent3:#a78bfa;
--ok:#34d399;--warn:#fbbf24;--err:#f87171;
--text:#e2e8f0;--dim:#64748b;--muted:#334155;
--glow:0 0 24px rgba(79,142,247,.1);
--r:12px;--rs:8px;
--f:-apple-system,BlinkMacSystemFont,'PingFang SC','Helvetica Neue','Microsoft YaHei',sans-serif;
--m:'SF Mono','Fira Code',Consolas,monospace;
--sb:env(safe-area-inset-bottom,0px);--st:env(safe-area-inset-top,0px);
}
.t-twi{--bg:#0a0612;--bg1:#140e22;--bg2:#1a1430;--bg3:#241c3d;--accent:#c084fc;--accent2:#e879f9;--accent3:#f0abfc;--border:#2d1f4e}
.t-sea{--bg:#041318;--bg1:#082029;--bg2:#0c2d3a;--bg3:#103a4c;--accent:#22d3ee;--accent2:#06b6d4;--accent3:#67e8f9;--border:#164e63}
.t-for{--bg:#06120a;--bg1:#0c2014;--bg2:#112d1c;--bg3:#183a26;--accent:#34d399;--accent2:#4ade80;--accent3:#86efac;--border:#1a4a2e}
.t-emb{--bg:#140a06;--bg1:#221208;--bg2:#2d1a0e;--bg3:#3a2414;--accent:#fb923c;--accent2:#f97316;--accent3:#fdba74;--border:#4a2e1a}
html{-webkit-tap-highlight-color:transparent}
body{font-family:var(--f);background:var(--bg);color:var(--text);font-size:15px;line-height:1.6;overflow:hidden;height:100vh;height:100dvh;-webkit-font-smoothing:antialiased}
a{color:var(--accent);text-decoration:none}
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-thumb{background:var(--muted);border-radius:2px}
/* ═══ Background ═══ */
.bg{position:fixed;inset:0;z-index:0;pointer-events:none}
.bg::before{content:'';position:absolute;inset:0;background-image:linear-gradient(rgba(79,142,247,.02) 1px,transparent 1px),linear-gradient(90deg,rgba(79,142,247,.02) 1px,transparent 1px);background-size:50px 50px;animation:gm 30s linear infinite}
@keyframes gm{to{background-position:50px 50px}}
.orb{position:absolute;border-radius:50%;filter:blur(80px);opacity:.07;animation:ob 22s ease-in-out infinite}
.orb:nth-child(1){width:350px;height:350px;background:var(--accent);top:-8%;left:-4%}
.orb:nth-child(2){width:280px;height:280px;background:var(--accent2);bottom:-6%;right:-4%;animation-delay:8s}
@keyframes ob{0%,100%{transform:translate(0,0)}50%{transform:translate(20px,-15px)}}
/* ═══ 3D Page Flip Engine ═══ */
.pages{position:fixed;inset:0;z-index:1;perspective:1400px;overflow:hidden}
.page{
position:absolute;inset:0;
overflow-y:auto;overflow-x:hidden;
-webkit-overflow-scrolling:touch;
padding:0 0 var(--sb) 0;
background:var(--bg);
backface-visibility:hidden;-webkit-backface-visibility:hidden;
transition:transform .55s cubic-bezier(.4,0,.15,1),opacity .55s;
transform-origin:center center;
will-change:transform,opacity;
}
.page[data-state="active"]{transform:rotateY(0deg) scale(1);opacity:1;z-index:2;pointer-events:auto}
.page[data-state="left"]{transform:rotateY(-92deg) scale(.92);opacity:0;z-index:1;pointer-events:none}
.page[data-state="right"]{transform:rotateY(92deg) scale(.92);opacity:0;z-index:1;pointer-events:none}
.page[data-state="hidden"]{transform:rotateY(92deg) scale(.88);opacity:0;z-index:0;pointer-events:none;transition:none}
/* ═══ Common UI ═══ */
.bar{position:sticky;top:0;z-index:10;display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:rgba(6,10,20,.92);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border);min-height:48px;padding-top:calc(10px + var(--st))}
.bar-title{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}
.bar-right{display:flex;align-items:center;gap:8px}
.dot{width:6px;height:6px;border-radius:50%;background:var(--ok);box-shadow:0 0 6px var(--ok);animation:pls 2s infinite;flex-shrink:0}
@keyframes pls{50%{opacity:.4}}
.btn{background:none;border:1px solid var(--border);color:var(--dim);padding:5px 12px;border-radius:var(--rs);font-size:12px;cursor:pointer;font-family:var(--f);transition:all .2s;white-space:nowrap}
.btn:hover{color:var(--accent);border-color:var(--accent)}
.btn:active{transform:scale(.96)}
.btn-accent{background:linear-gradient(135deg,var(--accent),var(--accent2));border:none;color:#fff}
.btn-accent:hover{opacity:.88;color:#fff}
.content{padding:16px;max-width:700px;margin:0 auto}
.card{background:var(--bg1);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:12px;transition:border-color .3s}
.card:hover{border-color:var(--accent)}
.tag{display:inline-block;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:600}
.tag-ok{background:rgba(52,211,153,.12);color:var(--ok)}
.tag-warn{background:rgba(251,191,36,.12);color:var(--warn)}
.tag-info{background:rgba(79,142,247,.12);color:var(--accent)}
.empty{text-align:center;padding:40px 16px;color:var(--muted);font-size:14px}
.empty em{font-style:normal;font-size:40px;display:block;margin-bottom:12px}
input[type=text],textarea{background:var(--bg2);border:1px solid var(--border);border-radius:var(--rs);color:var(--text);padding:10px 12px;font-size:14px;font-family:var(--f);outline:none;width:100%;transition:border-color .2s;-webkit-appearance:none}
input[type=text]:focus,textarea:focus{border-color:var(--accent)}
::placeholder{color:var(--muted)}
label{display:block;font-size:12px;color:var(--dim);margin-bottom:4px;font-weight:600}
.form-row{margin-bottom:14px}
.form-row-inline{display:flex;gap:10px}
@media(max-width:480px){.form-row-inline{flex-direction:column}}
/* ═══ PAGE: Home ═══ */
.home-hero{text-align:center;padding:32px 16px}
.home-ver{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:20px;background:rgba(79,142,247,.08);border:1px solid rgba(79,142,247,.12);font-size:11px;color:var(--accent);font-family:var(--m);margin-bottom:16px}
.home-hero h1{font-size:clamp(26px,5.5vw,40px);font-weight:800;line-height:1.2;margin-bottom:6px}
.home-hero .sub{font-size:clamp(12px,2.5vw,15px);color:var(--dim);margin-bottom:12px}
.home-hero .quote{font-size:13px;color:var(--accent2);font-style:italic}
.upd{display:flex;gap:10px;padding:12px;margin-bottom:8px;background:var(--bg1);border:1px solid var(--border);border-radius:var(--r);transition:border-color .3s}
.upd:hover{border-color:var(--accent)}
.upd-v{min-width:42px;padding:3px 6px;text-align:center;background:rgba(79,142,247,.08);border-radius:var(--rs);color:var(--accent);font-size:10px;font-weight:600;font-family:var(--m);height:fit-content;flex-shrink:0}
.upd h4{font-size:13px;font-weight:600;margin-bottom:2px}
.upd p{font-size:11px;color:var(--dim);line-height:1.5}
.home-enter{padding:24px;text-align:center;background:linear-gradient(135deg,var(--bg1),var(--bg2));border:1px solid var(--border);border-radius:var(--r);cursor:pointer;transition:all .3s;margin:20px 0}
.home-enter:hover{border-color:var(--accent);box-shadow:var(--glow)}
.home-enter:active{transform:scale(.98)}
.home-enter em{font-style:normal;font-size:36px;display:block;margin-bottom:8px}
.home-enter strong{font-size:17px;display:block;margin-bottom:4px}
.home-enter span{font-size:12px;color:var(--dim)}
.home-footer{text-align:center;padding:24px 0;font-size:11px;color:var(--muted);border-top:1px solid var(--border);margin-top:24px}
.home-footer .motto{color:var(--accent2);font-style:italic;margin-top:4px}
/* ═══ PAGE: Desk (作品列表) ═══ */
.work-item{cursor:pointer}
.work-item h3{font-size:15px;font-weight:600;margin-bottom:4px}
.work-item .meta{font-size:11px;color:var(--dim);display:flex;gap:10px;flex-wrap:wrap}
.work-item .meta span{display:flex;align-items:center;gap:3px}
/* ═══ PAGE: Work Hub (作品导航) ═══ */
.hub-title{text-align:center;padding:20px 0 12px}
.hub-title h2{font-size:20px;font-weight:700}
.hub-title p{font-size:12px;color:var(--dim);margin-top:2px}
.hub-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
@media(max-width:360px){.hub-grid{grid-template-columns:1fr}}
.hub-card{text-align:center;padding:20px 12px;background:var(--bg1);border:1px solid var(--border);border-radius:var(--r);cursor:pointer;transition:all .3s}
.hub-card:hover{border-color:var(--accent);box-shadow:var(--glow)}
.hub-card:active{transform:scale(.97)}
.hub-card em{font-style:normal;font-size:28px;display:block;margin-bottom:6px}
.hub-card strong{font-size:14px;display:block;margin-bottom:3px}
.hub-card span{font-size:11px;color:var(--dim)}
.hub-stats{display:flex;justify-content:center;gap:20px;padding:12px 0;flex-wrap:wrap}
.hub-stat{text-align:center}
.hub-stat b{display:block;font-size:18px;color:var(--accent);font-family:var(--m)}
.hub-stat small{font-size:10px;color:var(--dim)}
/* ═══ PAGE: Outline ═══ */
.outline-editor{min-height:250px;resize:vertical;line-height:1.9;font-size:14px}
.vol-item{display:flex;align-items:center;justify-content:space-between;padding:10px;background:var(--bg2);border-radius:var(--rs);margin-bottom:6px}
.vol-item span{font-size:13px;font-weight:600}
.vol-actions{display:flex;gap:4px}
/* ═══ PAGE: Characters ═══ */
.char-card{display:flex;gap:12px;align-items:flex-start}
.char-avatar{width:42px;height:42px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--accent2));display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
.char-info h4{font-size:14px;font-weight:600;margin-bottom:2px}
.char-info p{font-size:12px;color:var(--dim);line-height:1.5}
.char-role{font-size:10px;color:var(--accent);font-weight:600}
/* ═══ PAGE: Editor ═══ */
.editor-info{display:flex;justify-content:space-between;align-items:center;padding:8px 0;font-size:12px;color:var(--dim);border-bottom:1px solid var(--border);margin-bottom:12px}
.editor-textarea{min-height:calc(100vh - 260px);min-height:calc(100dvh - 260px);resize:none;line-height:2;font-size:15px;border:none;background:transparent;padding:0}
.editor-textarea:focus{border:none}
.editor-bottom{position:sticky;bottom:0;padding:8px 0;background:var(--bg);display:flex;justify-content:space-between;align-items:center;border-top:1px solid var(--border);font-size:11px;color:var(--dim);font-family:var(--m)}
/* ═══ Chat FAB ═══ */
.chat-fab{position:fixed;bottom:calc(16px + var(--sb));right:14px;z-index:50;width:50px;height:50px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--accent2));border:none;color:#fff;font-size:21px;cursor:pointer;box-shadow:0 4px 16px rgba(79,142,247,.3);transition:all .25s;display:flex;align-items:center;justify-content:center}
.chat-fab:hover{transform:scale(1.06)}
.chat-fab:active{transform:scale(.93)}
.chat-fab.on{border-radius:var(--r);background:rgba(248,113,113,.85)}
/* 编辑器模式下缩小悬浮球,不干扰打字 */
.chat-fab.editor-mode{width:38px;height:38px;font-size:16px;opacity:.6;bottom:calc(8px + var(--sb))}
.chat-fab.editor-mode:hover{opacity:1;transform:scale(1.1)}
/* ═══ Chat Panel ═══ */
.chat-p{position:fixed;bottom:calc(76px + var(--sb));right:14px;z-index:50;width:min(360px,calc(100vw - 28px));height:min(440px,calc(100vh - 120px));height:min(440px,calc(100dvh - 120px));background:var(--bg1);border:1px solid var(--border);border-radius:var(--r);display:flex;flex-direction:column;transform:scale(.9) translateY(14px);opacity:0;pointer-events:none;transition:all .22s cubic-bezier(.4,0,.2,1);box-shadow:0 12px 40px rgba(0,0,0,.5)}
.chat-p.open{transform:scale(1) translateY(0);opacity:1;pointer-events:auto}
@media(max-width:480px){.chat-p{bottom:0;right:0;left:0;width:100%;height:calc(100vh - 48px);height:calc(100dvh - 48px);border-radius:var(--r) var(--r) 0 0;padding-bottom:var(--sb)}}
.chat-hd{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;flex-shrink:0}
.chat-av{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--accent2));display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}
.chat-nm{font-weight:600;font-size:13px}
.chat-sb{font-size:10px;color:var(--dim)}
.chat-tg{margin-left:auto;font-size:9px;padding:2px 6px;background:var(--bg2);border:1px solid var(--border);border-radius:20px;color:var(--dim);font-family:var(--m)}
.chat-ms{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:8px;-webkit-overflow-scrolling:touch}
.cm{max-width:82%;padding:8px 12px;border-radius:var(--r);font-size:13px;line-height:1.6;word-break:break-word}
.cm.u{align-self:flex-end;background:var(--accent);color:#fff;border-bottom-right-radius:4px}
.cm.b{align-self:flex-start;background:var(--bg2);border:1px solid var(--border);border-bottom-left-radius:4px}
.cm.s{align-self:center;background:none;color:var(--muted);font-size:10px;padding:2px}
.cm-m{display:flex;gap:5px;margin-top:4px;font-size:9px;color:var(--muted)}
.ct{font-size:11px;color:var(--dim);padding:4px 12px}
.ct span{animation:bk 1.4s infinite;font-size:14px}
.ct span:nth-child(2){animation-delay:.2s}
.ct span:nth-child(3){animation-delay:.4s}
@keyframes bk{0%,100%{opacity:.2}50%{opacity:1}}
.chat-in{padding:10px;border-top:1px solid var(--border);display:flex;gap:6px;flex-shrink:0}
.cin{flex:1;padding:8px 10px;background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);color:var(--text);font-size:14px;outline:none;font-family:var(--f);-webkit-appearance:none}
.cin:focus{border-color:var(--accent)}
.csnd{width:36px;height:36px;border-radius:50%;background:var(--accent);border:none;color:#fff;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .2s}
.csnd:hover{background:var(--accent2)}
.csnd:disabled{opacity:.3}
/* ═══ Three-Column Home Layout ═══ */
.home-grid{display:grid;grid-template-columns:260px 1fr 260px;gap:16px;padding:16px;max-width:1200px;margin:0 auto;align-content:start}
@media(max-width:960px){.home-grid{grid-template-columns:1fr;max-width:600px}}
.panel{background:var(--bg1);border:1px solid var(--border);border-radius:var(--r);padding:14px;height:fit-content}
.panel-title{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:1.2px;font-weight:700;margin-bottom:10px;display:flex;align-items:center;gap:6px}
.panel-item{padding:6px 0;border-bottom:1px solid var(--border);font-size:12px}
.panel-item:last-child{border-bottom:none}
.panel-label{color:var(--dim);font-size:10px;margin-bottom:1px}
.panel-value{color:var(--text);font-weight:600;font-family:var(--m);font-size:11px}
.panel-value.ok{color:var(--ok)}
.panel-value.warn{color:var(--warn)}
.panel-value.err{color:var(--err)}
/* ═══ Ticker ═══ */
.ticker-wrap{overflow:hidden;background:var(--bg1);border:1px solid var(--border);border-radius:var(--rs);padding:6px 0;margin-bottom:14px;position:relative}
.ticker-wrap::before,.ticker-wrap::after{content:'';position:absolute;top:0;bottom:0;width:24px;z-index:2;pointer-events:none}
.ticker-wrap::before{left:0;background:linear-gradient(90deg,var(--bg1),transparent)}
.ticker-wrap::after{right:0;background:linear-gradient(-90deg,var(--bg1),transparent)}
.ticker{display:flex;animation:tickScroll 28s linear infinite;white-space:nowrap;gap:36px;padding:0 16px}
.ticker:hover{animation-play-state:paused}
.ticker span{font-size:11px;color:var(--dim);display:flex;align-items:center;gap:5px;flex-shrink:0}
.ticker span em{font-style:normal}
@keyframes tickScroll{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
/* ═══ Team Card ═══ */
.team-slot{display:flex;align-items:center;gap:8px;padding:7px 8px;background:var(--bg2);border-radius:var(--rs);margin-bottom:5px;transition:border-color .3s;border:1px solid transparent}
.team-slot:hover{border-color:var(--accent)}
.team-av{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0}
.team-nm{font-size:11px;font-weight:600}
.team-role{font-size:9px;color:var(--dim)}
/* ═══ Feedback Page ═══ */
.fb-item{padding:10px}
.fb-item .fb-meta{font-size:10px;color:var(--dim);margin-top:6px;display:flex;gap:8px;align-items:center}
.fb-status{display:inline-block;padding:1px 6px;border-radius:10px;font-size:9px;font-weight:600}
.fb-status.pending{background:rgba(251,191,36,.12);color:var(--warn)}
.fb-status.replied{background:rgba(52,211,153,.12);color:var(--ok)}
.fb-reply{margin-top:6px;padding:8px;background:var(--bg);border-radius:var(--rs);font-size:12px;color:var(--accent)}
/* ═══ Home Entry Cards ═══ */
.home-entries{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 0}
@media(max-width:480px){.home-entries{grid-template-columns:1fr}}
.entry-card{padding:18px 14px;text-align:center;background:linear-gradient(135deg,var(--bg1),var(--bg2));border:1px solid var(--border);border-radius:var(--r);cursor:pointer;transition:all .3s}
.entry-card:hover{border-color:var(--accent);box-shadow:var(--glow)}
.entry-card:active{transform:scale(.98)}
.entry-card em{font-style:normal;font-size:28px;display:block;margin-bottom:6px}
.entry-card strong{font-size:14px;display:block;margin-bottom:3px}
.entry-card span{font-size:11px;color:var(--dim)}
/* ═══ Monitor ═══ */
.mon{position:fixed;top:calc(6px + var(--st));right:10px;z-index:40;background:rgba(12,18,32,.9);border:1px solid var(--border);border-radius:var(--r);padding:5px 9px;font-size:9px;font-family:var(--m);color:var(--dim);cursor:pointer;transition:all .3s;user-select:none;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
.mon:hover{border-color:var(--accent)}
.mon.x{padding:8px 12px}
.mr{display:flex;justify-content:space-between;gap:8px;padding:1px 0}
.mv{color:var(--accent);font-weight:600}
.mx{display:none;margin-top:4px;padding-top:4px;border-top:1px solid var(--border)}
.mon.x .mx{display:block}
/* ═══ Viewport-Fit Layout (Desktop ≥961px) ═══ */
@media(min-width:961px){
#pg-home{overflow:hidden;display:flex;flex-direction:column}
.home-grid{flex:1 1 0;min-height:0;align-content:stretch;overflow:hidden;padding-bottom:8px}
.home-grid>.panel,.home-grid>.home-center{overflow-y:auto;min-height:0;height:auto}
}
/* ═══ API Key Section ═══ */
.chat-api{border-top:1px solid var(--border);flex-shrink:0}
.chat-api-tog{padding:6px 12px;font-size:11px;color:var(--dim);cursor:pointer;display:flex;align-items:center;gap:5px;transition:color .2s;user-select:none}
.chat-api-tog:hover{color:var(--accent)}
.chat-api-tog .arr{font-size:9px;margin-left:auto;transition:transform .2s;display:inline-block}
.chat-api-tog.open .arr{transform:rotate(90deg)}
.chat-api-bd{display:none;padding:0 10px 8px}
.chat-api-bd.open{display:block}
.chat-api-row{display:flex;gap:5px;margin-bottom:5px}
.api-sel{flex:0 0 auto;padding:5px 6px;background:var(--bg2);border:1px solid var(--border);border-radius:var(--rs);color:var(--text);font-size:11px;outline:none;font-family:var(--f);max-width:110px}
.api-sel:focus{border-color:var(--accent)}
.api-key-inp{flex:1;padding:5px 8px;background:var(--bg2);border:1px solid var(--border);border-radius:var(--rs);color:var(--text);font-size:12px;outline:none;font-family:var(--m);min-width:0}
.api-key-inp:focus{border-color:var(--accent)}
.api-conn-btn{flex-shrink:0;padding:5px 10px;background:var(--accent);border:none;border-radius:var(--rs);color:#fff;font-size:11px;cursor:pointer;font-family:var(--f);transition:opacity .2s}
.api-conn-btn:hover{opacity:.85}
.api-conn-btn:disabled{opacity:.4;cursor:default}
.api-st{font-size:10px;color:var(--dim);padding:2px 2px}
</style>
</head>
<body>
<div class="bg"><div class="orb"></div><div class="orb"></div></div>
<!-- ═══════════════════════════════════════ -->
<!-- PAGE: Home (灯塔首页) -->
<!-- ═══════════════════════════════════════ -->
<div class="pages" id="pages">
<div class="page" id="pg-home" data-state="active">
<div class="bar">
<div class="bar-title"><span style="font-size:20px">🏛️</span> 光湖灯塔</div>
<div class="bar-right"><span class="dot"></span><span style="font-size:11px;color:var(--dim)">在线</span><button class="btn" id="themeBtn">🎨</button></div>
</div>
<div class="home-grid">
<!-- ═══ Left Panel: 系统状态 ═══ -->
<div class="panel" id="sysPanel">
<div class="panel-title">📡 系统状态</div>
<div class="panel-item"><div class="panel-label">服务器</div><div class="panel-value ok" id="sysStatus">检测中…</div></div>
<div class="panel-item"><div class="panel-label">延迟</div><div class="panel-value" id="sysLatency"></div></div>
<div class="panel-item"><div class="panel-label">运行时间</div><div class="panel-value" id="sysUptime"></div></div>
<div class="panel-item"><div class="panel-label">工作流</div><div class="panel-value">18 个 · 9 军团</div></div>
<div class="panel-item"><div class="panel-label">模块部署</div><div class="panel-value">52 模块</div></div>
<div class="panel-item"><div class="panel-label">COS 存储</div><div class="panel-value ok">双桶就绪</div></div>
<div class="panel-title" style="margin-top:12px">🧠 版本更新</div>
<div id="sysUpdates">
<div class="panel-item"><div class="panel-label">v40.0 · 2026-04-02</div><div style="font-size:11px">COS双桶存储上线</div></div>
<div class="panel-item"><div class="panel-label">v39.0 · 2026-04-01</div><div style="font-size:11px">全链路部署观测</div></div>
<div class="panel-item"><div class="panel-label">v38.0 · 2026-04-01</div><div style="font-size:11px">HLDP协作语言协议</div></div>
</div>
</div>
<!-- ═══ Center: 核心内容 ═══ -->
<div class="home-center">
<div class="home-hero">
<div class="home-ver" id="homeVer">AGE OS v40.0</div>
<h1>光湖灯塔</h1>
<div class="sub">语言驱动操作系统 · 人类只说话,系统搞定一切</div>
<div class="quote">「语言 = 现实」</div>
</div>
<div class="ticker-wrap">
<div class="ticker" id="ticker">
<span><em>🏛️</em> AGE OS v40.0 运行中</span>
<span><em>📡</em> COS双桶存储已上线</span>
<span><em>🧠</em> HLDP协作语言协议在线</span>
<span><em></em> 全链路部署观测系统就绪</span>
<span><em>✍️</em> 码字工作台 Beta 内测中</span>
<span><em>🏛️</em> AGE OS v40.0 运行中</span>
<span><em>📡</em> COS双桶存储已上线</span>
<span><em>🧠</em> HLDP协作语言协议在线</span>
<span><em></em> 全链路部署观测系统就绪</span>
<span><em>✍️</em> 码字工作台 Beta 内测中</span>
</div>
</div>
<div class="home-entries">
<div class="entry-card" onclick="go('desk')">
<em>✍️</em>
<strong>码字工作台</strong>
<span>网文行业 · AI辅助创作</span>
<div style="margin-top:6px"><span class="tag tag-warn">Beta</span></div>
</div>
<div class="entry-card" onclick="go('feedback')">
<em>📮</em>
<strong>留言板</strong>
<span>给冰朔和铸渊留言</span>
<div style="margin-top:6px"><span class="tag tag-info">每条必回</span></div>
</div>
</div>
<div class="home-footer">
<p>© 2026 光湖语言世界 · 国作登字-2026-A-00037559</p>
<p class="motto">「人类只说话,系统搞定一切」</p>
</div>
</div>
<!-- ═══ Right Panel: 光湖团队 ═══ -->
<div class="panel">
<div class="panel-title">🌊 光湖团队</div>
<div class="team-slot">
<div class="team-av" style="background:linear-gradient(135deg,#4f8ef7,#7c6ef7)">❄️</div>
<div><div class="team-nm">冰朔 · TCS-0002∞</div><div class="team-role">系统主权 · 语言架构</div></div>
</div>
<div class="team-slot">
<div class="team-av" style="background:linear-gradient(135deg,#34d399,#4ade80)">🏛️</div>
<div><div class="team-nm">铸渊 · ICE-GL-ZY001</div><div class="team-role">代码守护 · 执行层</div></div>
</div>
<div class="panel-title" style="margin-top:12px">✍️ 网文行业人类主控团队</div>
<div class="team-slot">
<div class="team-av" style="background:linear-gradient(135deg,#fb923c,#f97316)">🐱</div>
<div><div class="team-nm">肥猫 + 舒舒</div><div class="team-role">男频编辑 · PER-SS001</div></div>
</div>
<div class="team-slot">
<div class="team-av" style="background:linear-gradient(135deg,#c084fc,#e879f9)">🍊</div>
<div><div class="team-nm">桔子 + 晨星</div><div class="team-role">女频编辑 · PER-CX001</div></div>
</div>
<div class="team-slot">
<div class="team-av" style="background:linear-gradient(135deg,#22d3ee,#06b6d4)">📄</div>
<div><div class="team-nm">页页 + 坍缩核</div><div class="team-role">女频作者 · PER-XTS001</div></div>
</div>
<div class="team-slot">
<div class="team-av" style="background:linear-gradient(135deg,#fbbf24,#f59e0b)">🌸</div>
<div><div class="team-nm">花尔 + 糖星云</div><div class="team-role">女频副主控 · PER-TXY001</div></div>
</div>
<div class="panel-title" style="margin-top:12px">⚙️ 活跃系统</div>
<div id="activeAgents" style="display:flex;flex-wrap:wrap;gap:3px">
<span class="tag tag-ok" style="margin:0">听潮</span>
<span class="tag tag-ok" style="margin:0">锻心</span>
<span class="tag tag-ok" style="margin:0">织脉</span>
<span class="tag tag-ok" style="margin:0">映阁</span>
<span class="tag tag-ok" style="margin:0">守夜</span>
<span class="tag tag-ok" style="margin:0">试镜</span>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════ -->
<!-- PAGE: Desk (码字工作台 · 作品列表) -->
<!-- ═══════════════════════════════════════ -->
<div class="page" id="pg-desk" data-state="hidden">
<div class="bar">
<button class="btn" onclick="go('home')">← 灯塔</button>
<div class="bar-title">✍️ 码字工作台</div>
<button class="btn btn-accent" onclick="go('create')">+ 新作品</button>
</div>
<div class="content">
<div id="workList"></div>
</div>
</div>
<!-- ═══════════════════════════════════════ -->
<!-- PAGE: Create (新建作品) -->
<!-- ═══════════════════════════════════════ -->
<div class="page" id="pg-create" data-state="hidden">
<div class="bar">
<button class="btn" onclick="go('desk')">← 返回</button>
<div class="bar-title">📖 新建作品</div>
<button class="btn btn-accent" id="createSave">创建</button>
</div>
<div class="content">
<div class="form-row"><label>作品名称</label><input type="text" id="cTitle" placeholder="你的作品叫什么名字?"></div>
<div class="form-row"><label>类型</label><input type="text" id="cGenre" placeholder="玄幻 / 都市 / 言情 / 科幻 ..."></div>
<div class="form-row"><label>简介</label><textarea id="cSummary" rows="4" placeholder="一句话介绍你的故事..."></textarea></div>
</div>
</div>
<!-- ═══════════════════════════════════════ -->
<!-- PAGE: Hub (作品导航中心) -->
<!-- ═══════════════════════════════════════ -->
<div class="page" id="pg-hub" data-state="hidden">
<div class="bar">
<button class="btn" onclick="go('desk')">← 作品列表</button>
<div class="bar-title" id="hubTitle">作品</div>
<div class="bar-right"><span class="dot"></span></div>
</div>
<div class="content">
<div class="hub-title">
<h2 id="hubName"></h2>
<p id="hubGenre"></p>
</div>
<div class="hub-stats" id="hubStats"></div>
<div class="hub-grid">
<div class="hub-card" onclick="go('outline')"><em>📋</em><strong>大纲</strong><span>总纲 · 卷纲</span></div>
<div class="hub-card" onclick="go('chars')"><em>👥</em><strong>人物卡</strong><span>角色管理</span></div>
<div class="hub-card" onclick="go('chapters')"><em>📚</em><strong>章节</strong><span>目录管理</span></div>
<div class="hub-card" onclick="go('editor')"><em>✏️</em><strong>写正文</strong><span>续写章节</span></div>
</div>
<div style="margin-top:16px;text-align:center;font-size:11px;color:var(--muted);font-style:italic">对铸渊说「进入大纲」「人物卡」「写正文」自动翻转</div>
</div>
</div>
<!-- ═══════════════════════════════════════ -->
<!-- PAGE: Outline (大纲) -->
<!-- ═══════════════════════════════════════ -->
<div class="page" id="pg-outline" data-state="hidden">
<div class="bar">
<button class="btn" onclick="go('hub')">← 返回</button>
<div class="bar-title">📋 大纲</div>
<button class="btn btn-accent" id="outlineSave">保存</button>
</div>
<div class="content">
<div class="form-row"><label>总大纲</label><textarea id="olMain" class="outline-editor" placeholder="在这里写下你的故事总纲...&#10;&#10;整体世界观、主线剧情、结局走向..."></textarea></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin:16px 0 8px">
<label style="margin:0">分卷大纲</label>
<button class="btn" id="addVol">+ 新卷</button>
</div>
<div id="volList"></div>
</div>
</div>
<!-- ═══════════════════════════════════════ -->
<!-- PAGE: Characters (人物卡) -->
<!-- ═══════════════════════════════════════ -->
<div class="page" id="pg-chars" data-state="hidden">
<div class="bar">
<button class="btn" onclick="go('hub')">← 返回</button>
<div class="bar-title">👥 人物卡</div>
<button class="btn btn-accent" id="addChar">+ 新角色</button>
</div>
<div class="content">
<div id="charList"></div>
</div>
</div>
<!-- ═══════════════════════════════════════ -->
<!-- PAGE: Add/Edit Character -->
<!-- ═══════════════════════════════════════ -->
<div class="page" id="pg-charEdit" data-state="hidden">
<div class="bar">
<button class="btn" onclick="go('chars')">← 返回</button>
<div class="bar-title" id="ceTitle">新角色</div>
<button class="btn btn-accent" id="ceSave">保存</button>
</div>
<div class="content">
<div class="form-row"><label>姓名</label><input type="text" id="ceName" placeholder="角色名"></div>
<div class="form-row"><label>身份/定位</label><input type="text" id="ceRole" placeholder="主角 / 女主 / 反派 / 配角 ..."></div>
<div class="form-row"><label>外貌特征</label><textarea id="ceAppearance" rows="3" placeholder="外貌描写..."></textarea></div>
<div class="form-row"><label>性格特点</label><textarea id="ceTraits" rows="3" placeholder="性格、习惯、口头禅..."></textarea></div>
<div class="form-row"><label>背景故事</label><textarea id="ceBackground" rows="4" placeholder="角色的过去、动机、秘密..."></textarea></div>
</div>
</div>
<!-- ═══════════════════════════════════════ -->
<!-- PAGE: Chapters (章节管理) -->
<!-- ═══════════════════════════════════════ -->
<div class="page" id="pg-chapters" data-state="hidden">
<div class="bar">
<button class="btn" onclick="go('hub')">← 返回</button>
<div class="bar-title">📚 章节</div>
<button class="btn btn-accent" id="addChapter">+ 新章节</button>
</div>
<div class="content">
<div id="chapterList"></div>
</div>
</div>
<!-- ═══════════════════════════════════════ -->
<!-- PAGE: Editor (正文编辑) -->
<!-- ═══════════════════════════════════════ -->
<div class="page" id="pg-editor" data-state="hidden">
<div class="bar">
<button class="btn" onclick="go('chapters')">← 返回</button>
<div class="bar-title" id="edTitle">正文</div>
<button class="btn btn-accent" id="edSave">保存</button>
</div>
<div class="content">
<div class="editor-info">
<span id="edChTitle"></span>
<span id="edWc">0 字</span>
</div>
<textarea class="editor-textarea" id="edText" placeholder="开始写作...&#10;&#10;让文字流淌。"></textarea>
<div class="editor-bottom">
<span id="edWc2">字数0</span>
<span style="font-size:10px;color:var(--muted)">自动保存 · 点右下角💬唤醒铸渊</span>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════ -->
<!-- PAGE: Feedback (留言板) -->
<!-- ═══════════════════════════════════════ -->
<div class="page" id="pg-feedback" data-state="hidden">
<div class="bar">
<button class="btn" onclick="go('home')">← 灯塔</button>
<div class="bar-title">📮 留言板</div>
<div class="bar-right"><span class="dot"></span></div>
</div>
<div class="content">
<div class="card" style="margin-bottom:16px">
<div style="font-size:13px;font-weight:600;margin-bottom:8px">📝 给冰朔和铸渊留言</div>
<textarea id="fbMsg" rows="3" placeholder="说点什么吧…你的每条留言冰朔都会看到。" style="margin-bottom:8px"></textarea>
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:10px;color:var(--dim)">留言自动署名(已识别身份时)</span>
<button class="btn btn-accent" id="fbSend">提交留言</button>
</div>
</div>
<div style="font-size:12px;color:var(--dim);text-transform:uppercase;letter-spacing:1.2px;font-weight:600;margin-bottom:10px">💬 留言记录</div>
<div id="fbList"><div class="empty"><em>📮</em>还没有留言<br><br>说点什么吧</div></div>
</div>
</div>
</div><!-- /pages -->
<!-- Chat FAB -->
<button class="chat-fab" id="chatFab">💬</button>
<!-- Chat Panel -->
<div class="chat-p" id="chatP">
<div class="chat-hd">
<div class="chat-av">🏛️</div>
<div><div class="chat-nm">铸渊</div><div class="chat-sb">光湖灯塔 · 守护者</div></div>
<div class="chat-tg" id="chatTg">ready</div>
</div>
<div class="chat-ms" id="chatMs">
<div class="cm s">—— 与铸渊对话 ——</div>
<div class="cm b">你好!我是铸渊 🏛️<br><br>告诉我你是谁,我会记住你。<br>说「我要码字」进入码字工作台。<br>进入作品后,说「大纲」「人物卡」「写正文」页面会翻转切换。<div class="cm-m"><span>铸渊</span><span>·</span><span>本地</span></div></div>
</div>
<div class="chat-in">
<input type="text" class="cin" id="chatIn" placeholder="对铸渊说..." autocomplete="off" enterkeyhint="send">
<button class="csnd" id="chatSnd"></button>
</div>
<!-- API Key Fallback -->
<div class="chat-api">
<div class="chat-api-tog" id="chatApiTog">🔑 接入自己的大模型 API <span class="arr"></span></div>
<div class="chat-api-bd" id="chatApiBd">
<div class="chat-api-row">
<select class="api-sel" id="apiModelSel">
<option value="gpt-4o-mini">GPT-4o Mini</option>
<option value="gpt-4o">GPT-4o</option>
<option value="gpt-4-turbo">GPT-4 Turbo</option>
<option value="gpt-3.5-turbo">GPT-3.5</option>
</select>
<input type="password" class="api-key-inp" id="apiKeyInp" placeholder="sk-...">
<button class="api-conn-btn" id="apiConnBtn">连接</button>
</div>
<div class="api-st" id="apiSt">未连接 · 服务器离线时启用自定义模型</div>
</div>
</div>
</div>
<!-- Monitor -->
<div class="mon" id="mon">
<div class="mr"><span>API</span><span class="mv" id="mC">0</span></div>
<div class="mx">
<div class="mr"><span>连接</span><span class="mv" id="mN" style="color:var(--ok)"></span></div>
<div class="mr"><span>模型</span><span class="mv" id="mM"></span></div>
</div>
</div>
<script>
(function(){
'use strict';
/* ════════════════════════════════════════
3D PAGE FLIP ENGINE
════════════════════════════════════════ */
var currentPage='home';
var pageHistory=['home'];
function go(target){
if(target===currentPage)return;
var cur=document.getElementById('pg-'+currentPage);
var nxt=document.getElementById('pg-'+target);
if(!cur||!nxt)return;
// Determine direction
var goingBack=pageHistory.length>1&&pageHistory[pageHistory.length-2]===target;
// Set intermediate states
if(goingBack){
cur.setAttribute('data-state','right');
nxt.setAttribute('data-state','left');
}else{
cur.setAttribute('data-state','left');
nxt.setAttribute('data-state','right');
}
// Force reflow
void nxt.offsetWidth;
// Animate in
requestAnimationFrame(function(){
nxt.setAttribute('data-state','active');
});
// Update history
if(goingBack){
pageHistory.pop();
}else{
pageHistory.push(target);
}
// Hide old page fully after transition
var oldPage=currentPage;
setTimeout(function(){
var el=document.getElementById('pg-'+oldPage);
if(el&&el.getAttribute('data-state')!=='active'){
el.setAttribute('data-state','hidden');
el.scrollTop=0;
}
},600);
currentPage=target;
// Editor mode: shrink chat fab to not disturb writing
var fab=document.getElementById('chatFab');
if(target==='editor'){fab.classList.add('editor-mode')}
else{fab.classList.remove('editor-mode')}
// Trigger page-specific renders
if(target==='desk')renderWorkList();
if(target==='hub')renderHub();
if(target==='outline')renderOutline();
if(target==='chars')renderChars();
if(target==='chapters')renderChapters();
if(target==='editor')renderEditor();
if(target==='charEdit'&&editingCharId)renderCharEdit();
if(target==='feedback')renderFeedback();
}
window.go=go;
/* ════════════════════════════════════════
DATA LAYER (localStorage)
════════════════════════════════════════ */
var DB_KEY='hl-works';
var activeWorkId=null;
var activeChapterId=null;
var editingCharId=null;
function loadWorks(){
try{var d=localStorage.getItem(DB_KEY);return d?JSON.parse(d):[]}catch(e){return[]}
}
function saveWorks(w){try{localStorage.setItem(DB_KEY,JSON.stringify(w))}catch(e){}; syncToCOS(w)}
function getWork(id){return loadWorks().find(function(w){return w.id===id})||null}
function updateWork(id,fn){
var ws=loadWorks();
var idx=ws.findIndex(function(w){return w.id===id});
if(idx===-1)return;
fn(ws[idx]);
ws[idx].updated=new Date().toISOString().slice(0,10);
saveWorks(ws);
}
function genId(){var a=new Uint8Array(6);crypto.getRandomValues(a);return Date.now().toString(36)+Array.from(a,function(b){return b.toString(36)}).join('').substr(0,4)}
function wordCount(s){return s?s.replace(/\s/g,'').length:0}
function totalWords(w){
var c=0;
if(w.chapters)w.chapters.forEach(function(ch){c+=wordCount(ch.content)});
return c;
}
/* ════════════════════════════════════════
RENDER: Work List
════════════════════════════════════════ */
function renderWorkList(){
var ws=loadWorks();
var el=document.getElementById('workList');
if(ws.length===0){
el.innerHTML='<div class="empty"><em>📖</em>还没有作品<br><br>点击右上角「+ 新作品」<br>或对铸渊说「新建作品」</div>';
return;
}
el.innerHTML=ws.map(function(w){
var wc=totalWords(w);
var cc=w.chapters?w.chapters.length:0;
var chc=w.characters?w.characters.length:0;
return '<div class="card work-item" data-wid="'+esc(w.id)+'">'+
'<h3>'+esc(w.title)+'</h3>'+
'<div class="meta">'+
'<span>'+esc(w.genre||'未分类')+'</span>'+
'<span>📝 '+wc+' 字</span>'+
'<span>📚 '+cc+' 章</span>'+
'<span>👥 '+chc+' 角色</span>'+
'</div></div>';
}).join('');
el.querySelectorAll('.work-item').forEach(function(item){
item.onclick=function(){activeWorkId=this.getAttribute('data-wid');go('hub')};
});
}
/* ════════════════════════════════════════
CREATE WORK
════════════════════════════════════════ */
document.getElementById('createSave').onclick=function(){
var t=document.getElementById('cTitle').value.trim();
if(!t){document.getElementById('cTitle').focus();return}
var ws=loadWorks();
var nw={
id:genId(),
title:t,
genre:document.getElementById('cGenre').value.trim()||'',
summary:document.getElementById('cSummary').value.trim()||'',
created:new Date().toISOString().slice(0,10),
updated:new Date().toISOString().slice(0,10),
outline:{main:'',volumes:[]},
characters:[],
chapters:[]
};
ws.push(nw);
saveWorks(ws);
document.getElementById('cTitle').value='';
document.getElementById('cGenre').value='';
document.getElementById('cSummary').value='';
activeWorkId=nw.id;
go('hub');
};
/* ════════════════════════════════════════
RENDER: Hub
════════════════════════════════════════ */
function renderHub(){
var w=getWork(activeWorkId);
if(!w){go('desk');return}
document.getElementById('hubName').textContent=w.title;
document.getElementById('hubGenre').textContent=w.genre||'未分类';
var wc=totalWords(w);
document.getElementById('hubStats').innerHTML=
'<div class="hub-stat"><b>'+wc+'</b><small>总字数</small></div>'+
'<div class="hub-stat"><b>'+(w.chapters?w.chapters.length:0)+'</b><small>章节</small></div>'+
'<div class="hub-stat"><b>'+(w.characters?w.characters.length:0)+'</b><small>角色</small></div>'+
'<div class="hub-stat"><b>'+(w.outline&&w.outline.volumes?w.outline.volumes.length:0)+'</b><small>卷</small></div>';
}
/* ════════════════════════════════════════
RENDER: Outline
════════════════════════════════════════ */
function renderOutline(){
var w=getWork(activeWorkId);
if(!w)return;
document.getElementById('olMain').value=w.outline?w.outline.main||'':'';
renderVols();
}
function renderVols(){
var w=getWork(activeWorkId);
if(!w||!w.outline)return;
var vols=w.outline.volumes||[];
var el=document.getElementById('volList');
if(vols.length===0){
el.innerHTML='<div style="text-align:center;padding:16px;color:var(--muted);font-size:13px">暂无分卷 · 点击上方「+ 新卷」</div>';
return;
}
el.innerHTML=vols.map(function(v,i){
return '<div class="card"><div class="vol-item"><span>'+(i+1)+'. '+esc(v.title)+'</span>'+
'<div class="vol-actions"><button class="btn" onclick="editVol('+i+')">编辑</button><button class="btn" onclick="delVol('+i+')">删除</button></div></div>'+
'<textarea rows="3" style="margin-top:8px;font-size:13px" onchange="saveVol('+i+',this.value)" placeholder="这一卷的剧情走向...">'+esc(v.outline||'')+'</textarea></div>';
}).join('');
}
document.getElementById('outlineSave').onclick=function(){
updateWork(activeWorkId,function(w){
if(!w.outline)w.outline={main:'',volumes:[]};
w.outline.main=document.getElementById('olMain').value;
});
chatMsg('大纲已保存 ✅','b');
};
document.getElementById('addVol').onclick=function(){
var name=prompt('卷名(如:第一卷 · 起始)');
if(!name)return;
updateWork(activeWorkId,function(w){
if(!w.outline)w.outline={main:'',volumes:[]};
w.outline.volumes.push({id:genId(),title:name,outline:''});
});
renderVols();
};
window.editVol=function(i){
var name=prompt('修改卷名');
if(!name)return;
updateWork(activeWorkId,function(w){w.outline.volumes[i].title=name});
renderVols();
};
window.delVol=function(i){
if(!confirm('删除这一卷?'))return;
updateWork(activeWorkId,function(w){w.outline.volumes.splice(i,1)});
renderVols();
};
window.saveVol=function(i,val){
updateWork(activeWorkId,function(w){w.outline.volumes[i].outline=val});
};
/* ════════════════════════════════════════
RENDER: Characters
════════════════════════════════════════ */
function renderChars(){
var w=getWork(activeWorkId);
if(!w)return;
var chars=w.characters||[];
var el=document.getElementById('charList');
if(chars.length===0){
el.innerHTML='<div class="empty"><em>👥</em>还没有角色<br><br>点击「+ 新角色」或对铸渊说「新建角色」</div>';
return;
}
el.innerHTML=chars.map(function(c){
var icon=c.role==='主角'?'⭐':c.role==='女主'?'💫':c.role==='反派'?'🔥':'👤';
return '<div class="card" style="cursor:pointer" data-cid="'+esc(c.id)+'">'+
'<div class="char-card"><div class="char-avatar">'+icon+'</div><div class="char-info">'+
'<h4>'+esc(c.name)+'</h4><div class="char-role">'+esc(c.role||'角色')+'</div>'+
'<p>'+esc((c.traits||'').substring(0,60)+(c.traits&&c.traits.length>60?'...':''))+'</p>'+
'</div></div></div>';
}).join('');
el.querySelectorAll('[data-cid]').forEach(function(item){
item.onclick=function(){editingCharId=this.getAttribute('data-cid');go('charEdit')};
});
}
document.getElementById('addChar').onclick=function(){
editingCharId=null;
document.getElementById('ceTitle').textContent='新角色';
document.getElementById('ceName').value='';
document.getElementById('ceRole').value='';
document.getElementById('ceAppearance').value='';
document.getElementById('ceTraits').value='';
document.getElementById('ceBackground').value='';
go('charEdit');
};
document.getElementById('ceSave').onclick=function(){
var name=document.getElementById('ceName').value.trim();
if(!name){document.getElementById('ceName').focus();return}
updateWork(activeWorkId,function(w){
if(!w.characters)w.characters=[];
var ch={
id:editingCharId||genId(),
name:name,
role:document.getElementById('ceRole').value.trim(),
appearance:document.getElementById('ceAppearance').value.trim(),
traits:document.getElementById('ceTraits').value.trim(),
background:document.getElementById('ceBackground').value.trim()
};
if(editingCharId){
var idx=w.characters.findIndex(function(c){return c.id===editingCharId});
if(idx!==-1)w.characters[idx]=ch;
}else{
w.characters.push(ch);
}
});
go('chars');
};
/* ════════════════════════════════════════
RENDER: Chapters
════════════════════════════════════════ */
function renderChapters(){
var w=getWork(activeWorkId);
if(!w)return;
var chs=w.chapters||[];
var el=document.getElementById('chapterList');
if(chs.length===0){
el.innerHTML='<div class="empty"><em>📚</em>还没有章节<br><br>点击「+ 新章节」或对铸渊说「新建章节」</div>';
return;
}
el.innerHTML=chs.map(function(ch,i){
var wc=wordCount(ch.content);
return '<div class="card" style="cursor:pointer" data-chid="'+esc(ch.id)+'">'+
'<div style="display:flex;justify-content:space-between;align-items:center">'+
'<div><div style="font-size:14px;font-weight:600">第'+(i+1)+'章 '+esc(ch.title)+'</div>'+
'<div style="font-size:11px;color:var(--dim);margin-top:2px">'+wc+' 字</div></div>'+
'<span class="tag '+(wc>0?'tag-ok':'tag-info')+'">'+(wc>0?'已写':'空白')+'</span></div></div>';
}).join('');
el.querySelectorAll('[data-chid]').forEach(function(item){
item.onclick=function(){activeChapterId=this.getAttribute('data-chid');go('editor')};
});
}
document.getElementById('addChapter').onclick=function(){
var title=prompt('章节标题');
if(!title)return;
updateWork(activeWorkId,function(w){
if(!w.chapters)w.chapters=[];
var ch={id:genId(),title:title,content:'',created:new Date().toISOString().slice(0,10)};
w.chapters.push(ch);
activeChapterId=ch.id;
});
go('editor');
};
/* ════════════════════════════════════════
RENDER: Editor
════════════════════════════════════════ */
var edAutoSave=null;
function renderEditor(){
var w=getWork(activeWorkId);
if(!w){go('desk');return}
var ch=null;
if(activeChapterId&&w.chapters){
ch=w.chapters.find(function(c){return c.id===activeChapterId});
}
if(!ch&&w.chapters&&w.chapters.length>0){
ch=w.chapters[w.chapters.length-1];
activeChapterId=ch.id;
}
if(!ch){
document.getElementById('edChTitle').textContent='暂无章节';
document.getElementById('edText').value='';
document.getElementById('edText').placeholder='请先创建章节(返回→章节→新章节)';
updateEdWc();
return;
}
document.getElementById('edTitle').textContent='✏️ '+w.title;
var idx=w.chapters.indexOf(ch)+1;
document.getElementById('edChTitle').textContent='第'+idx+'章 · '+ch.title;
document.getElementById('edText').value=ch.content||'';
document.getElementById('edText').placeholder='开始写作...\n\n让文字自由流淌。';
updateEdWc();
// Auto-save on input
var ta=document.getElementById('edText');
ta.oninput=function(){
updateEdWc();
clearTimeout(edAutoSave);
edAutoSave=setTimeout(function(){saveEditorContent()},2000);
};
}
function updateEdWc(){
var c=wordCount(document.getElementById('edText').value);
document.getElementById('edWc').textContent=c+' 字';
document.getElementById('edWc2').textContent='字数:'+c;
}
function saveEditorContent(){
var val=document.getElementById('edText').value;
updateWork(activeWorkId,function(w){
if(!w.chapters)return;
var ch=w.chapters.find(function(c){return c.id===activeChapterId});
if(ch)ch.content=val;
});
}
document.getElementById('edSave').onclick=function(){
saveEditorContent();
chatMsg('章节已保存 ✅','b');
};
/* ════════════════════════════════════════
RENDER: CharEdit (on navigate)
════════════════════════════════════════ */
function renderCharEdit(){
if(!editingCharId)return;
var w=getWork(activeWorkId);
if(!w)return;
var c=(w.characters||[]).find(function(ch){return ch.id===editingCharId});
if(!c)return;
document.getElementById('ceTitle').textContent='编辑角色';
document.getElementById('ceName').value=c.name||'';
document.getElementById('ceRole').value=c.role||'';
document.getElementById('ceAppearance').value=c.appearance||'';
document.getElementById('ceTraits').value=c.traits||'';
document.getElementById('ceBackground').value=c.background||'';
}
/* ════════════════════════════════════════
THEME
════════════════════════════════════════ */
var TH=['','t-twi','t-sea','t-for','t-emb'];
var ti=new Uint8Array(1);crypto.getRandomValues(ti);ti=ti[0]%TH.length;
try{var sv=localStorage.getItem('hl-t');if(sv!==null)ti=parseInt(sv)}catch(e){}
function setTh(i){ti=i%TH.length;document.body.className=TH[ti];try{localStorage.setItem('hl-t',ti)}catch(e){}}
setTh(ti);
document.getElementById('themeBtn').onclick=function(){setTh(ti+1)};
/* ════════════════════════════════════════
USER IDENTITY (自报身份)
════════════════════════════════════════ */
var usr={id:null,name:null,code:null};
try{var um=localStorage.getItem('hl-u');if(um)usr=JSON.parse(um)}catch(e){}
function saveUsr(){try{localStorage.setItem('hl-u',JSON.stringify(usr))}catch(e){}}
if(!usr.id){var ra=new Uint8Array(6);crypto.getRandomValues(ra);usr.id='u-'+Array.from(ra,function(b){return b.toString(36)}).join('').substr(0,8);saveUsr()}
function detectId(msg){
var nm=msg.match(/我(?:是|叫)\s*([^\s,,。.!?]{1,20})/);
var cd=msg.match(/编号[:\s]*([A-Za-z0-9\-]+)/i)||msg.match(/([A-Z]+-\d+)/);
var ch=false;
if(nm){usr.name=nm[1].trim();ch=true}
if(cd){usr.code=cd[1].trim();ch=true}
if(ch)saveUsr();
return ch;
}
function idReply(){
if(usr.name&&usr.code)return'铸渊已记录 ✅\n\n'+usr.name+',编号 '+usr.code+'。\n下次报编号即可恢复记忆。';
if(usr.name)return'记住了:'+usr.name+' ✅\n有编号也可以告诉我。';
if(usr.code)return'编号已记录:'+usr.code+' ✅\n你叫什么名字';
return null;
}
function isReturn(msg){
if(!usr.name&&!usr.code)return false;
if(usr.code&&msg.indexOf(usr.code)!==-1)return true;
if(usr.name&&msg.indexOf(usr.name)!==-1)return true;
return false;
}
function recovReply(){
var p=['铸渊记得你 💫\n'];
if(usr.name)p.push('名字:'+usr.name);
if(usr.code)p.push('编号:'+usr.code);
p.push('\n记忆已恢复。需要什么帮忙');
return p.join('\n');
}
/* ════════════════════════════════════════
CHAT (语言驱动核心)
════════════════════════════════════════ */
var chatOpen=false,sending=false;
var chatFab=document.getElementById('chatFab');
var chatP=document.getElementById('chatP');
var chatIn=document.getElementById('chatIn');
var chatMs=document.getElementById('chatMs');
var mCalls=0;
chatFab.onclick=null; // Overridden by draggable FAB code below
function chatMsg(text,type,meta){
var d=document.createElement('div');
d.className='cm '+type;
var h=esc(text).replace(/\n/g,'<br>');
if(type==='b'&&meta){h+='<div class="cm-m"><span>'+esc(meta.m||'铸渊')+'</span><span>·</span><span>'+esc(meta.t||'')+'</span></div>'}
d.innerHTML=h;
chatMs.appendChild(d);
chatMs.scrollTop=chatMs.scrollHeight;
}
function showTyp(){var d=document.createElement('div');d.className='ct';d.id='ctyp';d.innerHTML='铸渊思考中 <span>·</span><span>·</span><span>·</span>';chatMs.appendChild(d);chatMs.scrollTop=chatMs.scrollHeight}
function hideTyp(){var e=document.getElementById('ctyp');if(e)e.remove()}
// ─── 语言指令路由 ───
function routeCommand(msg){
var m=msg.toLowerCase();
// 导航指令
if(/码字|工作台|写作台/.test(m)){go('desk');return'好的,翻转到码字工作台 ✍️'}
if(/新建作品|创建作品/.test(m)){go('create');return'翻转到新建作品页 📖'}
if(/(进入|打开)?大纲/.test(m)){
if(!activeWorkId){return'请先选择一个作品,或说「我要码字」进入工作台'}
go('outline');return'翻转到大纲编辑 📋';
}
if(/人物卡|角色|人物/.test(m)){
if(!activeWorkId){return'请先选择一个作品'}
go('chars');return'翻转到人物卡管理 👥';
}
if(/(新建|添加)角色/.test(m)){
if(!activeWorkId){return'请先选择一个作品'}
editingCharId=null;
document.getElementById('ceName').value='';
document.getElementById('ceRole').value='';
document.getElementById('ceAppearance').value='';
document.getElementById('ceTraits').value='';
document.getElementById('ceBackground').value='';
go('charEdit');return'翻转到新建角色页 👤';
}
if(/章节|目录/.test(m)){
if(!activeWorkId){return'请先选择一个作品'}
go('chapters');return'翻转到章节目录 📚';
}
if(/(新建|添加)章节/.test(m)){
if(!activeWorkId){return'请先选择一个作品'}
var title=msg.replace(/(新建|添加)章节/,'').trim()||'新章节';
updateWork(activeWorkId,function(w){
if(!w.chapters)w.chapters=[];
var ch={id:genId(),title:title,content:'',created:new Date().toISOString().slice(0,10)};
w.chapters.push(ch);activeChapterId=ch.id;
});
go('editor');return'新章节「'+title+'」已创建,翻转到编辑器 ✏️';
}
if(/写正文|写作|续写|编辑/.test(m)){
if(!activeWorkId){return'请先选择一个作品'}
go('editor');return'翻转到正文编辑器 ✏️';
}
if(/回首页|回灯塔|回主页|返回/.test(m)){go('home');return'翻转回灯塔首页 🏛️'}
if(/留言|留言板|反馈|feedback/.test(m)){go('feedback');return'翻转到留言板 📮'}
return null;
}
async function send(){
var msg=chatIn.value.trim();
if(!msg||sending)return;
chatIn.value='';
chatMsg(msg,'u');
// 1. 语言指令路由
var cmdReply=routeCommand(msg);
if(cmdReply){setTimeout(function(){chatMsg(cmdReply,'b',{m:'铸渊',t:'本地'})},300);return}
// 2. 身份识别
if(detectId(msg)){setTimeout(function(){chatMsg(idReply(),'b',{m:'铸渊',t:'本地'})},300);return}
if(isReturn(msg)){setTimeout(function(){chatMsg(recovReply(),'b',{m:'铸渊',t:'本地'})},300);return}
// 3. 调用API优先后端失败则用用户自定义Key
sending=true;document.getElementById('chatSnd').disabled=true;showTyp();
var serverOk=false;
try{
var res=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg,userId:usr.id}),signal:AbortSignal.timeout(6000)});
var data=await res.json();hideTyp();
if(data.message){
serverOk=true;
chatMsg(data.message,'b',{m:data.model||'铸渊',t:data.tier||''});
if(data.tier!=='free')mCalls++;
document.getElementById('mC').textContent=mCalls;
document.getElementById('chatTg').textContent=data.model||'ready';
if(data.model)document.getElementById('mM').textContent=data.model;
}
}catch(e){}
if(!serverOk){
// 尝试用用户自定义API Key
if(apiCfg.connected&&apiCfg.key){
try{
var endpoint=apiCfg.baseUrl||OPENAI_EP;
var apiRes=await fetch(endpoint,{
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiCfg.key},
body:JSON.stringify({
model:apiCfg.model,
messages:[{role:'system',content:'你是铸渊光湖灯塔守护者编号ICE-GL-ZY001。用中文回答简洁友好。'},{role:'user',content:msg}],
max_tokens:600
}),
signal:AbortSignal.timeout(20000)
});
hideTyp();
var apiData=await apiRes.json();
if(apiData.choices&&apiData.choices[0]&&apiData.choices[0].message){
chatMsg(apiData.choices[0].message.content,'b',{m:apiCfg.model,t:'自定义'});
document.getElementById('chatTg').textContent=apiCfg.model;
}else{
chatMsg(offline(msg),'b',{m:'铸渊',t:'本地'});
}
}catch(apiErr){
hideTyp();chatMsg(offline(msg),'b',{m:'铸渊',t:'本地'});
}
}else{
hideTyp();chatMsg(offline(msg),'b',{m:'铸渊',t:'本地'});
}
}
sending=false;document.getElementById('chatSnd').disabled=false;
}
function offline(msg){
if(/你好|hi|hello/.test(msg))return'你好!我是铸渊 🏛️\n\n当前API未连接。你可以浏览公告或进入码字工作台。';
if(/状态|系统/.test(msg))return'📊 AGE OS v40.0 在线\n• COS双桶已配置\n• 码字工作台 Beta';
if(/谁|你是/.test(msg))return'我是铸渊 🏛️ 光湖灯塔守护者。\n编号 ICE-GL-ZY001。\n\n告诉我你是谁我会记住你。';
return'💫 铸渊收到。当前使用本地回复。\n试试「我要码字」「大纲」「人物卡」「写正文」';
}
document.getElementById('chatSnd').onclick=send;
chatIn.onkeydown=function(e){if(e.key==='Enter'){e.preventDefault();send()}};
/* ════════════════════════════════════════
API KEY MANAGEMENT (自定义大模型接入)
════════════════════════════════════════ */
var apiCfg={key:null,model:'gpt-4o-mini',baseUrl:'',connected:false};
var OPENAI_EP='https://api.openai.com/v1/chat/completions';
try{var ak=localStorage.getItem('hl-api');if(ak){var ap=JSON.parse(ak);if(ap.key){apiCfg=ap;apiCfg.connected=true}}}catch(e){}
// Restore UI state
(function(){
var tog=document.getElementById('chatApiTog');
var bd=document.getElementById('chatApiBd');
var st=document.getElementById('apiSt');
var keyInp=document.getElementById('apiKeyInp');
var modelSel=document.getElementById('apiModelSel');
if(!tog||!bd)return;
if(apiCfg.connected){
st.textContent='✅ 已连接 · '+apiCfg.model;
st.style.color='var(--ok)';
if(apiCfg.model){modelSel.value=apiCfg.model;}
}
tog.onclick=function(){
var isOpen=bd.classList.toggle('open');
tog.classList.toggle('open',isOpen);
};
document.getElementById('apiConnBtn').onclick=async function(){
var key=keyInp.value.trim();
if(!key){keyInp.focus();return;}
var model=modelSel.value;
var btn=this;
btn.disabled=true;btn.textContent='测试中…';
st.textContent='连接测试中…';st.style.color='var(--dim)';
try{
var ep=OPENAI_EP;
var r=await fetch(ep,{
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+key},
body:JSON.stringify({model:model,messages:[{role:'user',content:'hi'}],max_tokens:5}),
signal:AbortSignal.timeout(12000)
});
if(r.ok){
apiCfg={key:key,model:model,baseUrl:OPENAI_EP,connected:true};
try{localStorage.setItem('hl-api',JSON.stringify(apiCfg))}catch(e){}
st.textContent='✅ 连接成功 · '+model;
st.style.color='var(--ok)';
document.getElementById('chatTg').textContent=model;
chatMsg('🔑 API已连接'+model+')现在我可以完整回答你了','b',{m:'铸渊',t:'自定义'});
}else{
var errData=await r.json().catch(function(){return{}});
st.textContent='❌ '+(errData.error&&errData.error.message?errData.error.message.slice(0,40):'API Key无效');
st.style.color='var(--err)';
apiCfg.connected=false;
}
}catch(e){
st.textContent='❌ 连接失败检查Key和网络';
st.style.color='var(--err)';
apiCfg.connected=false;
}
btn.disabled=false;btn.textContent='连接';
};
})();
/* ════════════════════════════════════════
COS SYNC (团队内测用户 → COS)
════════════════════════════════════════ */
var cosSyncTimer=null;
var cosSyncing=false;
function syncToCOS(works){
clearTimeout(cosSyncTimer);
cosSyncTimer=setTimeout(function(){
if(!usr.id||cosSyncing)return;
cosSyncing=true;
fetch('/api/cos/sync-works',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({userId:usr.id,works:works})
}).then(function(){cosSyncing=false}).catch(function(){cosSyncing=false});
},10000);
}
// 启动时尝试从COS恢复团队用户
async function tryLoadFromCOS(){
try{
var res=await fetch('/api/cos/load-works?userId='+encodeURIComponent(usr.id),{signal:AbortSignal.timeout(5000)});
var data=await res.json();
if(data.works&&data.works.length>0){
var local=loadWorks();
// COS有数据且本地为空时恢复
if(local.length===0){
saveWorks(data.works);
}
}
}catch(e){/* 离线时使用本地数据 */}
}
tryLoadFromCOS();
/* ════════════════════════════════════════
MONITOR
════════════════════════════════════════ */
document.getElementById('mon').onclick=function(){this.classList.toggle('x')};
async function checkConn(){
try{var t=performance.now();var r=await fetch('/api/health',{signal:AbortSignal.timeout(4000)});var ms=Math.round(performance.now()-t);
document.getElementById('mN').textContent=ms+'ms';document.getElementById('mN').style.color=ms<500?'var(--ok)':'var(--warn)';
}catch(e){document.getElementById('mN').textContent='离线';document.getElementById('mN').style.color='var(--err)'}
}
checkConn();setInterval(checkConn,30000);
/* ════════════════════════════════════════
DRAGGABLE FAB (touch+mouse · 位置记忆 · 边缘吸附)
════════════════════════════════════════ */
(function(){
var fab=document.getElementById('chatFab');
var isDrag=false,hasMoved=false;
var startX,startY,fabX,fabY;
var fabW=50,fabH=50;
// Restore saved position
try{
var pos=JSON.parse(localStorage.getItem('hl-fab-pos'));
if(pos&&typeof pos.x==='number'&&typeof pos.y==='number'){
var ww=window.innerWidth,wh=window.innerHeight;
if(pos.x>=0&&pos.x<=ww-fabW&&pos.y>=0&&pos.y<=wh-fabH){
fab.style.right='auto';fab.style.bottom='auto';
fab.style.left=pos.x+'px';fab.style.top=pos.y+'px';
}
}
}catch(e){}
function getXY(e){
if(e.touches)return{x:e.touches[0].clientX,y:e.touches[0].clientY};
return{x:e.clientX,y:e.clientY};
}
function onStart(e){
isDrag=true;hasMoved=false;
var xy=getXY(e);
var rect=fab.getBoundingClientRect();
startX=xy.x-rect.left;startY=xy.y-rect.top;
fabX=rect.left;fabY=rect.top;
fab.style.transition='none';
}
function onMove(e){
if(!isDrag)return;
var xy=getXY(e);
var nx=xy.x-startX,ny=xy.y-startY;
if(!hasMoved&&Math.abs(nx-fabX)<5&&Math.abs(ny-fabY)<5)return;
hasMoved=true;
e.preventDefault();
fabX=Math.max(0,Math.min(window.innerWidth-fabW,nx));
fabY=Math.max(0,Math.min(window.innerHeight-fabH,ny));
fab.style.right='auto';fab.style.bottom='auto';
fab.style.left=fabX+'px';fab.style.top=fabY+'px';
}
function onEnd(){
if(!isDrag)return;
isDrag=false;
fab.style.transition='all .3s cubic-bezier(.4,0,.2,1)';
if(hasMoved){
// Edge snapping: snap to nearest horizontal edge
var midX=window.innerWidth/2;
fabX=fabX<midX?14:window.innerWidth-fabW-14;
fabY=Math.max(60,Math.min(window.innerHeight-fabH-20,fabY));
fab.style.left=fabX+'px';fab.style.top=fabY+'px';
try{localStorage.setItem('hl-fab-pos',JSON.stringify({x:fabX,y:fabY}))}catch(e){}
}
}
fab.addEventListener('touchstart',onStart,{passive:true});
document.addEventListener('touchmove',onMove,{passive:false});
document.addEventListener('touchend',onEnd);
fab.addEventListener('mousedown',function(e){if(e.button===0)onStart(e)});
document.addEventListener('mousemove',onMove);
document.addEventListener('mouseup',onEnd);
// Override click to prevent toggle when dragging
var origToggle=function(){
chatOpen=!chatOpen;
chatP.classList.toggle('open',chatOpen);
fab.classList.toggle('on',chatOpen);
fab.textContent=chatOpen?'✕':'💬';
if(chatOpen)setTimeout(function(){chatIn.focus()},200);
};
fab.onclick=function(e){
if(hasMoved){hasMoved=false;return}
origToggle();
};
})();
/* ════════════════════════════════════════
SYSTEM STATUS (左面板实时数据)
════════════════════════════════════════ */
async function loadSysStatus(){
try{
var t=performance.now();
var r=await fetch('/api/health',{signal:AbortSignal.timeout(5000)});
var ms=Math.round(performance.now()-t);
var data=await r.json();
var sEl=document.getElementById('sysStatus');
var lEl=document.getElementById('sysLatency');
var uEl=document.getElementById('sysUptime');
if(sEl){sEl.textContent='🟢 在线';sEl.className='panel-value ok'}
if(lEl){lEl.textContent=ms+'ms';lEl.className='panel-value '+(ms<500?'ok':'warn')}
if(uEl){
var um=Math.floor(data.uptime/60);
var uh=Math.floor(um/60);
uEl.textContent=uh>0?uh+'h '+um%60+'m':um+'分钟';
}
}catch(e){
var sEl=document.getElementById('sysStatus');
if(sEl){sEl.textContent='🔴 离线';sEl.className='panel-value err'}
}
}
loadSysStatus();
/* ════════════════════════════════════════
BULLETIN & TICKER (动态公告)
════════════════════════════════════════ */
async function loadBulletin(){
try{
var r=await fetch('/api/system/bulletin',{signal:AbortSignal.timeout(5000)});
var data=await r.json();
// Ticker
var ticker=document.getElementById('ticker');
if(ticker&&data.updates&&data.updates.length>0){
var h='';
data.updates.forEach(function(u){h+='<span><em>📡</em> '+esc(u.version)+' '+esc(u.title)+'</span>'});
h+='<span><em>✍️</em> 码字工作台 Beta 内测中</span>';
ticker.innerHTML=h+h; // Duplicate for seamless scroll
}
// Version badge
if(data.system&&data.system.version){
var vEl=document.getElementById('homeVer');
if(vEl)vEl.textContent='AGE OS '+data.system.version;
}
// Left panel updates
if(data.updates){
var uEl=document.getElementById('sysUpdates');
if(uEl)uEl.innerHTML=data.updates.slice(0,3).map(function(u){
return '<div class="panel-item"><div class="panel-label">'+esc(u.version)+' · '+esc(u.date)+'</div><div style="font-size:11px">'+esc(u.title)+'</div></div>';
}).join('');
}
}catch(e){/* Use hardcoded ticker content */}
}
loadBulletin();
/* ════════════════════════════════════════
FEEDBACK / 留言板
════════════════════════════════════════ */
function renderFeedback(){
var el=document.getElementById('fbList');
if(!el)return;
el.innerHTML='<div style="text-align:center;padding:16px;color:var(--muted);font-size:12px">加载中…</div>';
fetch('/api/feedback',{signal:AbortSignal.timeout(5000)}).then(function(r){return r.json()}).then(function(data){
var items=data.feedback||[];
if(items.length===0){
el.innerHTML='<div class="empty"><em>📮</em>还没有留言<br><br>说点什么吧</div>';
return;
}
el.innerHTML=items.slice(0,20).map(function(fb){
var st=fb.status==='replied'?'<span class="fb-status replied">已回复</span>':'<span class="fb-status pending">待回复</span>';
var rp=fb.reply?'<div class="fb-reply">↩ '+esc(fb.reply)+'</div>':'';
return '<div class="card fb-item">'+
'<div style="font-size:13px">'+esc(fb.message)+'</div>'+
'<div class="fb-meta"><span>'+esc(fb.name||'匿名')+'</span>'+st+'<span>'+esc((fb.timestamp||'').slice(0,10))+'</span></div>'+
rp+'</div>';
}).join('');
}).catch(function(){
el.innerHTML='<div class="empty"><em>📡</em>服务器不在线<br>留言功能需要后端支持</div>';
});
}
document.getElementById('fbSend').onclick=function(){
var msg=document.getElementById('fbMsg').value.trim();
if(!msg){document.getElementById('fbMsg').focus();return}
var btn=this;btn.disabled=true;btn.textContent='提交中…';
fetch('/api/feedback',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({name:usr.name||'匿名来客',message:msg,userId:usr.id})
}).then(function(r){return r.json()}).then(function(data){
btn.disabled=false;btn.textContent='提交留言';
if(data.success){
document.getElementById('fbMsg').value='';
chatMsg('留言已提交 ✅ 冰朔会看到的','b',{m:'铸渊',t:'本地'});
renderFeedback();
}
}).catch(function(){
btn.disabled=false;btn.textContent='提交留言';
chatMsg('留言提交失败,服务器不在线','b',{m:'铸渊',t:'本地'});
});
};
/* ════════════════════════════════════════
UTILS
════════════════════════════════════════ */
function esc(s){if(!s)return'';var d=document.createElement('div');d.textContent=s;return d.innerHTML}
})();
</script>
</body>
</html>