feat(t122 🔴🔴): 萃取引擎雲端設定+隨連線下發——封測者終於萃得出東西

真兇(總管查證):daemon/config 寫死 extractor='claude' 且不下發金鑰 ⇒ 封測者 100% 萃取失敗
(leo:「地端沒有 AI 根本不能萃,那它就不能玩」)。
- POST/GET /portal/admin/extractor(admin 閘;GET 只回 has_key 不回明文)
- daemon/config 改讀設定:未設定→gemma 無金鑰;設定後→含 gemini_api_key
- portal 設定頁加「萃取引擎」區(Gemini 推薦+aistudio 連結,存後提示「小幫手點連上知識庫重連即可」)
測試 3 條新綠(vitest 17 passed;1 紅=console HTML 搬遷陳舊測試,非本案)。
(實作=子 CC;驗證+commit=總管)
This commit is contained in:
uncle6me-web
2026-07-29 13:21:26 +08:00
parent 429b2d965f
commit eb9f2db513
4 changed files with 255 additions and 12 deletions
+88
View File
@@ -356,6 +356,29 @@
<div id="st-key-status" style="font-size:14px;min-height:1.2em"></div>
</div>
</div>
<!-- t122 萃取引擎(daemon 萃取知識卡用的 AIadmin-level,同 chat-keyserver 再強制閘) -->
<div class="panel" id="st-extractor-panel">
<div style="font-size:17px;font-weight:600">萃取引擎</div>
<div style="margin-top:4px;font-size:14px;line-height:1.65;color:rgba(var(--ink-rgb),.55)">同步小幫手把你的文件變成知識卡時用的 AI。選好後請讓小幫手重連一次。</div>
<div style="margin-top:14px;display:flex;flex-direction:column;gap:10px">
<div style="display:flex;gap:16px;align-items:center">
<label style="display:flex;gap:6px;align-items:center;cursor:pointer;font-size:14px">
<input type="radio" name="st-extractor-engine" id="st-engine-gemma" value="gemma" checked>
<span>Gemini(推薦,免費)</span>
</label>
<label style="display:flex;gap:6px;align-items:center;cursor:pointer;font-size:14px">
<input type="radio" name="st-extractor-engine" id="st-engine-claude" value="claude">
<span>Claude Code(本機已裝才選)</span>
</label>
</div>
<div id="st-extractor-key-row" style="display:flex;flex-direction:column;gap:8px">
<div style="font-size:13px;color:rgba(var(--ink-rgb),.55)"><a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener">aistudio.google.com/apikey</a> 免費取得金鑰</div>
<input type="password" id="st-extractor-key" class="txt" placeholder="貼上 Gemini API 金鑰(留空=不變更)" autocomplete="off">
</div>
<button class="btn" id="st-extractor-save">儲存萃取引擎設定</button>
<div id="st-extractor-status" style="font-size:14px;min-height:1.2em"></div>
</div>
</div>
<button class="btn3" id="st-logout" style="padding:14px;font-size:16px;border-radius:11px">登出</button>
</div>
</div>
@@ -793,6 +816,70 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
});
})();
// t122 萃取引擎設定(engine 切換顯示隱藏金鑰輸入框 + 存檔)
(function () {
// 引擎切換:Gemini 顯示金鑰欄;Claude Code 隱藏
function updateExtractorKeyRowVisibility() {
var row = $('st-extractor-key-row');
if (!row) return;
var el = document.getElementById('st-engine-gemma');
row.style.display = (el && el.checked) ? '' : 'none';
}
['st-engine-gemma', 'st-engine-claude'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.addEventListener('change', updateExtractorKeyRowVisibility);
});
updateExtractorKeyRowVisibility();
// 進入設定頁時讀取現有設定(GET /portal/admin/extractor
function loadExtractorConfig() {
if (!(S.profile && S.profile.role === 'admin')) return;
fetch(API_BASE + '/portal/admin/extractor', { headers: authHeaders() })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (!d) return;
var gemmaEl = document.getElementById('st-engine-gemma');
var claudeEl = document.getElementById('st-engine-claude');
if (d.engine === 'claude' && claudeEl) claudeEl.checked = true;
else if (gemmaEl) gemmaEl.checked = true;
updateExtractorKeyRowVisibility();
if (d.has_key) {
var ki = $('st-extractor-key');
if (ki) ki.placeholder = '已設定(留空=不變更)';
}
})
.catch(function () { /* 讀不到不擋頁面 */ });
}
// 存檔
var sb = $('st-extractor-save');
if (sb) sb.addEventListener('click', function () {
var m = $('st-extractor-status');
var engineEl = document.querySelector('input[name="st-extractor-engine"]:checked');
var engine = engineEl ? engineEl.value : 'gemma';
var key = (($('st-extractor-key') && $('st-extractor-key').value) || '').trim();
var body = { engine: engine };
if (engine === 'gemma' && key) body.gemini_api_key = key;
sb.disabled = true; m.textContent = '儲存中…'; m.style.color = '';
fetch(API_BASE + '/portal/admin/extractor', {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()),
body: JSON.stringify(body)
}).then(function (r) { return safeJson(r).then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
.then(function (x) {
sb.disabled = false;
if (guard401(x.status)) return;
if (!x.ok) { m.textContent = (x.d && x.d.error) || '儲存失敗'; m.style.color = '#b4462f'; return; }
if ($('st-extractor-key')) { $('st-extractor-key').value = ''; $('st-extractor-key').placeholder = '已設定(留空=不變更)'; }
m.textContent = '已儲存。小幫手請點「連上知識庫」重連一次即可生效'; m.style.color = '#3f7a4f';
})
.catch(function (e) { sb.disabled = false; m.textContent = friendlyErr(e); m.style.color = '#b4462f'; });
});
// loadExtractorConfig 掛到全域(供 loadSettings 呼叫)
window._loadExtractorConfig = loadExtractorConfig;
})();
$('st-logout').addEventListener('click', function () {
fetch(API_BASE + '/portal/logout', { method: 'POST', headers: authHeaders() }).catch(function () { /* 盡力而為 */ });
dropSession();
@@ -1605,6 +1692,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
function loadSettings() {
var p = S.profile;
if (!p) { $('st-me').innerHTML = '<div class="muted">載入中…</div>'; return; }
if (window._loadExtractorConfig) window._loadExtractorConfig();
var libs = p.libraries || [];
var libHtml = libs.indexOf('*') >= 0
? '<span class="tag green">全部知識庫</span>'