Add L0 index and anchor UI updates
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// story-summary-ui.js
|
||||
// story-summary-ui.js
|
||||
// iframe 内 UI 逻辑
|
||||
|
||||
(function () {
|
||||
@@ -73,33 +73,6 @@
|
||||
'陌生': 'trend-stranger', '投缘': 'trend-click', '亲密': 'trend-close', '交融': 'trend-merge'
|
||||
};
|
||||
|
||||
const LOCAL_MODELS_INFO = {
|
||||
'bge-small-zh': { desc: '手机/低配适用' },
|
||||
'bge-base-zh': { desc: 'PC 推荐,效果更好' },
|
||||
'e5-small': { desc: '非中文用户' }
|
||||
};
|
||||
|
||||
const ONLINE_PROVIDERS_INFO = {
|
||||
siliconflow: {
|
||||
url: 'https://api.siliconflow.cn',
|
||||
models: ['BAAI/bge-m3', 'BAAI/bge-large-zh-v1.5', 'BAAI/bge-small-zh-v1.5'],
|
||||
hint: '💡 <a href="https://siliconflow.cn" target="_blank">硅基流动</a> 注册即送额度,推荐 BAAI/bge-m3',
|
||||
canFetch: false, urlEditable: false
|
||||
},
|
||||
cohere: {
|
||||
url: 'https://api.cohere.ai',
|
||||
models: ['embed-multilingual-v3.0', 'embed-english-v3.0'],
|
||||
hint: '💡 <a href="https://cohere.com" target="_blank">Cohere</a> 提供免费试用额度',
|
||||
canFetch: false, urlEditable: false
|
||||
},
|
||||
openai: {
|
||||
url: '',
|
||||
models: [],
|
||||
hint: '💡 可用 Hugging Face Space 免费自建<br><button class="btn btn-sm" id="btn-hf-guide" style="margin-top:6px">查看部署指南</button>',
|
||||
canFetch: true, urlEditable: true
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_FILTER_RULES = [
|
||||
{ start: '<think>', end: '</think>' },
|
||||
{ start: '<thinking>', end: '</thinking>' },
|
||||
@@ -119,6 +92,7 @@
|
||||
let summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], facts: [] };
|
||||
let localGenerating = false;
|
||||
let vectorGenerating = false;
|
||||
let anchorGenerating = false;
|
||||
let relationChart = null;
|
||||
let relationChartFullscreen = null;
|
||||
let currentEditSection = null;
|
||||
@@ -172,7 +146,7 @@
|
||||
const settingsOpen = $('settings-modal')?.classList.contains('active');
|
||||
if (settingsOpen) config.vector = getVectorConfig();
|
||||
if (!config.vector) {
|
||||
config.vector = { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } };
|
||||
config.vector = { enabled: false, engine: 'online', online: { provider: 'siliconflow', key: '', model: 'BAAI/bge-m3' } };
|
||||
}
|
||||
localStorage.setItem('summary_panel_config', JSON.stringify(config));
|
||||
postMsg('SAVE_PANEL_CONFIG', { config });
|
||||
@@ -186,38 +160,16 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function getVectorConfig() {
|
||||
const safeVal = (id, fallback) => {
|
||||
const el = $(id);
|
||||
if (!el) return fallback;
|
||||
return el.type === 'checkbox' ? el.checked : (el.value?.trim() || fallback);
|
||||
};
|
||||
const safeRadio = (name, fallback) => {
|
||||
const el = document.querySelector(`input[name="${name}"]:checked`);
|
||||
return el?.value || fallback;
|
||||
};
|
||||
const modelSelect = $('vector-model-select');
|
||||
const modelCache = [];
|
||||
if (modelSelect) {
|
||||
for (const opt of modelSelect.options) {
|
||||
if (opt.value) modelCache.push(opt.value);
|
||||
}
|
||||
}
|
||||
const result = {
|
||||
enabled: safeVal('vector-enabled', false),
|
||||
engine: safeRadio('vector-engine', 'online'),
|
||||
local: { modelId: safeVal('local-model-select', 'bge-small-zh') },
|
||||
return {
|
||||
enabled: $('vector-enabled')?.checked || false,
|
||||
engine: 'online',
|
||||
online: {
|
||||
provider: safeVal('online-provider', 'siliconflow'),
|
||||
url: safeVal('vector-api-url', ''),
|
||||
key: safeVal('vector-api-key', ''),
|
||||
model: safeVal('vector-model-select', ''),
|
||||
modelCache
|
||||
}
|
||||
provider: 'siliconflow',
|
||||
key: $('vector-api-key')?.value?.trim() || '',
|
||||
model: 'BAAI/bge-m3',
|
||||
},
|
||||
textFilterRules: collectFilterRules(),
|
||||
};
|
||||
|
||||
// 收集过滤规则
|
||||
result.textFilterRules = collectFilterRules();
|
||||
return result;
|
||||
}
|
||||
|
||||
function loadVectorConfig(cfg) {
|
||||
@@ -225,70 +177,14 @@
|
||||
$('vector-enabled').checked = !!cfg.enabled;
|
||||
$('vector-config-area').classList.toggle('hidden', !cfg.enabled);
|
||||
|
||||
const engine = cfg.engine || 'online';
|
||||
const engineRadio = document.querySelector(`input[name="vector-engine"][value="${engine}"]`);
|
||||
if (engineRadio) engineRadio.checked = true;
|
||||
|
||||
$('local-engine-area').classList.toggle('hidden', engine !== 'local');
|
||||
$('online-engine-area').classList.toggle('hidden', engine !== 'online');
|
||||
|
||||
if (cfg.local?.modelId) {
|
||||
$('local-model-select').value = cfg.local.modelId;
|
||||
updateLocalModelDesc(cfg.local.modelId);
|
||||
}
|
||||
if (cfg.online) {
|
||||
const provider = cfg.online.provider || 'siliconflow';
|
||||
$('online-provider').value = provider;
|
||||
updateOnlineProviderUI(provider);
|
||||
if (cfg.online.url) $('vector-api-url').value = cfg.online.url;
|
||||
if (cfg.online.key) $('vector-api-key').value = cfg.online.key;
|
||||
if (cfg.online.modelCache?.length) {
|
||||
setSelectOptions($('vector-model-select'), cfg.online.modelCache);
|
||||
}
|
||||
if (cfg.online.model) $('vector-model-select').value = cfg.online.model;
|
||||
if (cfg.online?.key) {
|
||||
$('vector-api-key').value = cfg.online.key;
|
||||
}
|
||||
|
||||
// 加载过滤规则
|
||||
renderFilterRules(cfg?.textFilterRules || DEFAULT_FILTER_RULES);
|
||||
}
|
||||
|
||||
function updateLocalModelDesc(modelId) {
|
||||
const info = LOCAL_MODELS_INFO[modelId];
|
||||
$('local-model-desc').textContent = info?.desc || '';
|
||||
}
|
||||
|
||||
function updateOnlineProviderUI(provider) {
|
||||
const info = ONLINE_PROVIDERS_INFO[provider];
|
||||
if (!info) return;
|
||||
|
||||
const urlInput = $('vector-api-url');
|
||||
const urlRow = $('online-url-row');
|
||||
if (info.urlEditable) {
|
||||
urlInput.value = urlInput.value || '';
|
||||
urlInput.disabled = false;
|
||||
urlRow.style.display = '';
|
||||
} else {
|
||||
urlInput.value = info.url;
|
||||
urlInput.disabled = true;
|
||||
urlRow.style.display = 'none';
|
||||
}
|
||||
|
||||
const modelSelect = $('vector-model-select');
|
||||
const fetchBtn = $('btn-fetch-models');
|
||||
if (info.canFetch) {
|
||||
fetchBtn.style.display = '';
|
||||
setHtml(modelSelect, '<option value="">点击拉取或手动输入</option>');
|
||||
} else {
|
||||
fetchBtn.style.display = 'none';
|
||||
setSelectOptions(modelSelect, info.models);
|
||||
}
|
||||
|
||||
setHtml($('provider-hint'), info.hint);
|
||||
const guideBtn = $('btn-hf-guide');
|
||||
if (guideBtn) guideBtn.onclick = e => { e.preventDefault(); openHfGuide(); };
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Filter Rules UI
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -352,31 +248,6 @@
|
||||
list.appendChild(div);
|
||||
}
|
||||
|
||||
function updateLocalModelStatus(status, message) {
|
||||
const dot = $('local-model-status').querySelector('.status-dot');
|
||||
const text = $('local-model-status').querySelector('.status-text');
|
||||
dot.className = 'status-dot ' + status;
|
||||
text.textContent = message;
|
||||
|
||||
const btnDownload = $('btn-download-model');
|
||||
const btnCancel = $('btn-cancel-download');
|
||||
const btnDelete = $('btn-delete-model');
|
||||
const progress = $('local-model-progress');
|
||||
|
||||
btnDownload.style.display = (status === 'not_downloaded' || status === 'cached' || status === 'error') ? '' : 'none';
|
||||
btnCancel.style.display = (status === 'downloading') ? '' : 'none';
|
||||
btnDelete.style.display = (status === 'ready' || status === 'cached') ? '' : 'none';
|
||||
progress.classList.toggle('hidden', status !== 'downloading');
|
||||
|
||||
btnDownload.textContent = status === 'cached' ? '加载模型' : status === 'error' ? '重试下载' : '下载模型';
|
||||
}
|
||||
|
||||
function updateLocalModelProgress(percent) {
|
||||
const progress = $('local-model-progress');
|
||||
progress.classList.remove('hidden');
|
||||
progress.querySelector('.progress-inner').style.width = percent + '%';
|
||||
progress.querySelector('.progress-text').textContent = percent + '%';
|
||||
}
|
||||
|
||||
function updateOnlineStatus(status, message) {
|
||||
const dot = $('online-api-status').querySelector('.status-dot');
|
||||
@@ -385,116 +256,129 @@
|
||||
text.textContent = message;
|
||||
}
|
||||
|
||||
function updateOnlineModels(models) {
|
||||
const select = $('vector-model-select');
|
||||
const current = select.value;
|
||||
setSelectOptions(select, models);
|
||||
if (current && models.includes(current)) select.value = current;
|
||||
if (!config.vector) config.vector = { enabled: false, engine: 'online', local: {}, online: {} };
|
||||
if (!config.vector.online) config.vector.online = {};
|
||||
config.vector.online.modelCache = [...models];
|
||||
}
|
||||
|
||||
function updateVectorStats(stats) {
|
||||
$('vector-atom-count').textContent = stats.stateAtoms || 0;
|
||||
$('vector-chunk-count').textContent = stats.chunkCount || 0;
|
||||
$('vector-event-count').textContent = stats.eventVectors || 0;
|
||||
if ($('vector-event-total')) $('vector-event-total').textContent = stats.eventCount || 0;
|
||||
if ($('vector-chunk-count')) $('vector-chunk-count').textContent = stats.chunkCount || 0;
|
||||
if ($('vector-chunk-floors')) $('vector-chunk-floors').textContent = stats.builtFloors || 0;
|
||||
if ($('vector-chunk-total')) $('vector-chunk-total').textContent = stats.totalFloors || 0;
|
||||
if ($('vector-message-count')) $('vector-message-count').textContent = stats.totalMessages || 0;
|
||||
}
|
||||
|
||||
function updateVectorGenProgress(phase, current, total) {
|
||||
const progressId = phase === 'L1' ? 'vector-gen-progress-l1' : 'vector-gen-progress-l2';
|
||||
const progress = $(progressId);
|
||||
const btnGen = $('btn-gen-vectors');
|
||||
const btnCancel = $('btn-cancel-vectors');
|
||||
const btnClear = $('btn-clear-vectors');
|
||||
|
||||
if (current < 0) {
|
||||
progress.classList.add('hidden');
|
||||
const l1Hidden = $('vector-gen-progress-l1').classList.contains('hidden');
|
||||
const l2Hidden = $('vector-gen-progress-l2').classList.contains('hidden');
|
||||
if (l1Hidden && l2Hidden) {
|
||||
btnGen.classList.remove('hidden');
|
||||
btnCancel.classList.add('hidden');
|
||||
btnClear.classList.remove('hidden');
|
||||
vectorGenerating = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
vectorGenerating = true;
|
||||
progress.classList.remove('hidden');
|
||||
btnGen.classList.add('hidden');
|
||||
btnCancel.classList.remove('hidden');
|
||||
btnClear.classList.add('hidden');
|
||||
|
||||
const percent = total > 0 ? Math.round(current / total * 100) : 0;
|
||||
progress.querySelector('.progress-inner').style.width = percent + '%';
|
||||
progress.querySelector('.progress-text').textContent = `${current}/${total}`;
|
||||
}
|
||||
|
||||
function showVectorMismatchWarning(show) {
|
||||
$('vector-mismatch-warning').classList.toggle('hidden', !show);
|
||||
}
|
||||
|
||||
function initVectorUI() {
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 记忆锚点(L0)UI
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateAnchorStats(stats) {
|
||||
const extracted = stats.extracted || 0;
|
||||
const total = stats.total || 0;
|
||||
const pending = stats.pending || 0;
|
||||
const empty = stats.empty || 0;
|
||||
const fail = stats.fail || 0;
|
||||
|
||||
$('anchor-extracted').textContent = extracted;
|
||||
$('anchor-total').textContent = total;
|
||||
$('anchor-pending').textContent = pending;
|
||||
|
||||
const extra = document.getElementById('anchor-extra');
|
||||
if (extra) extra.textContent = `空 ${empty} · 失败 ${fail}`;
|
||||
|
||||
const pendingWrap = $('anchor-pending-wrap');
|
||||
if (pendingWrap) {
|
||||
pendingWrap.classList.toggle('hidden', pending === 0);
|
||||
}
|
||||
|
||||
const emptyWarning = $('vector-empty-l0-warning');
|
||||
if (emptyWarning) {
|
||||
emptyWarning.classList.toggle('hidden', extracted > 0);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAnchorProgress(current, total, message) {
|
||||
const progress = $('anchor-progress');
|
||||
const btnGen = $('btn-anchor-generate');
|
||||
const btnClear = $('btn-anchor-clear');
|
||||
const btnCancel = $('btn-anchor-cancel');
|
||||
|
||||
if (current < 0) {
|
||||
progress.classList.add('hidden');
|
||||
btnGen.classList.remove('hidden');
|
||||
btnClear.classList.remove('hidden');
|
||||
btnCancel.classList.add('hidden');
|
||||
anchorGenerating = false;
|
||||
} else {
|
||||
anchorGenerating = true;
|
||||
progress.classList.remove('hidden');
|
||||
btnGen.classList.add('hidden');
|
||||
btnClear.classList.add('hidden');
|
||||
btnCancel.classList.remove('hidden');
|
||||
|
||||
const percent = total > 0 ? Math.round(current / total * 100) : 0;
|
||||
progress.querySelector('.progress-inner').style.width = percent + '%';
|
||||
progress.querySelector('.progress-text').textContent = message || `${current}/${total}`;
|
||||
}
|
||||
}
|
||||
|
||||
function initAnchorUI() {
|
||||
$('btn-anchor-generate').onclick = () => {
|
||||
if (anchorGenerating) return;
|
||||
postMsg('ANCHOR_GENERATE');
|
||||
};
|
||||
|
||||
$('btn-anchor-clear').onclick = () => {
|
||||
if (confirm('清空所有记忆锚点?(L0 向量也会一并清除)')) {
|
||||
postMsg('ANCHOR_CLEAR');
|
||||
}
|
||||
};
|
||||
|
||||
$('btn-anchor-cancel').onclick = () => {
|
||||
postMsg('ANCHOR_CANCEL');
|
||||
};
|
||||
}
|
||||
|
||||
function initVectorUI() {
|
||||
$('vector-enabled').onchange = e => {
|
||||
$('vector-config-area').classList.toggle('hidden', !e.target.checked);
|
||||
};
|
||||
document.querySelectorAll('input[name="vector-engine"]').forEach(radio => {
|
||||
radio.onchange = e => {
|
||||
const isLocal = e.target.value === 'local';
|
||||
$('local-engine-area').classList.toggle('hidden', !isLocal);
|
||||
$('online-engine-area').classList.toggle('hidden', isLocal);
|
||||
};
|
||||
});
|
||||
$('local-model-select').onchange = e => {
|
||||
updateLocalModelDesc(e.target.value);
|
||||
postMsg('VECTOR_CHECK_LOCAL_MODEL', { modelId: e.target.value });
|
||||
};
|
||||
$('online-provider').onchange = e => updateOnlineProviderUI(e.target.value);
|
||||
$('btn-download-model').onclick = () => postMsg('VECTOR_DOWNLOAD_MODEL', { modelId: $('local-model-select').value });
|
||||
$('btn-cancel-download').onclick = () => postMsg('VECTOR_CANCEL_DOWNLOAD');
|
||||
$('btn-delete-model').onclick = () => {
|
||||
if (confirm('确定删除本地模型缓存?')) postMsg('VECTOR_DELETE_MODEL', { modelId: $('local-model-select').value });
|
||||
};
|
||||
$('btn-fetch-models').onclick = () => {
|
||||
postMsg('VECTOR_FETCH_MODELS', { config: { url: $('vector-api-url').value.trim(), key: $('vector-api-key').value.trim() } });
|
||||
};
|
||||
|
||||
$('btn-test-vector-api').onclick = () => {
|
||||
postMsg('VECTOR_TEST_ONLINE', {
|
||||
provider: $('online-provider').value,
|
||||
config: { url: $('vector-api-url').value.trim(), key: $('vector-api-key').value.trim(), model: $('vector-model-select').value.trim() }
|
||||
provider: 'siliconflow',
|
||||
config: {
|
||||
key: $('vector-api-key').value.trim(),
|
||||
model: 'BAAI/bge-m3',
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 过滤规则:添加按钮
|
||||
$('btn-add-filter-rule').onclick = addFilterRule;
|
||||
|
||||
$('btn-gen-vectors').onclick = () => {
|
||||
if (vectorGenerating) return;
|
||||
postMsg('VECTOR_GENERATE', { config: getVectorConfig() });
|
||||
};
|
||||
|
||||
$('btn-clear-vectors').onclick = () => {
|
||||
if (confirm('确定清除当前聊天的向量数据?')) postMsg('VECTOR_CLEAR');
|
||||
if (confirm('?????????')) postMsg('VECTOR_CLEAR');
|
||||
};
|
||||
|
||||
$('btn-cancel-vectors').onclick = () => postMsg('VECTOR_CANCEL_GENERATE');
|
||||
|
||||
// 导入导出
|
||||
$('btn-export-vectors').onclick = () => {
|
||||
$('btn-export-vectors').disabled = true;
|
||||
$('vector-io-status').textContent = '导出中...';
|
||||
$('vector-io-status').textContent = '???...';
|
||||
postMsg('VECTOR_EXPORT');
|
||||
};
|
||||
|
||||
$('btn-import-vectors').onclick = () => {
|
||||
// 让 parent 处理文件选择,避免 iframe 传大文件
|
||||
$('btn-import-vectors').disabled = true;
|
||||
$('vector-io-status').textContent = '导入中...';
|
||||
$('vector-io-status').textContent = '???...';
|
||||
postMsg('VECTOR_IMPORT_PICK');
|
||||
};
|
||||
|
||||
initAnchorUI();
|
||||
postMsg('REQUEST_ANCHOR_STATS');
|
||||
}
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Settings Modal
|
||||
@@ -1039,172 +923,48 @@
|
||||
postMsg('FULLSCREEN_CLOSED');
|
||||
}
|
||||
|
||||
function openHfGuide() {
|
||||
$('hf-guide-modal').classList.add('active');
|
||||
renderHfGuideContent();
|
||||
postMsg('FULLSCREEN_OPENED');
|
||||
}
|
||||
function renderArcsEditor(arcs) {
|
||||
const list = arcs?.length ? arcs : [{ name: '', trajectory: '', progress: 0, moments: [] }];
|
||||
const es = $('editor-struct');
|
||||
|
||||
function closeHfGuide() {
|
||||
$('hf-guide-modal').classList.remove('active');
|
||||
postMsg('FULLSCREEN_CLOSED');
|
||||
}
|
||||
|
||||
function renderHfGuideContent() {
|
||||
const body = $('hf-guide-body');
|
||||
if (!body || body.innerHTML.trim()) return;
|
||||
|
||||
setHtml(body, `
|
||||
<div class="hf-guide">
|
||||
<div class="hf-section hf-intro">
|
||||
<div class="hf-intro-text"><strong>免费自建 Embedding 服务</strong>,10 分钟搞定</div>
|
||||
<div class="hf-intro-badges">
|
||||
<span class="hf-badge">🆓 完全免费</span>
|
||||
<span class="hf-badge">⚡ 速度不快</span>
|
||||
<span class="hf-badge">🔐 数据私有</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hf-section">
|
||||
<div class="hf-step-header"><span class="hf-step-num">1</span><span class="hf-step-title">创建 Space</span></div>
|
||||
<div class="hf-step-content">
|
||||
<p>访问 <a href="https://huggingface.co/new-space" target="_blank">huggingface.co/new-space</a>,登录后创建:</p>
|
||||
<ul class="hf-checklist">
|
||||
<li>Space name: 随便取(如 <code>my-embedding</code>)</li>
|
||||
<li>SDK: 选 <strong>Docker</strong></li>
|
||||
<li>Hardware: 选 <strong>CPU basic (Free)</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hf-section">
|
||||
<div class="hf-step-header"><span class="hf-step-num">2</span><span class="hf-step-title">上传 3 个文件</span></div>
|
||||
<div class="hf-step-content">
|
||||
<p>在 Space 的 Files 页面,依次创建以下文件:</p>
|
||||
<div class="hf-file">
|
||||
<div class="hf-file-header"><span class="hf-file-icon">📄</span><span class="hf-file-name">requirements.txt</span></div>
|
||||
<pre class="hf-code"><code>fastapi
|
||||
uvicorn
|
||||
sentence-transformers
|
||||
torch</code><button class="copy-btn">复制</button></pre>
|
||||
</div>
|
||||
<div class="hf-file">
|
||||
<div class="hf-file-header"><span class="hf-file-icon">🐍</span><span class="hf-file-name">app.py</span><span class="hf-file-note">主程序</span></div>
|
||||
<pre class="hf-code"><code>import os
|
||||
os.environ["OMP_NUM_THREADS"] = "1"
|
||||
os.environ["MKL_NUM_THREADS"] = "1"
|
||||
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
||||
|
||||
import torch
|
||||
torch.set_num_threads(1)
|
||||
|
||||
import threading
|
||||
from functools import lru_cache
|
||||
from typing import List, Optional
|
||||
from fastapi import FastAPI, HTTPException, Header
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
ACCESS_KEY = os.environ.get("ACCESS_KEY", "")
|
||||
MODEL_ID = "BAAI/bge-m3"
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_model():
|
||||
return SentenceTransformer(MODEL_ID, trust_remote_code=True)
|
||||
|
||||
class EmbedRequest(BaseModel):
|
||||
input: List[str]
|
||||
model: Optional[str] = "bge-m3"
|
||||
|
||||
@app.post("/v1/embeddings")
|
||||
async def embed(req: EmbedRequest, authorization: Optional[str] = Header(None)):
|
||||
if ACCESS_KEY and (authorization or "").replace("Bearer ", "").strip() != ACCESS_KEY:
|
||||
raise HTTPException(401, "Unauthorized")
|
||||
embeddings = get_model().encode(req.input, normalize_embeddings=True)
|
||||
return {"data": [{"embedding": e.tolist(), "index": i} for i, e in enumerate(embeddings)]}
|
||||
|
||||
@app.get("/v1/models")
|
||||
async def models():
|
||||
return {"data": [{"id": "bge-m3"}]}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
threading.Thread(target=get_model, daemon=True).start()</code><button class="copy-btn">复制</button></pre>
|
||||
</div>
|
||||
<div class="hf-file">
|
||||
<div class="hf-file-header"><span class="hf-file-icon">🐳</span><span class="hf-file-name">Dockerfile</span></div>
|
||||
<pre class="hf-code"><code>FROM python:3.10-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY app.py ./
|
||||
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('BAAI/bge-m3', trust_remote_code=True)"
|
||||
EXPOSE 7860
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "2"]</code><button class="copy-btn">复制</button></pre>
|
||||
setHtml(es, `
|
||||
<div id="arc-list">
|
||||
${list.map((a, i) => `
|
||||
<div class="struct-item arc-item" data-index="${i}">
|
||||
<div class="struct-row"><input type="text" class="arc-name" placeholder="角色名" value="${h(a.name || '')}"></div>
|
||||
<div class="struct-row"><textarea class="arc-trajectory" rows="2" placeholder="当前状态描述">${h(a.trajectory || '')}</textarea></div>
|
||||
<div class="struct-row">
|
||||
<label style="font-size:.75rem;color:var(--txt3)">进度:<input type="number" class="arc-progress" min="0" max="100" value="${Math.round((a.progress || 0) * 100)}" style="width:64px;display:inline-block"> %</label>
|
||||
</div>
|
||||
<div class="struct-row"><textarea class="arc-moments" rows="3" placeholder="关键时刻,一行一个">${h((a.moments || []).map(m => typeof m === 'string' ? m : m.text).join('\n'))}</textarea></div>
|
||||
<div class="struct-actions"><span>角色弧光 ${i + 1}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hf-section">
|
||||
<div class="hf-step-header"><span class="hf-step-num">3</span><span class="hf-step-title">等待构建</span></div>
|
||||
<div class="hf-step-content">
|
||||
<p>上传完成后自动开始构建,约需 <strong>10 分钟</strong>(下载模型)。</p>
|
||||
<p>成功后状态变为 <span class="hf-status-badge">Running</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hf-section">
|
||||
<div class="hf-step-header"><span class="hf-step-num">4</span><span class="hf-step-title">在插件中配置</span></div>
|
||||
<div class="hf-step-content">
|
||||
<div class="hf-config-table">
|
||||
<div class="hf-config-row"><span class="hf-config-label">服务渠道</span><span class="hf-config-value">OpenAI 兼容</span></div>
|
||||
<div class="hf-config-row"><span class="hf-config-label">API URL</span><span class="hf-config-value"><code>https://用户名-空间名.hf.space</code></span></div>
|
||||
<div class="hf-config-row"><span class="hf-config-label">API Key</span><span class="hf-config-value">随便填</span></div>
|
||||
<div class="hf-config-row"><span class="hf-config-label">模型</span><span class="hf-config-value">点"拉取" → 选 <code>bge-m3</code></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hf-section hf-faq">
|
||||
<div class="hf-faq-title">💡 小提示</div>
|
||||
<ul>
|
||||
<li>URL 格式:<code>https://用户名-空间名.hf.space</code>(减号连接,非斜杠)</li>
|
||||
<li>免费 Space 一段时间无请求会休眠,首次唤醒需等 20-30 秒</li>
|
||||
<li>如需保持常驻,可用 <a href="https://cron-job.org" target="_blank">cron-job.org</a> 每 5 分钟 ping <code>/health</code></li>
|
||||
<li>如需密码,在 Space Settings 设置 <code>ACCESS_KEY</code> 环境变量</li>
|
||||
</ul>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div style="margin-top:8px"><button type="button" class="btn btn-sm" id="arc-add">+ 新增角色弧光</button></div>
|
||||
`);
|
||||
|
||||
// Add copy button handlers
|
||||
body.querySelectorAll('.copy-btn').forEach(btn => {
|
||||
btn.onclick = async () => {
|
||||
const code = btn.previousElementSibling?.textContent || '';
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
btn.textContent = '已复制';
|
||||
setTimeout(() => btn.textContent = '复制', 1200);
|
||||
} catch {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = code;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
ta.remove();
|
||||
btn.textContent = '已复制';
|
||||
setTimeout(() => btn.textContent = '复制', 1200);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
es.querySelectorAll('.arc-item').forEach(addDeleteHandler);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Recall Log
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
$('arc-add').onclick = () => {
|
||||
const listEl = $('arc-list');
|
||||
const idx = listEl.querySelectorAll('.arc-item').length;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'struct-item arc-item';
|
||||
div.dataset.index = idx;
|
||||
setHtml(div, `
|
||||
<div class="struct-row"><input type="text" class="arc-name" placeholder="角色名"></div>
|
||||
<div class="struct-row"><textarea class="arc-trajectory" rows="2" placeholder="当前状态描述"></textarea></div>
|
||||
<div class="struct-row">
|
||||
<label style="font-size:.75rem;color:var(--txt3)">进度:<input type="number" class="arc-progress" min="0" max="100" value="0" style="width:64px;display:inline-block"> %</label>
|
||||
</div>
|
||||
<div class="struct-row"><textarea class="arc-moments" rows="3" placeholder="关键时刻,一行一个"></textarea></div>
|
||||
<div class="struct-actions"><span>角色弧光 ${idx + 1}</span></div>
|
||||
`);
|
||||
addDeleteHandler(div);
|
||||
listEl.appendChild(div);
|
||||
};
|
||||
}
|
||||
|
||||
function setRecallLog(text) {
|
||||
lastRecallLogText = text || '';
|
||||
@@ -1357,50 +1117,7 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
};
|
||||
}
|
||||
|
||||
function renderArcsEditor(arcs) {
|
||||
const list = arcs?.length ? arcs : [{ name: '', trajectory: '', progress: 0, moments: [] }];
|
||||
const es = $('editor-struct');
|
||||
|
||||
setHtml(es, `
|
||||
<div id="arc-list">
|
||||
${list.map((a, i) => `
|
||||
<div class="struct-item arc-item" data-index="${i}">
|
||||
<div class="struct-row"><input type="text" class="arc-name" placeholder="角色名" value="${h(a.name || '')}"></div>
|
||||
<div class="struct-row"><textarea class="arc-trajectory" rows="2" placeholder="当前状态描述">${h(a.trajectory || '')}</textarea></div>
|
||||
<div class="struct-row">
|
||||
<label style="font-size:.75rem;color:var(--txt3)">进度:<input type="number" class="arc-progress" min="0" max="100" value="${Math.round((a.progress || 0) * 100)}" style="width:64px;display:inline-block"> %</label>
|
||||
</div>
|
||||
<div class="struct-row"><textarea class="arc-moments" rows="3" placeholder="关键时刻,一行一个">${h((a.moments || []).map(m => typeof m === 'string' ? m : m.text).join('\n'))}</textarea></div>
|
||||
<div class="struct-actions"><span>角色弧光 ${i + 1}</span></div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div style="margin-top:8px"><button type="button" class="btn btn-sm" id="arc-add">+ 新增角色弧光</button></div>
|
||||
`);
|
||||
|
||||
es.querySelectorAll('.arc-item').forEach(addDeleteHandler);
|
||||
|
||||
$('arc-add').onclick = () => {
|
||||
const listEl = $('arc-list');
|
||||
const idx = listEl.querySelectorAll('.arc-item').length;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'struct-item arc-item';
|
||||
div.dataset.index = idx;
|
||||
setHtml(div, `
|
||||
<div class="struct-row"><input type="text" class="arc-name" placeholder="角色名"></div>
|
||||
<div class="struct-row"><textarea class="arc-trajectory" rows="2" placeholder="当前状态描述"></textarea></div>
|
||||
<div class="struct-row">
|
||||
<label style="font-size:.75rem;color:var(--txt3)">进度:<input type="number" class="arc-progress" min="0" max="100" value="0" style="width:64px;display:inline-block"> %</label>
|
||||
</div>
|
||||
<div class="struct-row"><textarea class="arc-moments" rows="3" placeholder="关键时刻,一行一个"></textarea></div>
|
||||
<div class="struct-actions"><span>角色弧光 ${idx + 1}</span></div>
|
||||
`);
|
||||
addDeleteHandler(div);
|
||||
listEl.appendChild(div);
|
||||
};
|
||||
}
|
||||
|
||||
function openEditor(section) {
|
||||
function openEditor(section) {
|
||||
currentEditSection = section;
|
||||
const meta = SECTION_META[section];
|
||||
const es = $('editor-struct');
|
||||
@@ -1615,31 +1332,50 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
if (d.config) loadVectorConfig(d.config);
|
||||
break;
|
||||
|
||||
case 'VECTOR_LOCAL_MODEL_STATUS':
|
||||
updateLocalModelStatus(d.status, d.message);
|
||||
break;
|
||||
|
||||
case 'VECTOR_LOCAL_MODEL_PROGRESS':
|
||||
updateLocalModelProgress(d.percent);
|
||||
break;
|
||||
|
||||
case 'VECTOR_ONLINE_STATUS':
|
||||
updateOnlineStatus(d.status, d.message);
|
||||
break;
|
||||
|
||||
case 'VECTOR_ONLINE_MODELS':
|
||||
updateOnlineModels(d.models || []);
|
||||
break;
|
||||
|
||||
case 'VECTOR_STATS':
|
||||
updateVectorStats(d.stats);
|
||||
if (d.mismatch !== undefined) showVectorMismatchWarning(d.mismatch);
|
||||
break;
|
||||
|
||||
case 'VECTOR_GEN_PROGRESS':
|
||||
updateVectorGenProgress(d.phase, d.current, d.total);
|
||||
case 'ANCHOR_STATS':
|
||||
updateAnchorStats(d.stats || {});
|
||||
break;
|
||||
|
||||
case 'ANCHOR_GEN_PROGRESS':
|
||||
updateAnchorProgress(d.current, d.total, d.message);
|
||||
break;
|
||||
|
||||
case 'VECTOR_GEN_PROGRESS': {
|
||||
const progress = $('vector-gen-progress');
|
||||
const btnGen = $('btn-gen-vectors');
|
||||
const btnCancel = $('btn-cancel-vectors');
|
||||
const btnClear = $('btn-clear-vectors');
|
||||
|
||||
if (d.current < 0) {
|
||||
progress.classList.add('hidden');
|
||||
btnGen.classList.remove('hidden');
|
||||
btnCancel.classList.add('hidden');
|
||||
btnClear.classList.remove('hidden');
|
||||
vectorGenerating = false;
|
||||
} else {
|
||||
vectorGenerating = true;
|
||||
progress.classList.remove('hidden');
|
||||
btnGen.classList.add('hidden');
|
||||
btnCancel.classList.remove('hidden');
|
||||
btnClear.classList.add('hidden');
|
||||
|
||||
const percent = d.total > 0 ? Math.round(d.current / d.total * 100) : 0;
|
||||
progress.querySelector('.progress-inner').style.width = percent + '%';
|
||||
const displayText = d.message || `${d.phase || ''}: ${d.current}/${d.total}`;
|
||||
progress.querySelector('.progress-text').textContent = displayText;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'VECTOR_EXPORT_RESULT':
|
||||
$('btn-export-vectors').disabled = false;
|
||||
if (d.success) {
|
||||
@@ -1772,8 +1508,6 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
$('rel-fs-close').onclick = closeRelationsFullscreen;
|
||||
|
||||
// HF guide
|
||||
$('hf-guide-backdrop').onclick = closeHfGuide;
|
||||
$('hf-guide-close').onclick = closeHfGuide;
|
||||
|
||||
// Character selector
|
||||
$('char-sel-trigger').onclick = e => {
|
||||
|
||||
Reference in New Issue
Block a user