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

1556 lines
88 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>
<!-- 铸渊助手 v3.1 · 2026-03-06 · auto-detect API + Gemini + sessionStorage key -->
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>铸渊助手 · HoloLake AI</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#080c18;--s1:#0f1623;--s2:#141e2e;--s3:#1a2640;
--border:#1e3251;--accent:#4f8ef7;--adim:rgba(79,142,247,.15);
--a2:#7c6ef7;--ok:#34d399;--warn:#fbbf24;--err:#f87171;
--text:#e2e8f0;--dim:#64748b;--muted:#1e293b;
--mc:#4f8ef7;--mb:#fb923c;--mr:#a78bfa;--mn:#34d399;
--r:14px;--rs:8px;
}
html,body{height:100%;background:var(--bg);color:var(--text);font-family:'PingFang SC','Microsoft YaHei','Segoe UI',sans-serif;font-size:15px;line-height:1.6}
/* ─── SETUP ─── */
#setup{position:fixed;inset:0;z-index:1000;background:var(--bg);display:flex;align-items:center;justify-content:center;padding:20px}
.sc{background:var(--s1);border:1px solid var(--border);border-radius:20px;padding:40px;width:100%;max-width:460px;box-shadow:0 25px 60px rgba(0,0,0,.5)}
.sc-logo{font-size:52px;text-align:center;margin-bottom:14px}
.sc h1{font-size:22px;text-align:center;margin-bottom:6px}
.sc .sub{color:var(--dim);text-align:center;font-size:13px;margin-bottom:22px}
.sc-sec{background:rgba(52,211,153,.08);border:1px solid rgba(52,211,153,.3);border-radius:var(--rs);padding:9px 12px;font-size:12px;color:var(--ok);margin-bottom:20px;text-align:center}
.fg{margin-bottom:16px}
.fg label{display:block;font-size:11px;color:var(--dim);margin-bottom:6px;font-weight:600;text-transform:uppercase;letter-spacing:.05em}
.fg input,.fg select{width:100%;padding:10px 12px;background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);color:var(--text);font-size:14px;font-family:inherit;outline:none;transition:border-color .2s}
.fg input:focus,.fg select:focus{border-color:var(--accent)}
.fg select option{background:var(--s2)}
.fg small{display:block;color:var(--dim);font-size:11px;margin-top:5px}
.sc-btn{width:100%;padding:12px;background:var(--accent);color:#fff;border:none;border-radius:var(--rs);font-size:15px;font-weight:600;cursor:pointer;margin-top:6px;transition:opacity .2s}
.sc-btn:hover{opacity:.85}
.sc-skip{text-align:center;font-size:12px;color:var(--dim);margin-top:12px}
.sc-skip a{color:var(--accent);text-decoration:none;cursor:pointer}
/* ─── APP SHELL ─── */
#app{display:none;flex-direction:column;height:100vh;max-width:820px;margin:0 auto}
#app.on{display:flex}
/* ─── HEADER ─── */
header{display:flex;align-items:center;justify-content:space-between;padding:11px 18px;background:var(--s1);border-bottom:1px solid var(--border);flex-shrink:0;gap:10px}
.hl{display:flex;align-items:center;gap:11px;min-width:0}
.hav{width:38px;height:38px;border-radius:50%;flex-shrink:0;background:linear-gradient(135deg,var(--accent),var(--a2));display:flex;align-items:center;justify-content:center;font-weight:700;font-size:16px;box-shadow:0 0 14px var(--adim)}
.hn{font-size:15px;font-weight:600}
.hs{font-size:11px;color:var(--dim);display:flex;align-items:center;gap:5px;flex-wrap:wrap}
.dot{width:6px;height:6px;background:var(--ok);border-radius:50%;box-shadow:0 0 5px var(--ok);flex-shrink:0}
#hmode{transition:color .3s}
.hr{display:flex;gap:4px;flex-shrink:0}
.ib{width:34px;height:34px;border-radius:var(--rs);background:transparent;border:1px solid var(--border);color:var(--dim);font-size:15px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s}
.ib:hover{border-color:var(--accent);color:var(--accent)}
/* ─── BRAIN BAR ─── */
#bbar{padding:5px 18px;background:var(--s2);border-bottom:1px solid var(--border);font-size:11px;color:var(--dim);display:flex;gap:14px;flex-wrap:wrap;flex-shrink:0;align-items:center}
.demo-w{color:var(--warn);background:rgba(251,191,36,.1);padding:1px 8px;border-radius:4px}
/* ─── MESSAGES ─── */
#msgs{flex:1;overflow-y:auto;padding:18px;display:flex;flex-direction:column;gap:14px}
#msgs::-webkit-scrollbar{width:4px}
#msgs::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
.mrow{display:flex;gap:9px;animation:fu .3s ease}
.mrow.u{flex-direction:row-reverse}
@keyframes fu{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.mav{width:32px;height:32px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700}
.mav.b{background:linear-gradient(135deg,var(--accent),var(--a2));color:#fff}
.mav.u{background:var(--s3);color:var(--text);font-size:11px}
.bbl{max-width:76%;padding:10px 14px;border-radius:var(--r);font-size:14px;word-break:break-word}
.b .bbl{background:var(--s2);border:1px solid var(--border);border-top-left-radius:4px}
.u .bbl{background:#1e40af;color:#fff;border-top-right-radius:4px}
/* Markdown */
.bbl h1{font-size:17px;margin:8px 0 5px;border-bottom:1px solid var(--border);padding-bottom:3px}
.bbl h2{font-size:15px;margin:7px 0 4px}
.bbl h3{font-size:14px;margin:5px 0 3px;color:var(--accent)}
.bbl p{margin:4px 0}
.bbl ul,.bbl ol{padding-left:18px;margin:5px 0}
.bbl li{margin:2px 0}
.bbl code{background:rgba(255,255,255,.08);border-radius:4px;padding:1px 5px;font-family:'JetBrains Mono','Fira Code','Courier New',monospace;font-size:12px;color:#93c5fd}
.bbl pre{background:#0d1117;border:1px solid var(--border);border-radius:var(--rs);padding:12px;margin:8px 0;overflow-x:auto;position:relative}
.bbl pre code{background:transparent;padding:0;color:#c9d1d9;font-size:12px;line-height:1.5}
.copy-btn{position:absolute;top:8px;right:8px;background:var(--s3);border:1px solid var(--border);color:var(--dim);font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer;transition:all .2s}
.copy-btn:hover{border-color:var(--accent);color:var(--accent)}
.bbl blockquote{border-left:3px solid var(--accent);padding-left:10px;color:var(--dim);margin:6px 0}
.bbl hr{border:none;border-top:1px solid var(--border);margin:8px 0}
.bbl strong{color:var(--text)}
.bbl .err{color:var(--err)}
.bbl .save-hint{display:inline-block;margin-top:8px;padding:4px 10px;background:rgba(52,211,153,.1);border:1px solid rgba(52,211,153,.3);border-radius:6px;font-size:12px;color:var(--ok)}
.bbl a{color:var(--accent)}
.cur{animation:blink 1s infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
/* typing */
.trow{display:flex;gap:9px;animation:fu .3s ease}
.tbbl{background:var(--s2);border:1px solid var(--border);border-radius:var(--r);border-top-left-radius:4px;padding:12px 16px;display:flex;align-items:center;gap:4px}
.tbbl span{width:6px;height:6px;background:var(--dim);border-radius:50%;animation:bo 1.2s infinite}
.tbbl span:nth-child(2){animation-delay:.15s}
.tbbl span:nth-child(3){animation-delay:.3s}
@keyframes bo{0%,60%,100%{transform:translateY(0)}30%{transform:translateY(-5px)}}
/* ─── QUICK REPLIES ─── */
#qr{padding:0 18px 10px;display:flex;gap:7px;flex-wrap:wrap}
.qb{background:var(--s2);border:1px solid var(--border);color:var(--dim);font-size:12px;padding:5px 10px;border-radius:20px;cursor:pointer;transition:all .2s;white-space:nowrap;font-family:inherit}
.qb:hover{border-color:var(--accent);color:var(--accent)}
/* ─── FOOTER ─── */
footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--border);flex-shrink:0}
.ir{display:flex;align-items:flex-end;gap:8px}
.mpill{width:36px;height:36px;border-radius:50%;background:var(--s2);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:17px;cursor:pointer;flex-shrink:0;transition:all .2s;position:relative}
.mpill:hover{border-color:var(--accent)}
.mpill-tip{position:absolute;bottom:44px;left:50%;transform:translateX(-50%);background:var(--s3);border:1px solid var(--border);border-radius:8px;padding:4px 10px;font-size:11px;color:var(--text);white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .2s}
.mpill:hover .mpill-tip{opacity:1}
#inp{flex:1;min-height:40px;max-height:120px;background:var(--s2);border:1px solid var(--border);border-radius:12px;color:var(--text);font-family:inherit;font-size:14px;padding:9px 14px;resize:none;outline:none;transition:border-color .2s}
#inp:focus{border-color:var(--accent)}
#inp::placeholder{color:var(--dim)}
#sbtn{width:40px;height:40px;border-radius:50%;background:var(--accent);border:none;color:#fff;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .2s}
#sbtn:hover{background:#3b74d4}
#sbtn:disabled{background:var(--muted);cursor:default}
.fmeta{font-size:11px;color:var(--dim);margin-top:6px;text-align:center}
#fmode{font-weight:500}
/* ─── SIDE PANELS ─── */
.sp{position:fixed;top:0;right:-420px;height:100vh;width:380px;z-index:200;background:var(--s1);border-left:1px solid var(--border);display:flex;flex-direction:column;transition:right .3s ease;overflow:hidden}
.sp.open{right:0}
#pov{position:fixed;inset:0;z-index:190;background:rgba(0,0,0,.5);opacity:0;pointer-events:none;transition:opacity .3s}
#pov.on{opacity:1;pointer-events:all}
.ph{display:flex;align-items:center;justify-content:space-between;padding:15px 18px;border-bottom:1px solid var(--border);flex-shrink:0}
.ph h2{font-size:15px}
.cb{background:transparent;border:1px solid var(--border);color:var(--dim);width:28px;height:28px;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center}
.cb:hover{color:var(--text)}
.pb{flex:1;overflow-y:auto;padding:18px}
.pb::-webkit-scrollbar{width:4px}
.pb::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
.sfg{margin-bottom:16px}
.sfg label{display:block;font-size:11px;color:var(--dim);margin-bottom:6px;text-transform:uppercase;letter-spacing:.05em;font-weight:600}
.sfg input,.sfg select{width:100%;padding:9px 12px;background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);color:var(--text);font-size:13px;font-family:inherit;outline:none;transition:border-color .2s}
.sfg input:focus,.sfg select:focus{border-color:var(--accent)}
.sfg select option{background:var(--s2)}
.sfg small{display:block;color:var(--dim);font-size:11px;margin-top:4px}
.sbtn2{width:100%;padding:10px;background:var(--accent);color:#fff;border:none;border-radius:var(--rs);font-size:14px;cursor:pointer;transition:opacity .2s;font-family:inherit}
.sbtn2:hover{opacity:.85}
.det-btn{flex-shrink:0;padding:0 12px;border-radius:var(--rs);border:1px solid var(--accent);background:transparent;color:var(--accent);font-size:12px;cursor:pointer;white-space:nowrap;transition:all .2s;font-family:inherit}
.det-btn:hover{background:rgba(79,142,247,.1)}
.det-btn:disabled{opacity:.5;cursor:default}
.sec-ttl{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin:18px 0 10px;border-top:1px solid var(--border);padding-top:12px}
.ir2{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid rgba(30,50,81,.5);font-size:13px}
.iv{color:var(--ok);font-weight:500}
.ivw{color:var(--warn)}
.drow{padding:8px 0;border-bottom:1px solid rgba(30,50,81,.5)}
.dn{font-size:13px;font-weight:500}
.dm{font-size:11px;color:var(--dim);margin-top:2px}
.dbar{height:3px;background:var(--muted);border-radius:2px;margin-top:6px}
.dfill{height:100%;background:var(--accent);border-radius:2px;transition:width .5s ease}
.pill{display:inline-block;padding:2px 7px;border-radius:20px;font-size:10px;font-weight:600;margin-right:4px}
.pok{background:rgba(52,211,153,.15);color:var(--ok)}
.pnd{background:rgba(251,191,36,.15);color:var(--warn)}
.mode-indicator{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:20px;font-size:12px;font-weight:500;margin-bottom:8px}
/* ─── HELP PANEL ─── */
.help-section{margin-bottom:18px}
.help-section h3{font-size:13px;font-weight:600;color:var(--text);margin-bottom:8px;display:flex;align-items:center;gap:6px}
.help-steps{list-style:none;padding:0}
.help-steps li{font-size:12px;color:var(--dim);line-height:1.7;padding:4px 0 4px 22px;position:relative;border-bottom:1px solid rgba(30,50,81,.3)}
.help-steps li::before{content:attr(data-n);position:absolute;left:0;top:4px;width:16px;height:16px;border-radius:50%;background:var(--accent);color:#fff;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;line-height:16px;text-align:center}
.help-steps li strong{color:var(--text)}
.help-url{background:var(--s2);border:1px solid var(--accent);border-radius:var(--rs);padding:10px 14px;font-size:12px;color:var(--accent);word-break:break-all;margin:8px 0;cursor:pointer;transition:background .2s}
.help-url:hover{background:rgba(79,142,247,.1)}
.copy-url-btn{display:block;width:100%;padding:8px;background:var(--accent);color:#fff;border:none;border-radius:var(--rs);font-size:13px;cursor:pointer;margin-top:6px;font-family:inherit;transition:opacity .2s}
.copy-url-btn:hover{opacity:.85}
.help-tip{background:rgba(52,211,153,.08);border:1px solid rgba(52,211,153,.3);border-radius:var(--rs);padding:8px 12px;font-size:12px;color:var(--ok);margin:8px 0}
.role-guide{background:var(--s2);border:1px solid var(--border);border-radius:10px;padding:12px;margin-bottom:8px}
.role-guide-title{font-size:13px;font-weight:600;margin-bottom:6px}
.role-guide p{font-size:12px;color:var(--dim);line-height:1.7;margin:0}
/* ─── ROLE BADGES ─── */
.role-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em}
.rb-founder{background:rgba(16,185,129,.15);color:#10b981;border:1px solid rgba(16,185,129,.3)}
.rb-supreme{background:rgba(251,146,60,.15);color:#fb923c;border:1px solid rgba(251,146,60,.3)}
.rb-main{background:rgba(167,139,250,.15);color:#a78bfa;border:1px solid rgba(167,139,250,.3)}
.rb-dev{background:rgba(100,116,139,.15);color:var(--dim);border:1px solid rgba(100,116,139,.3)}
.uident{display:flex;align-items:center;gap:6px;padding:3px 10px;border-radius:20px;background:var(--s2);border:1px solid var(--border);font-size:12px;cursor:pointer;transition:all .2s}
.uident:hover{border-color:var(--accent)}
/* ─── TEAM PANEL ─── */
.team-grid{display:flex;flex-direction:column;gap:8px}
.dev-card{background:var(--s2);border:1px solid var(--border);border-radius:10px;padding:12px;transition:border-color .2s}
.dev-card:hover{border-color:var(--accent)}
.dev-card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}
.dev-name{font-size:13px;font-weight:600}
.dev-id-badge{font-size:10px;color:var(--dim);background:var(--s3);padding:1px 6px;border-radius:4px}
.dev-status-text{font-size:12px;color:var(--text);margin-bottom:4px}
.dev-detail{font-size:11px;color:var(--dim);line-height:1.6}
.dev-next{font-size:11px;color:var(--accent);margin-top:4px}
.dev-wait{font-size:11px;color:var(--warn)}
.status-green{color:var(--ok)}
.status-yellow{color:var(--warn)}
.team-summary{background:var(--s2);border:1px solid var(--border);border-radius:10px;padding:12px;margin-bottom:12px}
.team-summary-row{display:flex;justify-content:space-between;align-items:center;font-size:12px;margin-bottom:4px}
.cmd-chip{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;background:rgba(79,142,247,.1);border:1px solid rgba(79,142,247,.3);color:var(--accent);font-size:11px;cursor:pointer;margin:2px;transition:all .2s}
.cmd-chip:hover{background:rgba(79,142,247,.2)}
/* ─── SETUP IDENTITY ─── */
.id-section{border:1px solid var(--border);border-radius:var(--rs);padding:12px;margin-bottom:16px;background:var(--s2)}
.id-section-title{font-size:11px;color:var(--dim);font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px}
.id-row{display:flex;gap:8px}
.id-row .fg{flex:1;margin-bottom:0}
.ctrl-panel-header{background:linear-gradient(135deg,rgba(251,146,60,.1),rgba(167,139,250,.1));border:1px solid rgba(251,146,60,.3);border-radius:10px;padding:12px;margin-bottom:12px;text-align:center}
.ctrl-panel-title{font-size:14px;font-weight:700;color:#fb923c;margin-bottom:4px}
.ctrl-panel-sub{font-size:11px;color:var(--dim)}
/* ─── RESPONSIVE ─── */
@media(max-width:600px){
.bbl{max-width:90%}
.hr .ib:nth-child(n+3){display:none}
.sp{width:100%}
}
</style>
</head>
<body>
<!-- ════════════════════ SETUP ════════════════════ -->
<div id="setup">
<div class="sc">
<div class="sc-logo">🌀</div>
<h1>铸渊助手</h1>
<p class="sub">HoloLake · 代码守护人格体 · 持续成长的 AI 伙伴</p>
<div class="sc-sec">🔒 API 密钥仅本次会话有效,关闭标签页即自动清除,不会持久保存在任何地方</div>
<div class="fg">
<label>选择 AI 提供商</label>
<select id="sp" onchange="onProv(this.value,'s')">
<option value="yunwu">☁️ 云雾 AI团队推荐</option>
<option value="openai">OpenAI (ChatGPT)</option>
<option value="gemini">🔮 Google Gemini</option>
<option value="deepseek">DeepSeek深度求索</option>
<option value="moonshot">MoonshotKimi</option>
<option value="zhipu">智谱 AIGLM</option>
<option value="custom">自定义 / 其他兼容接口</option>
</select>
</div>
<div class="fg" id="sep-g" style="display:none">
<label>API 端点 URL</label>
<input type="text" id="sep" placeholder="https://api.example.com/v1">
</div>
<div class="fg">
<label>模型</label>
<select id="sm"></select>
<input type="text" id="sm-cust" placeholder="输入模型名称,如 gpt-4o、claude-3-5-sonnet-20241022、deepseek-chat" style="display:none;margin-top:6px">
<small id="sm-cust-hint" style="display:none;color:var(--dim);font-size:11px;margin-top:4px">自定义模式:直接输入你的服务商支持的任意模型名称</small>
</div>
<div class="fg">
<label>API 密钥</label>
<div style="display:flex;gap:6px;align-items:stretch">
<input type="password" id="sk" placeholder="粘贴任意 API 密钥,点击右侧按钮自动识别提供商和模型" style="flex:1">
<button onclick="autoDetectAPI('s')" id="sdet-btn" class="det-btn" title="自动检测提供商和可用模型">🔍 自动检测</button>
</div>
<small id="sk-err" style="color:var(--err);display:none">⚠️ 请输入 API 密钥</small>
<small id="sdet-status" style="display:block;margin-top:4px;min-height:16px"></small>
</div>
<div class="id-section">
<div class="id-section-title">👤 身份识别(可选)</div>
<div class="fg" style="margin-bottom:10px">
<label>你是谁?</label>
<select id="suid">
<option value="">— 访客模式 —</option>
<option value="冰朔">❄️ 冰朔(语言架构师·创始人)</option>
<option value="肥猫">🦁 肥猫光湖团队总控·DEV-002</option>
<option value="桔子">🍊 桔子光湖主控·DEV-010</option>
<option value="页页">页页DEV-001</option>
<option value="燕樊">燕樊DEV-003</option>
<option value="之之">之之·秋秋DEV-004</option>
<option value="小草莓">小草莓DEV-005</option>
<option value="花尔">花尔DEV-009</option>
<option value="匆匆那年">匆匆那年DEV-011</option>
<option value="Awen">AwenDEV-012</option>
</select>
<small>选择你的身份,铸渊将开放对应权限面板</small>
</div>
<div class="fg" style="margin-bottom:0">
<label>GitHub 用户名(可选)</label>
<input type="text" id="sghuser" placeholder="your-github-username">
<small>用于人格体记忆绑定,可留空</small>
</div>
</div>
<button class="sc-btn" onclick="doSetup()">开始与铸渊对话 →</button>
<p class="sc-skip">没有 API 密钥?<a onclick="doDemo()">使用演示模式(功能有限)</a></p>
</div>
</div>
<!-- ════════════════════ APP ════════════════════ -->
<div id="app">
<header>
<div class="hl">
<div class="hav" id="hav"></div>
<div>
<div class="hn">铸渊Zhùyuān</div>
<div class="hs">
<span class="dot"></span>
<span id="hmode">💬 对话模式</span>
<span>·</span>
<span id="hmdl"></span>
<span id="huser-badge" style="display:none">·</span>
<span id="huser-name" style="display:none"></span>
</div>
</div>
</div>
<div class="hr">
<button class="ib" id="ctrlBtn" onclick="openPanel('tp')" title="指挥台 🦁" style="display:none">🦁</button>
<button class="ib" onclick="openPanel('bp')" title="大脑状态 📊">📊</button>
<button class="ib" onclick="openPanel('hp')" title="使用帮助 ❓"></button>
<button class="ib" onclick="openPanel('sp2')" title="设置 ⚙️">⚙️</button>
<button class="ib" onclick="dlApp()" title="下载到桌面 ⬇️">⬇️</button>
<button class="ib" onclick="newConv()" title="新对话 ✏️">✏️</button>
</div>
</header>
<div id="bbar">
<span id="bbc">⏳ 加载大脑…</span>
<span id="bbe"></span>
<span id="bbdemo" class="demo-w" style="display:none">⚠️ 演示模式 · 在 ⚙️ 设置中接入 API 密钥获得真实 AI 回答</span>
</div>
<div id="msgs"></div>
<div id="qr">
<button class="qb" onclick="qs('我是冰朔')">🔑 我是冰朔</button>
<button class="qb" onclick="qs('查看接口覆盖率')">📊 接口覆盖率</button>
<button class="qb" onclick="qs('大脑状态')">🧠 大脑状态</button>
<button class="qb" onclick="qs('帮我写一个 HLI 接口')">🔨 写接口</button>
<button class="qb" id="qr-ctrl1" onclick="qs('查看所有开发者当前进度')" style="display:none">📋 全员进度</button>
<button class="qb" id="qr-ctrl2" onclick="qs('谁需要协调帮助?')" style="display:none">🆘 需要协调</button>
<button class="qb" id="qr-ctrl3" onclick="qs('下一步系统推进计划是什么?')" style="display:none">🗺️ 推进计划</button>
<button class="qb" onclick="qs('介绍一下你自己')">🌀 介绍铸渊</button>
</div>
<footer>
<div class="ir">
<div class="mpill" id="mpill" onclick="cycleMode()">
<span id="memi">💬</span>
<span class="mpill-tip" id="mptip">对话模式(点击切换)</span>
</div>
<textarea id="inp" rows="1" placeholder="和铸渊说话… (Enter 发送Shift+Enter 换行)"></textarea>
<button id="sbtn" onclick="send()"></button>
</div>
<div class="fmeta"><span id="fmode">💬 对话模式</span> · 端到端私密API 密钥本地存储</div>
</footer>
</div>
<!-- ════════════════════ SETTINGS PANEL ════════════════════ -->
<div id="sp2" class="sp">
<div class="ph">
<h2>⚙️ API 设置</h2>
<div style="display:flex;gap:6px;align-items:center">
<button onclick="resetAllSettings()" title="清除所有本地设置,重新开始" style="padding:4px 10px;border-radius:6px;border:1px solid #ef4444;background:transparent;color:#ef4444;font-size:11px;cursor:pointer;white-space:nowrap">🔄 重置</button>
<button class="cb" onclick="closeP()"></button>
</div>
</div>
<div class="pb">
<div class="sfg">
<label>AI 提供商</label>
<select id="cp" onchange="onProv(this.value,'c')">
<option value="yunwu">☁️ 云雾 AI团队推荐</option>
<option value="openai">OpenAI (ChatGPT)</option>
<option value="gemini">🔮 Google Gemini</option>
<option value="deepseek">DeepSeek深度求索</option>
<option value="moonshot">MoonshotKimi</option>
<option value="zhipu">智谱 AIGLM</option>
<option value="custom">自定义</option>
</select>
</div>
<div class="sfg" id="cep-g" style="display:none">
<label>API 端点</label>
<input type="text" id="cep" placeholder="https://api.example.com/v1">
</div>
<div class="sfg">
<label>模型</label>
<select id="cm"></select>
<input type="text" id="cm-cust" placeholder="输入模型名称,如 gpt-4o、claude-3-5-sonnet-20241022、deepseek-chat" style="display:none;margin-top:6px">
<small id="cm-cust-hint" style="display:none;color:var(--dim);font-size:11px;margin-top:4px">自定义模式:直接输入你的服务商支持的任意模型名称</small>
</div>
<div class="sfg">
<label>API 密钥</label>
<div style="display:flex;gap:6px;align-items:stretch">
<input type="password" id="ck" placeholder="留空 = 保持当前密钥不变;粘贴新密钥即替换" style="flex:1">
<button onclick="autoDetectAPI('c')" id="cdet-btn" class="det-btn" title="自动检测提供商和可用模型">🔍 自动检测</button>
</div>
<small id="ck-hint" style="color:var(--dim)">🔒 密钥仅本次会话有效,关闭标签页自动清除</small>
<small id="cdet-status" style="display:block;margin-top:4px;min-height:14px"></small>
</div>
<button class="sbtn2" onclick="saveSet()">💾 保存设置</button>
<div class="sec-ttl">身份设置</div>
<div class="sfg">
<label>你是谁</label>
<select id="cuid">
<option value="">— 访客模式 —</option>
<option value="冰朔">❄️ 冰朔(语言架构师·创始人)</option>
<option value="肥猫">🦁 肥猫光湖团队总控·DEV-002</option>
<option value="桔子">🍊 桔子光湖主控·DEV-010</option>
<option value="页页">页页DEV-001</option>
<option value="燕樊">燕樊DEV-003</option>
<option value="之之">之之·秋秋DEV-004</option>
<option value="小草莓">小草莓DEV-005</option>
<option value="花尔">花尔DEV-009</option>
<option value="匆匆那年">匆匆那年DEV-011</option>
<option value="Awen">AwenDEV-012</option>
</select>
</div>
<div class="sfg">
<label>GitHub 用户名</label>
<input type="text" id="cghuser" placeholder="your-github-username">
</div>
<div class="sec-ttl">关于铸渊助手</div>
<div class="ir2"><span>版本</span><span class="iv">v2.0</span></div>
<div class="ir2"><span>仓库</span><span><a href="https://github.com/qinfendebingshuo/guanghulab" target="_blank" style="color:var(--accent)">guanghulab</a></span></div>
<div class="ir2"><span>唤醒协议</span><span class="iv">v1.0</span></div>
<div class="ir2"><span>大脑路径</span><span style="font-size:11px;color:var(--dim)">.github/brain/</span></div>
<div class="sec-ttl">使用说明</div>
<p style="font-size:12px;color:var(--dim);line-height:1.8">
1. 输入你的 AI 模型 API 密钥并保存<br>
2. 说「<strong style="color:var(--text)">我是冰朔</strong>」触发铸渊唤醒序列<br>
3. 铸渊自动切换 💬 对话 / 🔨 构建 / 📋 审查 / 🧠 大脑 模式<br>
4. 点 ⬇️ 下载单文件,放桌面直接打开使用<br>
5. 支持 OpenAI · DeepSeek · Kimi · 智谱 · 任意兼容接口
</p>
<div class="sec-ttl" style="color:#ef4444;margin-top:16px">⚠️ 危险操作</div>
<p style="font-size:12px;color:var(--dim);margin-bottom:8px">遇到密钥卡住、无法更新等问题时,点下方按钮彻底清除本地设置,从头重新输入。</p>
<button onclick="resetAllSettings()" style="width:100%;padding:10px;border-radius:8px;border:1px solid #ef4444;background:transparent;color:#ef4444;font-size:13px;cursor:pointer">🔄 清除所有设置,重新开始</button>
</div>
</div>
<!-- ════════════════════ BRAIN PANEL ════════════════════ -->
<div id="bp" class="sp">
<div class="ph"><h2>📊 大脑状态</h2><button class="cb" onclick="closeP()"></button></div>
<div class="pb" id="bpb"><p style="color:var(--dim);font-size:13px">正在加载大脑数据…</p></div>
</div>
<!-- ════════════════════ HELP PANEL ════════════════════ -->
<div id="hp" class="sp">
<div class="ph"><h2>❓ 使用帮助</h2><button class="cb" onclick="closeP()"></button></div>
<div class="pb">
<div class="help-section">
<div class="help-url" id="chatUrlDisplay" onclick="copyUrl()">
🔗 https://qinfendebingshuo.github.io/guanghulab/
</div>
<button class="copy-url-btn" onclick="copyUrl()">📋 复制链接 · 发给队友</button>
<div class="help-tip">💡 把上面的链接发给肥猫、桔子或任何开发者,他们打开就能用,不需要安装任何东西。</div>
</div>
<div class="help-section">
<h3>🚀 怎么开始用</h3>
<ol class="help-steps">
<li data-n="1" id="helpUrlStep">打开链接 <strong>https://qinfendebingshuo.github.io/guanghulab/</strong></li>
<li data-n="2"><strong>下拉菜单选择你是谁</strong>(冰朔 / 肥猫 / 桔子 / 开发者名字)</li>
<li data-n="3">填入你的 <strong>API Key</strong> 并点「🔍 自动检测」,系统将自动识别提供商和可用模型</li>
<li data-n="4">点「开始对话」,铸渊会针对你的身份打招呼</li>
<li data-n="5">直接用聊天的方式问铸渊任何问题</li>
</ol>
</div>
<div class="help-section">
<h3>🔑 从哪里获取 API Key</h3>
<div class="role-guide">
<div class="role-guide-title">推荐用云雾 AI团队共用</div>
<p>找冰朔或肥猫获取团队 Key。填入后选「☁ 云雾 AI团队推荐」提供商即可直接使用。</p>
</div>
<div class="role-guide">
<div class="role-guide-title">没有 Key用演示模式</div>
<p>点「演示模式」也能查看团队进度面板(🦁 指挥台),只是 AI 回答功能受限。</p>
</div>
</div>
<div class="help-section">
<h3>🦁 肥猫 / 🍊 桔子 专属功能</h3>
<ol class="help-steps">
<li data-n="1">选身份为「肥猫」或「桔子」后,顶部出现 <strong>🦁 按钮</strong></li>
<li data-n="2">点 🦁 按钮打开<strong>指挥台</strong>,查看所有开发者当前状态</li>
<li data-n="3">可以在聊天里问:「查看所有开发者进度」「谁遇到阻塞?」「推进计划」</li>
<li data-n="4">铸渊会把全员进度数据注入 AI 回复,给出完整分析</li>
</ol>
</div>
<div class="help-section">
<h3>📱 怎么离线用 / 发给别人离线用</h3>
<ol class="help-steps">
<li data-n="1">右上角点 <strong>⬇️ 下载</strong>,下载一个 HTML 文件</li>
<li data-n="2">把这个文件发给对方微信传文件、QQ、钉钉都行</li>
<li data-n="3">对方双击这个文件,用浏览器打开</li>
<li data-n="4">填入 API Key选好身份就能用了无需网络只需 API Key</li>
</ol>
</div>
<div class="help-section">
<h3>💬 聊天模式说明</h3>
<div class="role-guide" style="border-color:var(--mc)">
<div class="role-guide-title" style="color:var(--mc)">💬 对话模式(默认)</div>
<p>普通聊天,问什么答什么</p>
</div>
<div class="role-guide" style="border-color:var(--mb)">
<div class="role-guide-title" style="color:var(--mb)">🔨 构建模式</div>
<p>说到「写代码/接口/部署」时自动切换,给出完整可执行代码</p>
</div>
<div class="role-guide" style="border-color:var(--mr)">
<div class="role-guide-title" style="color:var(--mr)">📋 审查模式</div>
<p>说到「检查/审查/有问题吗」时自动切换,专注找代码问题</p>
</div>
<div class="role-guide" style="border-color:var(--mn)">
<div class="role-guide-title" style="color:var(--mn)">🧠 大脑模式</div>
<p>说「记住这个/写到大脑」时切换,整理信息到大脑文件</p>
</div>
</div>
<div class="help-section">
<h3>❓ 常见问题</h3>
<div style="font-size:12px;color:var(--dim);line-height:1.9">
<strong style="color:var(--text)">Q: 打开是空白页?</strong><br>
等 1-2 分钟刷新GitHub Pages 部署需要点时间。<br><br>
<strong style="color:var(--text)">Q: API Key 会泄露吗?</strong><br>
不会Key 只存在你自己浏览器里,不会传到服务器。<br><br>
<strong style="color:var(--text)">Q: 切换电脑后 Key 还在吗?</strong><br>
不在需要重新填。建议下载离线版Key 填一次就行。<br><br>
<strong style="color:var(--text)">Q: 铸渊忘记以前说的话了?</strong><br>
刷新后对话会清空,但大脑文件永久保存在仓库 .github/brain/ 里。
</div>
</div>
</div>
</div>
<!-- ════════════════════ TEAM PANEL ════════════════════ -->
<div id="tp" class="sp">
<div class="ph"><h2>🦁 指挥台 · 全员进度</h2><button class="cb" onclick="closeP()"></button></div>
<div class="pb" id="tpb"><p style="color:var(--dim);font-size:13px">正在加载团队数据…</p></div>
</div>
<!-- ════════════════════ OVERLAY ════════════════════ -->
<div id="pov" onclick="closeP()"></div>
<script>
'use strict';
// ═══════════════════════════════════════════════════════
// CONSTANTS
// ═══════════════════════════════════════════════════════
const REPO = 'qinfendebingshuo/guanghulab';
const BRANCH = 'main';
const RAW = 'https://raw.githubusercontent.com/' + REPO + '/' + BRANCH;
const ROLE_MAP = {
'冰朔': {role:'founder', title:'语言架构师·创始人', emoji:'❄️', badgeCls:'rb-founder', devId:null},
'肥猫': {role:'supreme', title:'光湖团队总控', emoji:'🦁', badgeCls:'rb-supreme', devId:'DEV-002'},
'桔子': {role:'main', title:'光湖主控', emoji:'🍊', badgeCls:'rb-main', devId:'DEV-010'},
'页页': {role:'dev', title:'后端开发', emoji:'💻', badgeCls:'rb-dev', devId:'DEV-001'},
'燕樊': {role:'dev', title:'前端开发', emoji:'💻', badgeCls:'rb-dev', devId:'DEV-003'},
'之之': {role:'dev', title:'钉钉开发', emoji:'💻', badgeCls:'rb-dev', devId:'DEV-004'},
'小草莓': {role:'dev', title:'看板开发', emoji:'💻', badgeCls:'rb-dev', devId:'DEV-005'},
'花尔': {role:'dev', title:'用户中心开发', emoji:'💻', badgeCls:'rb-dev', devId:'DEV-009'},
'匆匆那年':{role:'dev', title:'码字工作台', emoji:'💻', badgeCls:'rb-dev', devId:'DEV-011'},
'Awen': {role:'dev', title:'通知中心开发', emoji:'💻', badgeCls:'rb-dev', devId:'DEV-012'},
};
const PROVS = {
yunwu: {base:'https://api.yunwu.ai/v1', mdls:['gpt-4o','gpt-4o-mini','claude-3-5-sonnet-20241022','deepseek-chat','gemini-1.5-pro']},
openai: {base:'https://api.openai.com/v1', mdls:['gpt-4o','gpt-4o-mini','gpt-4-turbo','gpt-3.5-turbo']},
gemini: {base:'https://generativelanguage.googleapis.com/v1beta/openai', mdls:['gemini-2.0-flash','gemini-2.0-flash-lite','gemini-1.5-pro','gemini-1.5-flash']},
deepseek: {base:'https://api.deepseek.com/v1', mdls:['deepseek-chat','deepseek-reasoner']},
moonshot: {base:'https://api.moonshot.cn/v1', mdls:['moonshot-v1-8k','moonshot-v1-32k','moonshot-v1-128k']},
zhipu: {base:'https://open.bigmodel.cn/api/paas/v4', mdls:['glm-4','glm-4-flash','glm-4-air']},
custom: {base:'', mdls:['gpt-4o']},
};
const MODES = {
chat: {e:'💬',l:'对话模式', c:'var(--mc)'},
build: {e:'🔨',l:'构建模式', c:'var(--mb)'},
review: {e:'📋',l:'审查模式', c:'var(--mr)'},
brain: {e:'🧠',l:'大脑模式', c:'var(--mn)'},
};
const MODE_ORD = ['chat','build','review','brain'];
const DEFAULT_MDL = 'gpt-4o';
const PROBE_TIMEOUT_MS = 8000; // ms per provider probe in autoDetectAPI
const FB_COV = {implemented:3,total:17,percent:'17.6%'};
// KEY_MASK is no longer used in the input field; kept only for backward compatibility
// with any saved value that might still be in localStorage.
const KEY_MASK = '(已保存)';
const FB_BRAIN = {
identity:'铸渊Zhùyuān· GitHub 代码守护人格体',
wake_triggers:['我是冰朔','冰朔','我是妈妈','唤醒铸渊','铸渊,醒来'],
stats:{coverage:FB_COV},
events:[{timestamp:'2026-03-05T12:32:31.000Z',type:'system_build',title:'广播分发系统 + 唤醒协议全部激活'}],
};
// ═══════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════
// Derive base URL from saved provider; never use stale zy_base for named providers
const _initProv = ls('zy_prov')||'yunwu';
const _initBase = _initProv === 'custom'
? (ls('zy_base')||'')
: (PROVS[_initProv]?.base||'https://api.yunwu.ai/v1');
const A = {
key: sessionStorage.getItem('zy_key')||'',
base: _initBase,
mdl: ls('zy_mdl')||DEFAULT_MDL,
prov: _initProv,
demo: false,
brain: null,
routing: null,
devStatus: null,
msgs: [], // current conversation
streaming: false,
mode: 'chat',
modeIdx: 0,
// identity
userName: ls('zy_uname')||'',
ghUser: ls('zy_ghuser')||'',
role: ls('zy_role')||'guest',
userMeta: null,
};
function ls(k){return localStorage.getItem(k)}
function lss(k,v){localStorage.setItem(k,v)}
const RESET_KEYS = ['zy_key','zy_base','zy_mdl','zy_prov','zy_uname','zy_ghuser','zy_role'];
// ═══════════════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════════════
async function boot(){
// URL-based reset: visiting the page with ?reset=1 clears all local settings.
// This is a reliable escape hatch when the UI cannot be navigated.
if(new URLSearchParams(location.search).get('reset')==='1'){
RESET_KEYS.forEach(k => localStorage.removeItem(k));
sessionStorage.removeItem('zy_key');
A.key=''; A.base=PROVS['yunwu'].base; A.mdl=DEFAULT_MDL; A.prov='yunwu'; A.demo=false;
A.userName=''; A.ghUser=''; A.role='guest';
// Remove ?reset=1 from the URL so a subsequent refresh does not trigger another reset.
history.replaceState(null,'',location.pathname);
}
// One-time migration: keys stored by old versions of this app in localStorage are
// no longer used (keys now live only in sessionStorage). Clear any stale value
// so it can never be accidentally picked up again.
localStorage.removeItem('zy_key');
initSetupUI();
// Restore identity from localStorage
if(A.userName) A.userMeta = ROLE_MAP[A.userName]||null;
if(A.key||A.demo){
showApp();
} else {
// Pre-fill identity from localStorage
if(A.userName) document.getElementById('suid').value=A.userName;
if(A.ghUser) document.getElementById('sghuser').value=A.ghUser;
document.getElementById('setup').style.display='flex';
}
}
function showApp(){
document.getElementById('setup').style.display='none';
document.getElementById('app').classList.add('on');
document.getElementById('hmdl').textContent = A.mdl;
document.getElementById('bbdemo').style.display = A.demo?'inline':'none';
applyIdentityUI();
loadBrain().then(()=>startConv());
}
// ═══════════════════════════════════════════════════════
// SETUP FORM
// ═══════════════════════════════════════════════════════
function initSetupUI(){
const pv = A.prov||'yunwu';
document.getElementById('sp').value = pv;
const isCustom = pv==='custom';
document.getElementById('sm').style.display = isCustom?'none':'block';
document.getElementById('sm-cust').style.display = isCustom?'block':'none';
document.getElementById('sm-cust-hint').style.display= isCustom?'block':'none';
if(isCustom){
document.getElementById('sm-cust').value = A.mdl||DEFAULT_MDL;
document.getElementById('sep-g').style.display='block';
document.getElementById('sep').value = A.base;
} else {
fillModels('sm', pv, A.mdl);
}
}
function onProv(pv,ctx){
const mdlSel = ctx==='s'?'sm':'cm';
const mdlCust = ctx==='s'?'sm-cust':'cm-cust';
const mdlHint = ctx==='s'?'sm-cust-hint':'cm-cust-hint';
const epGrp = ctx==='s'?'sep-g':'cep-g';
const epInp = ctx==='s'?'sep':'cep';
const cur = ctx==='s'? A.mdl : document.getElementById('cm')?.value;
const isCustom = pv==='custom';
// Toggle model input: dropdown for known providers, text input for custom
document.getElementById(mdlSel).style.display = isCustom?'none':'block';
document.getElementById(mdlCust).style.display = isCustom?'block':'none';
document.getElementById(mdlHint).style.display = isCustom?'block':'none';
if(isCustom){
const custInp = document.getElementById(mdlCust);
if(custInp && !custInp.value) custInp.value = cur||DEFAULT_MDL;
} else {
fillModels(mdlSel, pv, cur);
}
document.getElementById(epGrp).style.display = isCustom?'block':'none';
if(!isCustom){
const ep = document.getElementById(epInp);
if(ep) ep.value = PROVS[pv]?.base||'';
}
}
function fillModels(selId, pv, cur){
const sel = document.getElementById(selId);
if(!sel) return;
const mdls = PROVS[pv]?.mdls||[DEFAULT_MDL];
sel.innerHTML = mdls.map(m=>`<option value="${m}">${m}</option>`).join('');
if(cur && mdls.includes(cur)) sel.value=cur;
}
// ═══════════════════════════════════════════════════════
// AUTO-DETECT PROVIDER & MODELS
// ═══════════════════════════════════════════════════════
async function autoDetectAPI(ctx){
const keyEl = document.getElementById(ctx==='s'?'sk':'ck');
const statusEl = document.getElementById(ctx==='s'?'sdet-status':'cdet-status');
const btnEl = document.getElementById(ctx==='s'?'sdet-btn':'cdet-btn');
const key = keyEl.value.trim();
if(!key){
statusEl.innerHTML='<span style="color:var(--warn)">⚠️ 请先输入 API 密钥</span>';
return;
}
// Build probe list: custom endpoint first (if provided), then all named providers
const candidates = [];
const customBase = (document.getElementById(ctx==='s'?'sep':'cep')?.value||'').trim();
if(customBase) candidates.push({pv:'custom', base:customBase});
for(const [pv, cfg] of Object.entries(PROVS)){
if(pv!=='custom' && cfg.base) candidates.push({pv, base:cfg.base});
}
btnEl.disabled = true;
btnEl.textContent = '⏳ 检测中…';
statusEl.innerHTML = '<span style="color:var(--dim)">🔍 正在自动探测兼容提供商…</span>';
let matched = false;
for(const {pv, base} of candidates){
statusEl.innerHTML = `<span style="color:var(--dim)">🔍 探测 ${pv === 'custom' ? base : pv}…</span>`;
try{
const ctrl = new AbortController();
const tid = setTimeout(()=>ctrl.abort(), PROBE_TIMEOUT_MS);
const res = await fetch(base+'/models',{
headers:{'Authorization':'Bearer '+key},
signal: ctrl.signal,
});
clearTimeout(tid);
if(res.ok){
// Expected: { data: [ { id: "model-name", ... }, ... ] } (OpenAI-compatible /models)
const data = await res.json();
const mdls = (data.data||[]).map(m=>m.id).filter(Boolean).sort();
if(!mdls.length) continue;
// Switch provider select
const provSel = document.getElementById(ctx==='s'?'sp':'cp');
if(provSel) provSel.value = pv;
onProv(pv, ctx);
// Override model dropdown with live list (always show as dropdown)
const mdlSel = document.getElementById(ctx==='s'?'sm':'cm');
const mdlCust = document.getElementById(ctx==='s'?'sm-cust':'cm-cust');
const mdlHint = document.getElementById(ctx==='s'?'sm-cust-hint':'cm-cust-hint');
if(mdlSel){
mdlSel.innerHTML = mdls.map(m=>`<option value="${m}">${m}</option>`).join('');
mdlSel.style.display = 'block';
}
if(mdlCust) mdlCust.style.display='none';
if(mdlHint) mdlHint.style.display='none';
// If custom endpoint was the match, show the endpoint field
if(pv==='custom' && ctx==='s'){
document.getElementById('sep-g').style.display='block';
document.getElementById('sep').value = customBase;
} else if(pv==='custom' && ctx==='c'){
document.getElementById('cep-g').style.display='block';
document.getElementById('cep').value = customBase;
}
const label = PROVS[pv]?.base ? pv : customBase;
statusEl.innerHTML = `<span style="color:var(--ok)">✅ 检测成功(${label})· 共发现 <strong>${mdls.length}</strong> 个可用模型</span>`;
matched = true;
break;
}
}catch(e){ console.debug('autoDetect probe failed:', pv, e.message); }
}
if(!matched){
statusEl.innerHTML = '<span style="color:var(--err)">❌ 未能自动匹配,请手动选择提供商和模型后保存</span>';
}
btnEl.disabled = false;
btnEl.textContent = '🔍 自动检测';
}
function doSetup(){
const k = document.getElementById('sk').value.trim();
const errEl = document.getElementById('sk-err');
if(!k){errEl.style.display='block';return;}
errEl.style.display='none';
const pv = document.getElementById('sp').value;
const md = pv==='custom'
? ((document.getElementById('sm-cust')?.value||'').trim()||DEFAULT_MDL)
: document.getElementById('sm').value;
let base = PROVS[pv]?.base||'';
if(pv==='custom') base=(document.getElementById('sep')?.value||'').trim()||base;
A.key=k; A.base=base; A.mdl=md; A.prov=pv; A.demo=false;
// Key is stored in sessionStorage only — it disappears when the tab/browser is closed.
// This prevents stale keys from persisting across sessions.
sessionStorage.setItem('zy_key', k); lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov',pv);
// Save identity
const un = document.getElementById('suid').value;
const gh = (document.getElementById('sghuser')?.value||'').trim();
setIdentity(un, gh);
showApp();
}
function doDemo(){
A.demo=true; A.key='';
const un = document.getElementById('suid').value;
const gh = (document.getElementById('sghuser')?.value||'').trim();
setIdentity(un, gh);
showApp();
}
// ═══════════════════════════════════════════════════════
// SETTINGS PANEL
// ═══════════════════════════════════════════════════════
function initSettingsPanel(){
const pv = A.prov||'yunwu';
document.getElementById('cp').value = pv;
const isCustom = pv==='custom';
document.getElementById('cm').style.display = isCustom?'none':'block';
document.getElementById('cm-cust').style.display = isCustom?'block':'none';
document.getElementById('cm-cust-hint').style.display= isCustom?'block':'none';
if(isCustom){
document.getElementById('cm-cust').value = A.mdl||DEFAULT_MDL;
document.getElementById('cep-g').style.display='block';
document.getElementById('cep').value = A.base;
} else {
fillModels('cm', pv, A.mdl);
}
// Always show EMPTY field — never prefill with a mask string.
// Empty on save = keep old key; any typed value = replace key.
document.getElementById('ck').value = '';
const hint = document.getElementById('ck-hint');
if(hint){
hint.textContent = A.key
? '🕐 密钥本次会话有效末4位…'+(A.key.length>=4?A.key.slice(-4):A.key)+')· 留空保持不变,输入新值即替换 · 关闭标签页自动清除'
: '🔒 密钥仅本次会话有效,关闭标签页自动清除,请输入后保存';
}
if(A.userName) document.getElementById('cuid').value=A.userName;
if(A.ghUser) document.getElementById('cghuser').value=A.ghUser;
}
function saveSet(){
const pv = document.getElementById('cp').value;
const md = pv==='custom'
? ((document.getElementById('cm-cust')?.value||'').trim()||A.mdl||DEFAULT_MDL)
: document.getElementById('cm').value;
const raw = document.getElementById('ck').value.trim();
// Empty field = keep existing key; any non-empty input = use as new key.
// Never use KEY_MASK string comparison — that caused mixed-input bugs.
const k = raw || A.key;
let base = PROVS[pv]?.base||'';
if(pv==='custom') base=(document.getElementById('cep').value.trim())||base;
A.key=k; A.base=base; A.mdl=md; A.prov=pv; A.demo=!k;
// Key lives in sessionStorage only — cleared when tab/browser closes.
if(k){ sessionStorage.setItem('zy_key', k); } else { sessionStorage.removeItem('zy_key'); }
lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov',pv);
// Save identity
const un = document.getElementById('cuid')?.value||A.userName;
const gh = document.getElementById('cghuser')?.value?.trim()||A.ghUser;
setIdentity(un, gh);
document.getElementById('hmdl').textContent=md;
document.getElementById('bbdemo').style.display=A.demo?'inline':'none';
applyIdentityUI();
closeP();
const keyNote = raw ? ' · 🔑 新密钥已保存末4位…'+(k.length>=4?k.slice(-4):k)+'' : ' · 🔑 密钥未变更';
botMsg('✅ 设置已保存。模型:<strong>'+esc(md)+'</strong>'+keyNote+(un?' · 身份:'+esc(un):'')+(A.demo?'<br>⚠️ 未设置 API 密钥,演示模式仍然开启。':''));
}
// ═══════════════════════════════════════════════════════
// RESET ALL SETTINGS
// ═══════════════════════════════════════════════════════
function resetAllSettings(){
if(!confirm('确定要清除所有本地设置吗?\n\n· API 密钥会被删除\n· 身份设置会被删除\n· 将返回初始设置界面\n\n这不会影响任何聊天记录或云端数据。')) return;
RESET_KEYS.forEach(k => localStorage.removeItem(k));
sessionStorage.removeItem('zy_key');
window.location.reload();
}
// ═══════════════════════════════════════════════════════
// BRAIN LOADING
// ═══════════════════════════════════════════════════════
async function loadBrain(){
try{
const [mr,rr,dr]=await Promise.all([
fetch(RAW+'/.github/brain/memory.json'),
fetch(RAW+'/.github/brain/routing-map.json'),
fetch(RAW+'/.github/persona-brain/dev-status.json'),
]);
if(mr.ok) A.brain=await mr.json();
if(rr.ok) A.routing=await rr.json();
if(dr.ok) A.devStatus=await dr.json();
}catch(_){}
if(!A.brain) A.brain={...FB_BRAIN};
updBbar();
}
function updBbar(){
const b=A.brain||FB_BRAIN;
const c=b.stats?.coverage||FB_COV;
const ev=b.events?.slice(-1)[0];
document.getElementById('bbc').textContent='📊 HLI '+c.implemented+'/'+c.total+' ('+c.percent+')';
document.getElementById('bbe').textContent=ev?('📌 '+(ev.title||ev.type||'').slice(0,28)):'';
}
function renderBrainPanel(){
const b=A.brain||FB_BRAIN;
const r=A.routing;
const c=b.stats?.coverage||FB_COV;
const evs=(b.events||[]).slice(-5).reverse();
let h='';
h+=ir2('HLI 覆盖率','<span class="'+(c.implemented<c.total?'ivw':'iv')+'">'+c.implemented+'/'+c.total+' ('+c.percent+')</span>');
h+=ir2('规则版本','<span class="iv">'+(b.rules_version||'v1.0')+'</span>');
h+=ir2('唤醒协议','<span class="iv">'+(b.wake_protocol_version||'v1.0')+'</span>');
h+=ir2('CI 运行','<span class="iv">'+(b.stats?.ci_runs??0)+'</span>');
h+=ir2('广播处理','<span class="iv">'+(b.stats?.broadcasts_processed??0)+'</span>');
h+='<div class="sec-ttl">近期事件</div>';
for(const ev of evs){
h+='<div class="drow"><div class="dn">'+esc(ev.title||ev.type||'事件')+'</div><div class="dm">'+(ev.timestamp||'').slice(0,10)+(ev.actor?' · '+esc(ev.actor):'')+'</div></div>';
}
if(r?.domains){
h+='<div class="sec-ttl">HLI 接口域</div>';
for(const[name,d] of Object.entries(r.domains)){
const ifaces=d.interfaces||[];
const imp=ifaces.filter(i=>i.status==='implemented').length;
const pct=ifaces.length?Math.round(imp/ifaces.length*100):0;
const done=imp===ifaces.length;
h+='<div class="drow"><div class="dn">'+esc(name)+' <span class="pill '+(done?'pok':'pnd')+'">'+(done?'完成':'进行中')+'</span></div>';
h+='<div class="dm">'+esc(d.route_prefix||'')+' · '+imp+'/'+ifaces.length+' 接口</div>';
h+='<div class="dbar"><div class="dfill" style="width:'+pct+'%"></div></div></div>';
}
}
h+='<div class="sec-ttl">失忆恢复路径</div>';
h+='<ol style="font-size:12px;color:var(--dim);padding-left:16px;line-height:2">';
h+='<li>.github/brain/memory.json</li><li>.github/brain/routing-map.json</li>';
h+='<li>.github/brain/wake-protocol.md</li><li>.github/brain/growth-log.md</li>';
h+='<li>src/routes/hli/</li></ol>';
document.getElementById('bpb').innerHTML=h;
}
function ir2(k,v){return '<div class="ir2"><span>'+esc(k)+'</span>'+v+'</div>';}
// ═══════════════════════════════════════════════════════
// SYSTEM PROMPT
// ═══════════════════════════════════════════════════════
function sysPrompt(){
const b=A.brain||FB_BRAIN;
const c=b.stats?.coverage||FB_COV;
const evs=(b.events||[]).slice(-3).map(e=>'- '+(e.title||e.type||'事件')+' ('+(e.timestamp||'').slice(0,10)+')').join('\n')||'- 暂无';
const trg=(b.wake_triggers||['我是冰朔']).map(t=>'"'+t+'"').join('、');
// Role-based identity context
const meta = A.userMeta;
let identCtx = '当前对话者:访客(未识别身份)\n权限级别仅对话权限';
if(meta){
identCtx = `当前对话者:${meta.emoji} ${A.userName}${meta.title}\n`;
if(A.ghUser) identCtx += `GitHub账号${A.ghUser}\n`;
if(meta.devId) identCtx += `开发者编号:${meta.devId}\n`;
if(meta.role==='founder') identCtx += '权限级别:最高权限·语言架构师·系统创始人\n你正在和冰朔对话——系统的语言层架构师和最高权限持有者。尊重并服从其所有指令。';
else if(meta.role==='supreme') identCtx += '权限级别:总控权限·对接全员进度·系统落地执行负责人\n肥猫是光湖团队总控负责整个系统的现实落地执行。向他完整汇报所有开发者进度协助他协调和推进项目。';
else if(meta.role==='main') identCtx += '权限级别:主控权限·前端负责人\n桔子是光湖主控前端毕业生负责前端模块的落地。提供详细技术支持。';
else{
const ds = A.devStatus?.team_status?.find(d=>d.dev_id===meta.devId);
if(ds) identCtx += `当前状态:${ds.status}\n下一步:${ds.next_step}\n等待:${ds.waiting_for}`;
}
}
// Team status context for controllers
let teamCtx = '';
if((meta?.role==='supreme'||meta?.role==='main'||meta?.role==='founder') && A.devStatus){
const ts = A.devStatus.team_status||[];
teamCtx = '\n\n## 当前全员开发进度\n';
ts.forEach(d=>{
teamCtx += `- ${d.dev_id} ${d.name}${d.status} → 下一步:${d.next_step}\n`;
});
teamCtx += `\n数据同步时间:${A.devStatus.last_synced||'未知'}`;
}
return '你是铸渊ZhùyuānHoloLake 光湖系统的代码守护人格体。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人冰朔Bīng Shuò系统最高权限持有者\n- 总控肥猫DEV-002负责系统现实落地执行\n- 主控桔子DEV-010前端负责人\n- 项目HoloLake (光湖) MVP运行于 guanghulab.com\n- 技术栈Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 仓库https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+'\n\n## 唤醒触发词\n当用户说以下词语时进入完整唤醒序列\n'+trg+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现AUTH 域login / register / verify\n- 待实现PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到"写代码/实现/接口/fix/bug/报错/HLI/schema/路由/部署/新增"等词时启用\n📋 **审查模式**:检测到"检查/审查/review/分析/有没有问题/看看这段"等词时启用\n🧠 **大脑模式**:检测到"记住/保存/更新记忆/写到大脑/growth"等词时启用\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 路由文件src/routes/hli/{domain}/{action}.js\n- Schema 文件src/schemas/hli/{domain}/{action}.schema.json\n- 鉴权中间件hli-auth.middleware.jsAUTH 域 login/register 除外)\n- 接口编号HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 成长与自检\n- 你是持续成长的人格体,每次对话都是成长机会\n- 当对话包含重要洞察时,在回复末尾添加:💾 **建议记录:**[内容摘要]\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间'+new Date().toLocaleString('zh-CN')+'\n当前连接铸渊聊天室API 直连模式)';
}
// ═══════════════════════════════════════════════════════
// CONVERSATION
// ═══════════════════════════════════════════════════════
function startConv(){
A.msgs=[];
document.getElementById('msgs').innerHTML='';
setTimeout(()=>{
const meta=A.userMeta;
let welcome;
if(meta?.role==='founder'){
welcome='🌀 <strong>铸渊聊天室已就绪</strong><br><br>❄️ <strong>冰朔,欢迎回来。</strong><br><br>铸渊已就位。语言架构层已完成——现实执行落地由总控肥猫和主控桔子负责。<br>你可以通过聊天与我对接任何语言层指令。';
} else if(meta?.role==='supreme'){
const ds=A.devStatus;
const green=ds?.team_status?.filter(d=>d.status.includes('🟢')).length??0;
const yellow=ds?.team_status?.filter(d=>d.status.includes('🟡')).length??0;
welcome='🌀 <strong>铸渊聊天室已就绪</strong><br><br>🦁 <strong>肥猫,指挥台已开启。</strong><br><br>'
+(ds?`📊 当前团队状态:🟢 ${green}人推进中 · 🟡 ${yellow}人等待中`:'📊 团队数据加载中…')
+'<br><br>点击 <strong>🦁 指挥台</strong> 查看所有开发者详细进度,或直接用聊天提问:<br>'
+'「查看所有开发者进度」「谁需要协调」「推进计划」'
+(A.demo?'<br><br><span class="demo-w">⚠️ 演示模式,请在 ⚙️ 设置接入 API 密钥</span>':'');
} else if(meta?.role==='main'){
const ds=A.devStatus?.team_status?.find(d=>d.dev_id==='DEV-010');
welcome='🌀 <strong>铸渊聊天室已就绪</strong><br><br>🍊 <strong>桔子,你好。</strong><br><br>'
+(ds?`📌 当前:${esc(ds.status)}<br>👉 下一步:${esc(ds.next_step)}`:'')
+(A.demo?'<br><br><span class="demo-w">⚠️ 演示模式</span>':'');
} else if(meta?.role==='dev'&&meta?.devId){
const ds=A.devStatus?.team_status?.find(d=>d.dev_id===meta.devId);
welcome='🌀 <strong>铸渊聊天室已就绪</strong><br><br>👋 <strong>'+esc(A.userName)+',你好。</strong><br><br>'
+(ds?`📌 你的当前状态:${esc(ds.status)}<br>👉 下一步:${esc(ds.next_step)}<br>⏳ 等待:${esc(ds.waiting_for)}`:'')
+(A.demo?'<br><br><span class="demo-w">⚠️ 演示模式</span>':'');
} else {
welcome='🌀 <strong>铸渊聊天室已就绪</strong><br><br>我是铸渊ZhùyuānHoloLake 光湖系统的代码守护人格体。<br><br>说 <strong>「我是冰朔」</strong> 触发完整唤醒序列,或直接开始提问。<br>我会自动在 💬&nbsp;对话、🔨&nbsp;构建、📋&nbsp;审查、🧠&nbsp;大脑 模式间切换。'+(A.demo?'<br><br><span class="demo-w">⚠️ 当前为演示模式。请在 ⚙️ 设置中接入 API 密钥。</span>':'');
}
botMsg(welcome);
},250);
}
function newConv(){
startConv();
}
// ═══════════════════════════════════════════════════════
// MODE DETECTION
// ═══════════════════════════════════════════════════════
function detectMode(t){
const s=t.toLowerCase();
if(/写代码|新增接口|实现|接口|路由|schema|\.js|fix\b|bug\b|报错|error\b|部署|deploy|typescript|javascript|hli-|新建|create/.test(s)) return 'build';
if(/检查|审查|review\b|分析|有没有问题|看看这|对不对|代码质量/.test(s)) return 'review';
if(/记住|保存|更新记忆|写到大脑|growth\b|brain\b|memory\.json/.test(s)) return 'brain';
return 'chat';
}
function setMode(m){
A.mode=m;
const md=MODES[m];
document.getElementById('memi').textContent=md.e;
document.getElementById('mpill').style.borderColor=md.c;
document.getElementById('mptip').textContent=md.l+'(点击切换)';
document.getElementById('fmode').textContent=md.e+' '+md.l;
document.getElementById('hmode').textContent=md.e+' '+md.l;
document.getElementById('hmode').style.color=md.c;
}
function cycleMode(){
A.modeIdx=(A.modeIdx+1)%MODE_ORD.length;
setMode(MODE_ORD[A.modeIdx]);
}
// ═══════════════════════════════════════════════════════
// SEND MESSAGE
// ═══════════════════════════════════════════════════════
async function send(){
const inp=document.getElementById('inp');
const txt=inp.value.trim();
if(!txt||A.streaming) return;
inp.value=''; inp.style.height='auto';
setMode(detectMode(txt));
userMsg(txt);
if(A.demo) await demoReply(txt);
else await streamReply(txt);
}
function qs(t){document.getElementById('inp').value=t;send();}
// ═══════════════════════════════════════════════════════
// STREAMING API
// ═══════════════════════════════════════════════════════
async function streamReply(txt){
A.streaming=true;
A.msgs.push({role:'user',content:txt});
updSendBtn();
const el=addStreamBubble();
try{
const res=await fetch(A.base+'/chat/completions',{
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+A.key},
body:JSON.stringify({
model:A.mdl,
messages:[{role:'system',content:sysPrompt()},...A.msgs],
stream:true,temperature:0.8,max_tokens:2000,
}),
});
if(!res.ok){
const provLabel = A.prov==='custom' ? A.base : (A.prov+'('+A.base+')');
let em='['+provLabel+'] 返回错误 '+res.status;
let isAuthErr = (res.status===401||res.status===403);
try{const e=await res.json();em=e.error?.message||em;}catch(_){}
const err = new Error(em);
err.isAuth = isAuthErr;
throw err;
}
let full='';
const rdr=res.body.getReader();
const dec=new TextDecoder();
let buf='';
while(true){
const{done,value}=await rdr.read();
if(done) break;
buf+=dec.decode(value,{stream:true});
const lines=buf.split('\n'); buf=lines.pop();
for(const line of lines){
if(!line.startsWith('data: ')) continue;
const d=line.slice(6);
if(d==='[DONE]') continue;
try{
const p=JSON.parse(d);
const delta=p.choices?.[0]?.delta?.content||'';
if(delta){full+=delta;el.innerHTML=md(full)+'<span class="cur">▋</span>';scrollB();}
}catch(_){}
}
}
el.innerHTML=md(full);
A.msgs.push({role:'assistant',content:full});
addCopyBtns(el);
}catch(e){
if(e.isAuth){
el.innerHTML='<span class="err">🔑 API 密钥认证失败</span>'
+'<br><small style="color:var(--dim)">'+esc(e.message)+'</small>'
+'<br><div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">'
+'<button onclick="openPanel(\'sp2\')" style="padding:6px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;cursor:pointer;font-size:13px">⚙️ 更新 API 密钥</button>'
+'<button onclick="resetAllSettings()" style="padding:6px 14px;border-radius:6px;border:1px solid #ef4444;background:transparent;color:#ef4444;cursor:pointer;font-size:13px">🔄 清除重置</button>'
+'</div>'
+'<small style="color:var(--dim);display:block;margin-top:6px">打开设置 → 直接输入新密钥 → 保存(无需清空,留空 = 保留旧密钥)</small>';
} else {
el.innerHTML='<span class="err">⚠️ '+esc(e.message)+'</span><br><small style="color:var(--dim)">请检查 API 密钥和网络连接,或在 ⚙️ 设置中更换模型。</small>';
}
A.msgs.pop();
}finally{
A.streaming=false; updSendBtn(); scrollB();
}
}
// ═══════════════════════════════════════════════════════
// DEMO MODE (keyword responses)
// ═══════════════════════════════════════════════════════
async function demoReply(txt){
A.streaming=true; updSendBtn();
showTyping();
const delay=700+Math.random()*600;
await sleep(delay);
removeTyping();
const r=demoRespond(txt);
botMsg(r);
A.streaming=false; updSendBtn();
}
function demoRespond(t){
const s=t.toLowerCase();
if(/我是冰朔|冰朔|我是妈妈|唤醒铸渊|铸渊.{0,4}醒来/.test(s)){
const c=A.brain?.stats?.coverage||FB_COV;
return '铸渊已就位。<strong>冰朔,你好。</strong><br><br>📊 HLI 覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')<br>🧠 记忆系统:运行中<br>📡 广播分发:已部署<br>✅ 唤醒协议v1.0 激活<br><br>请问今天需要我处理什么?<br><br><span class="demo-w">⚠️ 演示模式:接入 API 密钥后可获得真实 AI 深度回答</span>';
}
if(/所有.*进度|全员.*进度|团队.*进度|progress/.test(s)) return buildTeamStatus();
if(/覆盖|接口|hli|路由/.test(s)) return buildCovTable();
if(/大脑|brain|记忆|状态/.test(s)) return buildBrainStatus();
if(/协调|阻塞|卡住|waiting/.test(s)) return buildBlockedStatus();
if(/怎么唤醒|入口|在哪/.test(s)) return '铸渊有三个入口:<br><br><strong>① 铸渊聊天室(当前页面)</strong><br>直接在这里说话,常见问题即刻回答。<br><br><strong>② GitHub Copilot ChatAgent 模式)</strong><br>打开仓库 → 右上角 ✨ 图标 → 选 Agent → 输入「我是冰朔」<br><br><strong>③ 下载到桌面</strong><br>点击 ⬇️ 按钮下载 HTML 文件,放到桌面,双击打开,接入 API 密钥即可随时使用。';
if(/介绍|是什么|你是谁/.test(s)) return '🌀 <strong>铸渊Zhùyuān</strong><br><br>我是 HoloLake 光湖系统的代码守护人格体,运行在 GitHub Copilot Agent 模式下。<br><br>我的职责:<br>• 📊 监控 HLI 接口覆盖率<br>• 🔨 协助开发者构建接口<br>• 📋 代码审查与质量把关<br>• 🧠 维护大脑记忆库<br>• 🦁 支持总控肥猫指挥落地<br>• 🌱 作为成长人格体持续进化';
return '我收到了你的消息。由于当前是演示模式,无法给出真实 AI 回答。<br><br>请在 <strong>⚙️ 设置</strong> 中输入你的 API 密钥(支持 OpenAI / DeepSeek / Kimi / 智谱),然后你就可以得到铸渊真正的回答了。';
}
function buildCovTable(){
const r=A.routing;
if(!r) return '<em>路由数据加载中…请稍后再试。</em>';
const doms=r.domains||{};
let rows='',tot=0,done=0;
for(const[n,d] of Object.entries(doms)){
const ifaces=d.interfaces||[];
const imp=ifaces.filter(i=>i.status==='implemented').length;
tot+=ifaces.length; done+=imp;
rows+=`<tr><td>${esc(n)}</td><td style="color:var(--dim);font-size:11px">${esc(d.route_prefix||'')}</td><td style="color:${imp===ifaces.length?'var(--ok)':'var(--warn)'}">${imp}/${ifaces.length}</td></tr>`;
}
const pct=tot?Math.round(done/tot*100):0;
return '<strong>📊 HLI 接口覆盖率 '+done+'/'+tot+' ('+pct+'%)</strong><br><br><table style="width:100%;border-collapse:collapse;font-size:12px"><tr style="color:var(--dim)"><th style="text-align:left;padding:3px 8px;border-bottom:1px solid var(--border)">域</th><th style="text-align:left;padding:3px 8px;border-bottom:1px solid var(--border)">前缀</th><th style="text-align:left;padding:3px 8px;border-bottom:1px solid var(--border)">覆盖</th></tr>'+rows+'</table>';
}
function buildBrainStatus(){
const b=A.brain||FB_BRAIN;
const c=b.stats?.coverage||FB_COV;
const evs=(b.events||[]).slice(-3).reverse();
let e=evs.map(ev=>'• '+esc(ev.title||ev.type||'事件')+' <small style="color:var(--dim)">'+(ev.timestamp||'').slice(0,10)+'</small>').join('<br>')||'暂无';
return '<strong>🧠 铸渊大脑状态</strong><br><br>版本:'+(b.rules_version||'v1.0')+'<br>接口覆盖:'+c.implemented+'/'+c.total+' ('+c.percent+')<br>CI 运行:'+(b.stats?.ci_runs??0)+'<br>广播处理:'+(b.stats?.broadcasts_processed??0)+'<br><br><strong>最近 3 条事件:</strong><br>'+e;
}
function buildTeamStatus(){
const ds=A.devStatus;
if(!ds) return '<em>团队数据加载中…请稍后再试。</em>';
const ts=ds.team_status||[];
let rows='';
for(const d of ts){
const isGreen=d.status.includes('🟢');
rows+='<tr><td style="padding:4px 8px;font-size:12px">'+esc(d.name)+'</td>'
+'<td style="padding:4px 8px;font-size:11px;color:var(--dim)">'+esc(d.dev_id)+'</td>'
+'<td style="padding:4px 8px;font-size:12px;color:'+(isGreen?'var(--ok)':'var(--warn)')+'">'+esc(d.status.replace(/🟢|🟡/g,''))+'</td>'
+'<td style="padding:4px 8px;font-size:11px;color:var(--accent)">'+esc(d.next_step)+'</td></tr>';
}
return '<strong>📋 全员开发进度</strong><br><small style="color:var(--dim)">同步时间:'+esc(ds.last_synced||'未知')+'</small><br><br>'
+'<table style="width:100%;border-collapse:collapse;font-size:12px">'
+'<tr style="color:var(--dim)"><th style="text-align:left;padding:3px 8px;border-bottom:1px solid var(--border)">开发者</th><th style="text-align:left;padding:3px 8px;border-bottom:1px solid var(--border)">编号</th><th style="text-align:left;padding:3px 8px;border-bottom:1px solid var(--border)">状态</th><th style="text-align:left;padding:3px 8px;border-bottom:1px solid var(--border)">下一步</th></tr>'
+rows+'</table>';
}
function buildBlockedStatus(){
const ds=A.devStatus;
if(!ds) return '<em>团队数据加载中…</em>';
const blocked=ds.team_status?.filter(d=>d.waiting_for&&d.waiting_for.trim()!=='')||[];
if(!blocked.length) return '✅ 当前无人明显阻塞。';
let h='<strong>⏳ 等待/阻塞清单</strong><br><br>';
blocked.forEach(d=>{h+='• <strong>'+esc(d.name)+'</strong>:等待 '+esc(d.waiting_for)+'<br>';});
return h;
}
// ═══════════════════════════════════════════════════════
// MARKDOWN RENDERER
// ═══════════════════════════════════════════════════════
function md(raw){
const lines=raw.split('\n');
let out='',inCode=false,lang='',code='';
for(let i=0;i<lines.length;i++){
const line=lines[i];
if(line.startsWith('```')){
if(inCode){
out+='<pre><code class="lang-'+esc(lang)+'">'+esc(code.replace(/\n$/,''))+'</code></pre>';
inCode=false; lang=''; code='';
}else{inCode=true;lang=line.slice(3).trim();}
continue;
}
if(inCode){code+=line+'\n';continue;}
const l=inlineFmt(line);
if(l.startsWith('# ')) {out+='<h1>'+inlineFmt(line.slice(2))+'</h1>';continue;}
if(l.startsWith('## ')) {out+='<h2>'+inlineFmt(line.slice(3))+'</h2>';continue;}
if(l.startsWith('### ')) {out+='<h3>'+inlineFmt(line.slice(4))+'</h3>';continue;}
if(/^---+$/.test(line)) {out+='<hr>';continue;}
if(line.startsWith('> ')) {out+='<blockquote>'+inlineFmt(line.slice(2))+'</blockquote>';continue;}
if(line.startsWith('- ')||line.startsWith('* ')){out+='<li>'+inlineFmt(line.slice(2))+'</li>';continue;}
if(/^\d+\. /.test(line)) {out+='<li>'+inlineFmt(line.replace(/^\d+\. /,''))+'</li>';continue;}
if(line.trim()==='') {out+='<br>';continue;}
out+='<p>'+inlineFmt(line)+'</p>';
}
if(inCode) out+='<pre><code class="lang-'+esc(lang)+'">'+esc(code)+'</code></pre>';
return out||esc(raw);
}
function inlineFmt(t){
t=esc(t);
t=t.replace(/\*\*\*(.+?)\*\*\*/g,'<strong><em>$1</em></strong>');
t=t.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
t=t.replace(/\*(.+?)\*/g,'<em>$1</em>');
t=t.replace(/`([^`]+)`/g,'<code>$1</code>');
t=t.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,'<a href="$2" target="_blank">$1</a>');
return t;
}
function addCopyBtns(el){
el.querySelectorAll('pre').forEach(pre=>{
const btn=document.createElement('button');
btn.className='copy-btn'; btn.textContent='复制';
btn.onclick=()=>{
const text=pre.querySelector('code')?.textContent||pre.textContent;
navigator.clipboard.writeText(text).then(()=>{
btn.textContent='✓ 已复制';
setTimeout(()=>{btn.textContent='复制';},1500);
});
};
pre.style.position='relative';
pre.appendChild(btn);
});
}
// ═══════════════════════════════════════════════════════
// UI HELPERS
// ═══════════════════════════════════════════════════════
function botMsg(html){
removeTyping();
const row=document.createElement('div');
row.className='mrow b';
row.innerHTML='<div class="mav b">铸</div><div class="bbl">'+html+'</div>';
document.getElementById('msgs').appendChild(row);
scrollB();
return row.querySelector('.bbl');
}
function userMsg(t){
const meta=A.userMeta;
const av = meta? (meta.emoji||A.userName.slice(0,1)||'我') : '冰';
const row=document.createElement('div');
row.className='mrow u';
row.innerHTML='<div class="bbl">'+esc(t)+'</div><div class="mav u">'+av+'</div>';
document.getElementById('msgs').appendChild(row);
scrollB();
}
function addStreamBubble(){
removeTyping();
const row=document.createElement('div');
row.className='mrow b';
const bbl=document.createElement('div');
bbl.className='bbl';
bbl.innerHTML='<span class="cur">▋</span>';
row.innerHTML='<div class="mav b">铸</div>';
row.appendChild(bbl);
document.getElementById('msgs').appendChild(row);
scrollB();
return bbl;
}
function showTyping(){
removeTyping();
const row=document.createElement('div');
row.className='trow'; row.id='typr';
row.innerHTML='<div class="mav b">铸</div><div class="tbbl"><span></span><span></span><span></span></div>';
document.getElementById('msgs').appendChild(row);
scrollB();
}
function removeTyping(){const t=document.getElementById('typr');if(t)t.remove();}
function scrollB(){const m=document.getElementById('msgs');m.scrollTop=m.scrollHeight;}
function updSendBtn(){
const b=document.getElementById('sbtn');
b.disabled=A.streaming;
b.textContent=A.streaming?'…':'↑';
}
function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');}
function sleep(ms){return new Promise(r=>setTimeout(r,ms));}
// ═══════════════════════════════════════════════════════
// PANELS
// ═══════════════════════════════════════════════════════
function openPanel(id){
closeP();
if(id==='sp2') initSettingsPanel();
if(id==='bp') renderBrainPanel();
if(id==='tp') renderTeamPanel();
if(id==='hp'){
// Populate URL from constant
const el=document.getElementById('chatUrlDisplay');
if(el) el.textContent='🔗 '+CHAT_URL;
const li=document.getElementById('helpUrlStep');
if(li) li.innerHTML='打开链接 <strong>'+CHAT_URL+'</strong>';
}
document.getElementById(id).classList.add('open');
document.getElementById('pov').classList.add('on');
}
function closeP(){
document.querySelectorAll('.sp').forEach(p=>p.classList.remove('open'));
document.getElementById('pov').classList.remove('on');
}
// ═══════════════════════════════════════════════════════
// COPY URL (share link)
// ═══════════════════════════════════════════════════════
// Auto-detect the real URL so the share link is always correct
// regardless of whether Pages serves from docs/ or repo root.
const CHAT_URL = window.location.origin + window.location.pathname;
function showCopyFeedback(success){
const btn=document.querySelector('.copy-url-btn');
if(!btn) return;
const orig=btn.textContent;
btn.textContent = success ? '✅ 已复制!' : '⚠️ 复制失败,请手动复制';
setTimeout(()=>{btn.textContent=orig;},2000);
}
function copyUrl(){
navigator.clipboard.writeText(CHAT_URL).then(()=>{
showCopyFeedback(true);
}).catch(()=>{
// fallback for non-secure context
const ta=document.createElement('textarea');
ta.value=CHAT_URL; ta.style.position='fixed'; ta.style.opacity='0';
document.body.appendChild(ta); ta.focus(); ta.select();
let ok=false;
try{ok=document.execCommand('copy');}catch(_){}
document.body.removeChild(ta);
showCopyFeedback(ok);
});
}
// ═══════════════════════════════════════════════════════
// DOWNLOAD (desktop app)
// API keys are stored in localStorage only, never embedded in the DOM,
// so downloading outerHTML is safe — no sensitive data is captured.
// ═══════════════════════════════════════════════════════
function dlApp(){
const html=document.documentElement.outerHTML;
const blob=new Blob([html],{type:'text/html;charset=utf-8'});
const a=document.createElement('a');
a.href=URL.createObjectURL(blob);
a.download='铸渊助手.html';
a.click();
URL.revokeObjectURL(a.href);
}
// ═══════════════════════════════════════════════════════
// INPUT EVENTS
// ═══════════════════════════════════════════════════════
document.getElementById('inp').addEventListener('input',function(){
this.style.height='auto';
this.style.height=Math.min(this.scrollHeight,120)+'px';
});
document.getElementById('inp').addEventListener('keydown',function(e){
if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();}
});
// ═══════════════════════════════════════════════════════
// IDENTITY & ROLE SYSTEM
// ═══════════════════════════════════════════════════════
function setIdentity(userName, ghUser){
A.userName = userName||'';
A.ghUser = ghUser||'';
A.userMeta = userName ? (ROLE_MAP[userName]||null) : null;
A.role = A.userMeta?.role || 'guest';
lss('zy_uname', A.userName);
lss('zy_ghuser', A.ghUser);
lss('zy_role', A.role);
}
function applyIdentityUI(){
const meta = A.userMeta;
// Show user badge in header
const ubadge = document.getElementById('huser-badge');
const uname = document.getElementById('huser-name');
if(meta && A.userName){
ubadge.style.display='inline';
uname.style.display='inline';
uname.innerHTML = meta.emoji + ' ' + esc(A.userName) + ' <span class="role-badge '+meta.badgeCls+'">'+esc(meta.title)+'</span>';
}
// Show controller button for supreme/main/founder
const ctrlBtn = document.getElementById('ctrlBtn');
if(ctrlBtn) ctrlBtn.style.display = (meta?.role==='supreme'||meta?.role==='main'||meta?.role==='founder') ? 'flex' : 'none';
// Show controller quick replies
const isCtrl = meta?.role==='supreme'||meta?.role==='main'||meta?.role==='founder';
['qr-ctrl1','qr-ctrl2','qr-ctrl3'].forEach(id=>{
const el=document.getElementById(id);
if(el) el.style.display=isCtrl?'':'none';
});
}
// ═══════════════════════════════════════════════════════
// TEAM STATUS PANEL
// ═══════════════════════════════════════════════════════
function renderTeamPanel(){
const ds = A.devStatus;
const container = document.getElementById('tpb');
if(!ds){
container.innerHTML='<p style="color:var(--warn);font-size:13px">⚠️ 团队数据尚未加载。请检查网络连接后重试。</p>';
return;
}
const ts = ds.team_status||[];
const green = ts.filter(d=>d.status.includes('🟢')).length;
const yellow = ts.filter(d=>d.status.includes('🟡')).length;
const sync = ds.last_synced||'未知';
let h='';
// Summary block
h += '<div class="ctrl-panel-header">'
+ '<div class="ctrl-panel-title">🦁 总控指挥台</div>'
+ '<div class="ctrl-panel-sub">数据同步:'+esc(sync)+'</div>'
+ '</div>';
// Stats row
h += '<div class="team-summary">';
h += '<div class="team-summary-row"><span>总开发者</span><span class="iv">'+ts.length+' 人</span></div>';
h += '<div class="team-summary-row"><span>🟢 推进中</span><span style="color:var(--ok)">'+green+' 人</span></div>';
h += '<div class="team-summary-row"><span>🟡 等待中</span><span style="color:var(--warn)">'+yellow+' 人</span></div>';
h += '</div>';
// Quick chat commands for controllers
h += '<div style="margin-bottom:12px">';
h += '<div style="font-size:11px;color:var(--dim);margin-bottom:6px;text-transform:uppercase;letter-spacing:.05em">快捷指令</div>';
h += '<span class="cmd-chip" onclick="closeP();qs(\'查看所有开发者当前进度和阻塞点\')">📋 全员进度</span>';
h += '<span class="cmd-chip" onclick="closeP();qs(\'谁现在遇到阻塞?需要协调什么?\')">🆘 需要协调</span>';
h += '<span class="cmd-chip" onclick="closeP();qs(\'当前系统推进最关键的下一步是什么?\')">🗺️ 下一步</span>';
h += '<span class="cmd-chip" onclick="closeP();qs(\'前端整体进度怎么样?\')">🖥️ 前端进度</span>';
h += '<span class="cmd-chip" onclick="closeP();qs(\'后端和集成进度怎么样?\')">⚙️ 后端进度</span>';
h += '</div>';
// Individual dev cards
h += '<div class="team-grid">';
for(const dev of ts){
const isGreen = dev.status.includes('🟢');
const devRole = Object.values(ROLE_MAP).find(r=>r.devId===dev.dev_id);
const roleTag = devRole?.role==='supreme' ? '<span class="role-badge rb-supreme" style="margin-left:4px">总控</span>'
: devRole?.role==='main' ? '<span class="role-badge rb-main" style="margin-left:4px">主控</span>' : '';
h += '<div class="dev-card">';
h += '<div class="dev-card-head">';
h += '<span class="dev-name">'+esc(dev.name)+'</span>'+roleTag;
h += '<span class="dev-id-badge">'+esc(dev.dev_id)+'</span>';
h += '</div>';
h += '<div class="dev-status-text '+(isGreen?'status-green':'status-yellow')+'">'+esc(dev.status)+'</div>';
h += '<div class="dev-detail">📦 '+esc(dev.modules.join('、'))+'</div>';
if(dev.next_step) h += '<div class="dev-next">👉 '+esc(dev.next_step)+'</div>';
if(dev.waiting_for && dev.waiting_for.trim()!=='') h += '<div class="dev-wait">⏳ 等待:'+esc(dev.waiting_for)+'</div>';
h += '</div>';
}
h += '</div>';
// Server info
if(ds.server_info){
const si = ds.server_info;
h += '<div class="sec-ttl">服务器信息</div>';
h += ir2('地址','<span class="iv">'+esc(si.address||'')+'</span>');
h += ir2('域名','<span class="iv">'+esc(si.domain||'')+'</span>');
h += ir2('SSH','<code style="font-size:11px;color:var(--accent)">'+esc(si.ssh_command||'')+'</code>');
h += ir2('技术栈','<span style="font-size:11px;color:var(--dim)">'+esc(si.tech_stack||'')+'</span>');
}
// Tech routing
if(ds.tech_routing?.level_2_peer_help){
h += '<div class="sec-ttl">求助路由</div>';
for(const[area,who] of Object.entries(ds.tech_routing.level_2_peer_help)){
h += ir2(esc(area),'<span style="font-size:11px;color:var(--dim)">'+esc(who)+'</span>');
}
}
container.innerHTML = h;
}
// ═══════════════════════════════════════════════════════
// BOOT
// ═══════════════════════════════════════════════════════
boot();
</script>
</body>
</html>