feat(t131): AI 設定合併成一把金鑰——聊天與萃取共用,Claude 為選填加強

leo:「拿到一把就很難了,還要拿兩把。一律規定先輸入 gemini api key,
如果想要強化本地萃取,可以選擇 claude……前面那個,聊天和萃都一次設好,後面那把,不填就是 gemini」
+「重點是更好的模型萃取知識更能抓重點,不然他也不知道加強什麼」(文案講感覺得到的差別)
+「本地如果有裝 claude,掃到,只要一個 checkbox 就好」(daemon 偵測回報,沒偵測到就停用選項)。
- POST/GET /portal/admin/ai(一次寫 chat-key 與 extractor)+/portal/daemon/report-capabilities
- 舊 chat-key/extractor 端點保留相容;UI 兩區合併成「AI 設定」
t131 新測 7 條全綠(總體 229 passed/9 紅皆 pre-existing:HTML shell 搬遷×2、library-map 未實作×6、
executor 零件×1)。(實作=子 CC;驗證+commit=總管)
This commit is contained in:
uncle6me-web
2026-07-29 15:28:10 +08:00
parent 0860e84d22
commit 9d38d580f2
4 changed files with 412 additions and 127 deletions
+92 -123
View File
@@ -347,36 +347,32 @@
<div style="margin-top:8px;font-size:13px;line-height:1.7;color:rgba(var(--ink-rgb),.5)">封測版未簽章,第一次請右鍵→打開。裝好第一次開啟時,貼上這個網址+你的帳號密碼就連上了。</div>
</div>
</div>
<div class="panel">
<div style="font-size:17px;font-weight:600">AI 問答金鑰</div>
<div style="margin-top:4px;font-size:14px;line-height:1.65;color:rgba(var(--ink-rgb),.55)">用來啟用「問 AI」。到 <a href="https://aistudio.google.com" target="_blank" rel="noopener">aistudio.google.com</a> 免費申請。金鑰只存在你自己的知識庫裡。</div>
<div style="margin-top:12px;display:flex;flex-direction:column;gap:10px">
<input type="password" id="st-key" class="txt" placeholder="貼上 Google AI 金鑰(留空=不變更)" autocomplete="off">
<button class="btn" id="st-key-save">儲存金鑰</button>
<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>
<!-- t131 合併 AI 設定(Gemini API Key 必填;Claude 加強版視 daemon 回報 enable -->
<div class="panel" id="st-ai-panel">
<div style="font-size:17px;font-weight:600">AI 設定</div>
<div style="margin-top:14px;display:flex;flex-direction:column;gap:16px">
<!-- 第一格:Gemini API Key(必填,聊天+萃取共用) -->
<div>
<div style="font-size:14px;font-weight:500;margin-bottom:4px">Gemini API Key</div>
<div style="font-size:13px;line-height:1.6;color:rgba(var(--ink-rgb),.55);margin-bottom:8px">聊天問答與文件萃取都用這一把。<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener">免費申請</a>,金鑰只存在你自己的知識庫裡。</div>
<input type="password" id="st-ai-key" class="txt" placeholder="貼上 Gemini API Key" autocomplete="off">
</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">
<!-- 第二格:Claude 加強版(選填,依 daemon 回報 enable -->
<div style="padding-top:12px;border-top:1px solid rgba(var(--ink-rgb),.08)">
<div style="font-size:14px;font-weight:500;margin-bottom:6px">讓知識卡整理得更好(選填)</div>
<label style="display:flex;gap:8px;align-items:flex-start;cursor:pointer">
<input type="checkbox" id="st-ai-use-claude" style="margin-top:3px;flex:none" disabled>
<span style="font-size:14px">本地萃取改用 Claude Code</span>
</label>
<div id="st-ai-claude-desc" style="margin-top:8px;font-size:13px;line-height:1.65;color:rgba(var(--ink-rgb),.55)">
讀你文件、整理成知識卡的那個 AI,換成更強的模型。<b style="color:var(--ink)">卡片會更抓得到重點、關聯也連得更準</b>,之後搜尋和問答的品質跟著提升。需要你的電腦已安裝 Claude Code。不填就用上面那把 Gemini,一樣能用。
</div>
<div id="st-ai-claude-hint" style="margin-top:6px;font-size:13px;color:rgba(var(--ink-rgb),.4);display:none"></div>
</div>
<div>
<button class="btn" id="st-ai-save">儲存 AI 設定</button>
<div id="st-ai-status" style="font-size:14px;min-height:1.2em;margin-top:8px"></div>
</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>
@@ -788,97 +784,72 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
if (dl.parentNode) dl.parentNode.insertBefore(both, dl.nextSibling);
}
}
var kb = $('st-key-save');
if (kb) kb.addEventListener('click', function () {
var k = ($('st-key').value || '').trim();
var m = $('st-key-status');
if (!k) { m.textContent = '請先貼上金鑰'; m.style.color = ''; return; }
kb.disabled = true; m.textContent = '儲存中…'; m.style.color = '';
fetch(API_BASE + '/portal/admin/chat-key', {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()),
body: JSON.stringify({ key: k })
}).then(function (r) { return safeJson(r).then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
.then(function (x) {
kb.disabled = false;
if (guard401(x.status)) return;
// t75 ①:404=這個實例的伺服器版本還沒有這個功能(不是使用者做錯)。
// 講清楚「為什麼」與「怎麼辦」,否則他只看到「儲存失敗」會反覆重試同一件事。
if (x.status === 404) {
m.textContent = '你的知識庫版本還沒有這個功能,請先更新知識庫再回來設定金鑰';
m.style.color = '#b4462f'; return;
}
if (!x.ok) { m.textContent = (x.d && x.d.error) || '儲存失敗'; m.style.color = '#b4462f'; return; }
$('st-key').value = '';
m.textContent = '已儲存,AI 問答可以用了'; m.style.color = '#3f7a4f';
})
.catch(function (e) { kb.disabled = false; m.textContent = friendlyErr(e); m.style.color = '#b4462f'; });
});
})();
// t131 AI 設定(合併 Gemini API KeyClaude 加強版)
(function () {
// 進入設定頁時讀取現有設定(GET /portal/admin/ai
function loadAiConfig() {
if (!(S.profile && S.profile.role === 'admin')) return;
fetch(API_BASE + '/portal/admin/ai', { headers: authHeaders() })
.then(function (r) { return r.ok ? safeJson(r) : null; })
.then(function (d) {
if (!d) return;
var ki = $('st-ai-key');
if (ki && d.has_key) ki.placeholder = '已設定(留空=不變更)';
var cb = $('st-ai-use-claude');
if (cb) {
// 有 claude 才能勾;沒有則停用並顯示提示
var hint = $('st-ai-claude-hint');
if (d.claude_available) {
cb.disabled = false;
cb.checked = !!d.use_claude_for_extract;
if (hint) hint.style.display = 'none';
} else {
cb.disabled = true;
cb.checked = false;
if (hint) {
hint.style.display = '';
// 區分:從未連上小幫手 vs 有連上但沒裝 Claude
hint.textContent = d.has_key
? '你的電腦沒有偵測到 Claude Code;裝好並讓小幫手重連一次後,這個選項就會開啟。'
: '連上小幫手後才知道你的電腦有沒有 Claude Code。';
}
}
}
})
.catch(function () { /* 讀不到不擋頁面 */ });
}
window._loadAiConfig = loadAiConfig;
// 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;
})();
// 儲存
var sb = $('st-ai-save');
if (sb) sb.addEventListener('click', function () {
var m = $('st-ai-status');
var k = (($('st-ai-key') && $('st-ai-key').value) || '').trim();
var cb = $('st-ai-use-claude');
var useClaud = cb && !cb.disabled ? cb.checked : undefined;
var body = {};
if (k) body.gemini_api_key = k;
if (useClaud !== undefined) body.use_claude_for_extract = useClaud;
sb.disabled = true; m.textContent = '儲存中…'; m.style.color = '';
fetch(API_BASE + '/portal/admin/ai', {
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-ai-key')) { $('st-ai-key').value = ''; $('st-ai-key').placeholder = '已設定(留空=不變更)'; }
var claudeOn = x.d && x.d.use_claude_for_extract;
m.textContent = claudeOn
? '已儲存,萃取改用 Claude Code。小幫手請重連一次生效。'
: '已儲存,AI 問答與萃取皆可使用。';
m.style.color = '#3f7a4f';
})
.catch(function (e) { sb.disabled = false; m.textContent = friendlyErr(e); m.style.color = '#b4462f'; });
});
})();
$('st-logout').addEventListener('click', function () {
fetch(API_BASE + '/portal/logout', { method: 'POST', headers: authHeaders() }).catch(function () { /* 盡力而為 */ });
@@ -973,15 +944,13 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
var m = document.getElementById('sc-key-msg');
if (!k) { m.textContent = '請先貼上金鑰'; return; }
kb.disabled = true; m.textContent = '啟用中…';
fetch(API_BASE + '/portal/admin/chat-key', {
fetch(API_BASE + '/portal/admin/ai', {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()),
body: JSON.stringify({ key: k })
body: JSON.stringify({ gemini_api_key: k })
}).then(function (r) { return safeJson(r).then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
.then(function (x) {
kb.disabled = false;
// t75 ①:同設定頁——404=實例版本太舊,不是使用者的錯。
if (x.status === 404) { m.textContent = '你的知識庫版本還沒有這個功能,請先更新知識庫'; return; }
if (!x.ok || !x.d.success) { m.textContent = (x.d && x.d.error) || '啟用失敗,請再試一次'; return; }
markStep('key');
})
@@ -1735,7 +1704,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();
if (window._loadAiConfig) window._loadAiConfig();
var libs = p.libraries || [];
var libHtml = libs.indexOf('*') >= 0
? '<span class="tag green">全部知識庫</span>'