feat(rag): 在线embedding/rerank(阿里llm表模型)+引擎配置界面,空=屏蔽;NER/图谱保持降级

This commit is contained in:
ymq 2026-09-01 12:32:02 +08:00
parent c7a82dc570
commit 5f3b456955
7 changed files with 286 additions and 198 deletions

View File

@ -141,18 +141,12 @@ async def search_handler(request, params_kw, *args, **kwargs):
seen.add(hid)
unique_hits.append(h)
# Rerank if query text provided
# Rerank if query text provided(在线 rerank未配置则保持召回分排序
if query and unique_hits:
try:
documents = [h.get("text", h.get("content", "")) for h in unique_hits[:recall_k]]
rerank_resp = await _call_uapi("rag-reranker", "rerank", {
"query": query,
"documents": documents
})
reranked = _apply_rerank(unique_hits, rerank_resp)
unique_hits = reranked
except Exception as e:
exception(f"rerank failed: {e}")
documents = [h.get("text", h.get("content", "")) for h in unique_hits[:recall_k]]
rerank_resp = await _online_rerank(env, query, documents)
if rerank_resp:
unique_hits = _apply_rerank(unique_hits[:recall_k], rerank_resp)
# Limit + enrich with DB metadata
final = unique_hits[:top_k]
@ -215,28 +209,8 @@ async def _build_search_vector(query, file_data, file_name, env=None, kb_id=''):
return None
combined = " ".join(texts)
try:
emb_engine = 'clip-vith14'
if env is not None and kb_id:
async with get_sor_context(env, 'rag') as sor:
krecs = await sor.sqlExe("SELECT embedding_engine FROM rag_knowledge_bases WHERE id=${kb_id}$", {"kb_id": kb_id})
if krecs:
emb_engine = (getattr(krecs[0], 'embedding_engine', '') or 'clip-vith14').strip()
if emb_engine == 'bge-m3':
emb_url = 'https://embedding.opencomputing.net:10443/txte/api/embed'
emb_model = 'bge-m3'
else:
emb_url = 'https://embedding.opencomputing.net:10443/mme/api/embed'
emb_model = 'CLIP-ViT-H-14'
import aiohttp
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=15)) as s:
async with s.post(emb_url, json={"texts": [combined], "model": emb_model}) as resp:
emb_resp = await resp.json()
vecs = emb_resp.get("text_embeddings", emb_resp.get("embeddings", [])) if isinstance(emb_resp, dict) else []
return vecs[0] if vecs else None
except Exception as e:
exception(f"query embedding failed: {e}")
return None
vecs = await _online_embed(env, [combined])
return vecs[0] if vecs else None
def _parse_vdb_hits(vdb_resp, kb_id):
@ -465,8 +439,88 @@ def _detect_file_type(name, mime):
return "other"
async def _get_engine_cfg(env, engine_type):
"""读 rag_engine_configs 的在线引擎配置embedding/rerank
返回 dict(model_id, api_base, api_key 明文) None未配置能力屏蔽"""
try:
from appPublic.rc4 import unpassword
async with get_sor_context(env, 'rag') as sor:
recs = await sor.sqlExe(
"SELECT model_id, api_base, api_key FROM rag_engine_configs "
"WHERE engine_type=${t}$ AND status='active' ORDER BY is_default DESC, priority DESC LIMIT 1",
{"t": engine_type})
if not recs:
return None
r = recs[0]
model_id = (getattr(r, "model_id", "") or "").strip()
api_base = (getattr(r, "api_base", "") or "").strip()
enc = (getattr(r, "api_key", "") or "").strip()
if not (model_id and api_base and enc):
return None
try:
from appPublic.jsonConfig import getConfig
key = getConfig().password_key or 'QRIVSRHrthhwyjy176556332'
api_key = unpassword(enc, key)
except Exception:
api_key = enc
return {"model_id": model_id, "api_base": api_base.rstrip("/"), "api_key": api_key}
except Exception as e:
exception(f"engine cfg read failed ({engine_type}): {e}")
return None
async def _online_embed(env, texts):
"""阿里在线 embeddingOpenAI 兼容 /embeddings。未配置或失败返回 []。"""
cfg = await _get_engine_cfg(env, "embedding")
if not cfg:
return []
import aiohttp
try:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20)) as s:
async with s.post(cfg["api_base"] + "/embeddings",
json={"model": cfg["model_id"], "input": texts},
headers={"Authorization": "Bearer " + cfg["api_key"],
"Content-Type": "application/json"}) as resp:
data = await resp.json()
items = data.get("data", []) if isinstance(data, dict) else []
return [it.get("embedding") for it in items if it.get("embedding")]
except Exception as e:
exception(f"online embed failed: {e}")
return []
async def _online_rerank(env, query, documents):
"""阿里在线 rerankdashscope compatible-api /reranks。未配置或失败返回 None。"""
cfg = await _get_engine_cfg(env, "rerank")
if not cfg:
return None
import aiohttp
base = cfg["api_base"]
if "compatible-mode" in base:
base = base.replace("compatible-mode", "compatible-api")
try:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=15)) as s:
async with s.post(base + "/reranks",
json={"model": cfg["model_id"], "query": query,
"documents": documents, "top_n": len(documents)},
headers={"Authorization": "Bearer " + cfg["api_key"],
"Content-Type": "application/json"}) as resp:
data = await resp.json()
results = data.get("results", []) if isinstance(data, dict) else []
# dashscope 返回 [{index, relevance_score}],转成与 hits 对齐的 scores 列表
scores = [0.0] * len(documents)
for it in results:
idx = it.get("index")
if isinstance(idx, int) and 0 <= idx < len(scores):
scores[idx] = it.get("relevance_score", 0)
return {"scores": scores}
except Exception as e:
exception(f"online rerank failed: {e}")
return None
async def _call_uapi(upappid, apiname, data, timeout=10):
"""Call GPU service via uapi config in rag database"""
"""Call service via uapi config (VDB 等)"""
import aiohttp
env = ServerEnv()
async with get_sor_context(env, 'rag') as sor:
@ -492,31 +546,14 @@ async def _rag_ingest_async(env, text, kb_id, doc_id):
return {"chunks": 0}
chunk_count = len(chunks)
# 1. Embedding按知识库向量引擎选文本/多模态端点)
try:
emb_engine = 'clip-vith14'
async with get_sor_context(env, 'rag') as sor:
krecs = await sor.sqlExe("SELECT embedding_engine FROM rag_knowledge_bases WHERE id=${kb_id}$", {"kb_id": kb_id})
if krecs:
emb_engine = (getattr(krecs[0], 'embedding_engine', '') or 'clip-vith14').strip()
if emb_engine == 'bge-m3':
emb_url = 'https://embedding.opencomputing.net:10443/txte/api/embed'
emb_model = 'bge-m3'
else:
emb_url = 'https://embedding.opencomputing.net:10443/mme/api/embed'
emb_model = 'CLIP-ViT-H-14'
import aiohttp
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=15)) as s:
async with s.post(emb_url, json={"texts": chunks, "model": emb_model}) as resp:
emb_resp = await resp.json()
embeddings = emb_resp.get("text_embeddings", emb_resp.get("embeddings", [])) if isinstance(emb_resp, dict) else []
except Exception as e:
exception(f"embedding failed: {e}")
embeddings = []
# 1. Embedding在线模型未配置则屏蔽 → 空向量,文档仍入库可浏览)
embeddings = await _online_embed(env, chunks)
# 2. VDB upsert
vector_ids = []
if embeddings:
if len(embeddings) != len(chunks):
embeddings = embeddings[:len(chunks)]
try:
vdb_data = {
"collection": kb_id,
@ -934,6 +971,131 @@ async def tag_sync_handler(request, params_kw, *args, **kwargs):
return json.dumps({"error": str(e)})
async def engine_options_handler(request, params_kw, *args, **kwargs):
"""产线平台 llm 表中可用的 embedding/rerank 模型选项(供配置界面下拉)"""
env = request._run_ns
try:
async with get_sor_context(env, 'rag') as sor:
recs = await sor.sqlExe(
"SELECT id, name, model_id, capabilities FROM llm WHERE status='active' ORDER BY name", {})
rows = [{"v": r.model_id, "t": f"{r.name}{r.model_id}"} for r in recs]
return json.dumps({"status": "SUCCEEDED", "rows": rows}, ensure_ascii=False)
except Exception as e:
exception(f"engine_options: {e}, {format_exc()}")
return json.dumps({"error": str(e)})
async def engine_cfg_get_handler(request, params_kw, *args, **kwargs):
"""读当前 embedding/rerank 引擎配置api_key 不回显明文)"""
env = request._run_ns
try:
async with get_sor_context(env, 'rag') as sor:
recs = await sor.sqlExe(
"SELECT engine_type, model_id, api_base, api_key, status FROM rag_engine_configs "
"WHERE engine_type IN ('embedding','rerank') ORDER BY engine_type", {})
rows = []
for r in recs:
enc = getattr(r, "api_key", "") or ""
rows.append({"engine_type": r.engine_type, "model_id": r.model_id or "",
"api_base": r.api_base or "", "status": r.status or "active",
"has_key": bool(enc)})
return json.dumps({"status": "SUCCEEDED", "rows": rows}, ensure_ascii=False)
except Exception as e:
exception(f"engine_cfg_get: {e}, {format_exc()}")
return json.dumps({"error": str(e)})
async def engine_cfg_save_handler(request, params_kw, *args, **kwargs):
"""保存引擎配置。api_key 留空=沿用旧 keymodel_id 清空=屏蔽该能力。"""
env = request._run_ns
try:
engine_type = (params_kw.get("engine_type") or "").strip()
if engine_type not in ("embedding", "rerank"):
return json.dumps({"error": "engine_type must be embedding/rerank"})
model_id = (params_kw.get("model_id") or "").strip()
api_base = (params_kw.get("api_base") or "").strip()
api_key_plain = (params_kw.get("api_key") or "").strip()
status = (params_kw.get("status") or "active").strip()
async with get_sor_context(env, 'rag') as sor:
recs = await sor.sqlExe(
"SELECT id, api_key FROM rag_engine_configs WHERE engine_type=${t}$ LIMIT 1",
{"t": engine_type})
if recs:
rid = recs[0].id
if api_key_plain:
from appPublic.rc4 import password
from appPublic.jsonConfig import getConfig
key = getConfig().password_key or 'QRIVSRHrthhwyjy176556332'
enc = password(api_key_plain, key=key)
else:
enc = recs[0].api_key or ""
await sor.sqlExe(
"UPDATE rag_engine_configs SET model_id=${m}$, api_base=${b}$, api_key=${k}$, "
"status=${s}$, is_default=1, updated_at=NOW() WHERE id=${id}$",
{"m": model_id, "b": api_base, "k": enc, "s": status, "id": rid})
else:
from appPublic.rc4 import password
from appPublic.jsonConfig import getConfig
key = getConfig().password_key or 'QRIVSRHrthhwyjy176556332'
enc = password(api_key_plain, key=key) if api_key_plain else ""
rid = uuid.uuid4().hex
await sor.sqlExe(
"INSERT INTO rag_engine_configs (id, engine_type, engine_name, endpoint_url, api_key, "
"model_name, is_default, priority, status, created_at, updated_at) "
"VALUES (${id}$, ${t}$, ${n}$, ${b}$, ${k}$, ${m}$, 1, 10, ${s}$, NOW(), NOW())",
{"id": rid, "t": engine_type, "n": engine_type, "b": api_base,
"k": enc, "m": model_id, "s": status})
shielded = not model_id or status != "active"
return json.dumps({"status": "SUCCEEDED", "shielded": shielded}, ensure_ascii=False)
except Exception as e:
exception(f"engine_cfg_save: {e}, {format_exc()}")
return json.dumps({"error": str(e)})
async def engine_cfg_test_handler(request, params_kw, *args, **kwargs):
"""连通性测试:用提交的 model/base/key或已存配置发一次真实调用"""
env = request._run_ns
try:
engine_type = (params_kw.get("engine_type") or "").strip()
model_id = (params_kw.get("model_id") or "").strip()
api_base = (params_kw.get("api_base") or "").strip()
api_key_plain = (params_kw.get("api_key") or "").strip()
if not (model_id and api_base):
cfg = await _get_engine_cfg(env, engine_type)
if not cfg:
return json.dumps({"error": "未配置,无法测试"}, ensure_ascii=False)
model_id, api_base = cfg["model_id"], cfg["api_base"]
api_key_plain = api_key_plain or cfg["api_key"]
if not api_key_plain:
return json.dumps({"error": "缺少 api_key"}, ensure_ascii=False)
import aiohttp
headers = {"Authorization": "Bearer " + api_key_plain, "Content-Type": "application/json"}
base = api_base.rstrip("/")
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20)) as s:
if engine_type == "embedding":
async with s.post(base + "/embeddings", json={"model": model_id, "input": ["连通性测试"]},
headers=headers) as resp:
data = await resp.json()
dim = len(data.get("data", [{}])[0].get("embedding", [])) if data.get("data") else 0
if not dim:
return json.dumps({"error": str(data)[:200]}, ensure_ascii=False)
return json.dumps({"status": "SUCCEEDED", "message": f"embedding OK维度 {dim}"}, ensure_ascii=False)
else:
if "compatible-mode" in base:
base = base.replace("compatible-mode", "compatible-api")
async with s.post(base + "/reranks",
json={"model": model_id, "query": "测试",
"documents": ["", ""], "top_n": 2},
headers=headers) as resp:
data = await resp.json()
if not data.get("results"):
return json.dumps({"error": str(data)[:200]}, ensure_ascii=False)
return json.dumps({"status": "SUCCEEDED", "message": "rerank OK"}, ensure_ascii=False)
except Exception as e:
exception(f"engine_cfg_test: {e}, {format_exc()}")
return json.dumps({"error": str(e)[:200]})
def init_rag_module():
env = ServerEnv()
rf = RegisterFunction()
@ -954,3 +1116,7 @@ def init_rag_module():
rf.register("tag_media_tags", tag_media_tags_handler)
rf.register("tag_search", tag_search_handler)
rf.register("tag_sync", tag_sync_handler)
rf.register("engine_options", engine_options_handler)
rf.register("engine_cfg_get", engine_cfg_get_handler)
rf.register("engine_cfg_save", engine_cfg_save_handler)
rf.register("engine_cfg_test", engine_cfg_test_handler)

View File

@ -54,6 +54,10 @@ PATHS_LOGINED = [
# CRUD 管理页
f"/{MOD}/documents_list/index.ui",
f"/{MOD}/engine_configs_list/index.ui",
f"/{MOD}/engine_configs_list/engine_form.dspy",
f"/{MOD}/engine_configs_list/engine_options.dspy",
f"/{MOD}/engine_configs_list/engine_save.dspy",
f"/{MOD}/engine_configs_list/engine_test.dspy",
f"/{MOD}/subscriptions_list/index.ui",
]

View File

@ -0,0 +1,46 @@
import json
env = request._run_ns
async def get_cfg(t):
async with get_sor_context(env, 'rag') as sor:
recs = await sor.sqlExe(
"SELECT model_id, api_base, api_key, status FROM rag_engine_configs WHERE engine_type=${t}$ LIMIT 1",
{"t": t})
if not recs:
return {"model_id": "", "api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1", "has_key": False, "status": ""}
r = recs[0]
return {"model_id": r.model_id or "", "api_base": r.api_base or "https://dashscope.aliyuncs.com/compatible-mode/v1",
"has_key": bool(r.api_key), "status": r.status or "active"}
emb = await get_cfg("embedding")
rr = await get_cfg("rerank")
def card(title, desc, prefix, cfg):
shielded = (not cfg["model_id"]) or cfg["status"] != "active"
state = "⛔ 已屏蔽" if shielded else "✅ 启用中"
key_hint = "已配置(留空沿用)" if cfg["has_key"] else "未配置"
return {
"widgettype": "VBox", "options": {"width": "100%", "spacing": "12px", "bgcolor": "#f8f9fa", "padding": "16px", "css": "card"},
"subwidgets": [
{"widgettype": "HBox", "options": {"spacing": "12px", "alignItems": "center"}, "subwidgets": [
{"widgettype": "Text", "options": {"text": title, "cfontsize": 16, "fontWeight": "bold"}},
{"widgettype": "Text", "id": prefix + "_state", "options": {"text": state, "cfontsize": 13, "color": "#f56c6c" if shielded else "#50b86c"}}
]},
{"widgettype": "Text", "options": {"text": desc, "cfontsize": 12, "color": "#888"}},
{"widgettype": "HBox", "options": {"spacing": "12px", "wrap": True}, "subwidgets": [
{"widgettype": "UiCode", "id": prefix + "_model", "options": {"name": prefix + "_model", "dataurl": "{{entire_url('./engine_options.dspy')}}", "text": "未配置(屏蔽)", "cwidth": 24, "allowempty": True, "value": cfg["model_id"]}},
{"widgettype": "Input", "id": prefix + "_base", "options": {"label": "API Base", "name": prefix + "_base", "value": cfg["api_base"], "cwidth": 30}},
{"widgettype": "Input", "id": prefix + "_key", "options": {"label": "API Key" + key_hint + "", "name": prefix + "_key", "type": "password", "cwidth": 24}}
]},
{"widgettype": "HBox", "options": {"spacing": "12px"}, "subwidgets": [
{"widgettype": "Button", "options": {"label": "测试连通", "css": "outline", "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": "var p=this.parent.parent;var m=bricks.getWidgetById('" + prefix + "_model',p).resultValue()||'';var b=bricks.getWidgetById('" + prefix + "_base',p).resultValue()||'';var k=bricks.getWidgetById('" + prefix + "_key',p).resultValue()||'';fetch('./engine_test.dspy?_webbricks_=1&engine_type=" + prefix + "&model_id='+encodeURIComponent(m)+'&api_base='+encodeURIComponent(b)+'&api_key='+encodeURIComponent(k)).then(function(r){return r.json()}).then(function(d){bricks.show_message({title:'" + title + " 测试',message:d.message||d.error||JSON.stringify(d)})})"}]},
{"widgettype": "Button", "options": {"label": "保存", "bgcolor": "#10b981", "color": "#fff", "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": "var p=this.parent.parent;var m=bricks.getWidgetById('" + prefix + "_model',p).resultValue()||'';var b=bricks.getWidgetById('" + prefix + "_base',p).resultValue()||'';var k=bricks.getWidgetById('" + prefix + "_key',p).resultValue()||'';fetch('./engine_save.dspy?_webbricks_=1&engine_type=" + prefix + "&model_id='+encodeURIComponent(m)+'&api_base='+encodeURIComponent(b)+'&api_key='+encodeURIComponent(k)).then(function(r){return r.json()}).then(function(d){bricks.show_message({title:'保存',message:(d.error||('已保存:'+(d.shielded?'能力屏蔽':'已启用')))+'(刷新页面查看状态)'})})"}]}
]}
]
}
result = {"widgettype": "VBox", "options": {"width": "100%", "spacing": "16px"}, "subwidgets": [
card("🔤 文本向量化Embedding", "文本知识库必需。模型留空或停用 = 屏蔽(文档仍入库可浏览,检索不可用)。", "embedding", emb),
card("🎯 重排序Rerank", "检索结果精排,可选。未配置时按召回分排序。", "rerank", rr)
]}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,7 @@
import json
env = request._run_ns
async with get_sor_context(env, 'rag') as sor:
recs = await sor.sqlExe(
"SELECT name, model_id FROM llm WHERE status='active' ORDER BY name", {})
rows = [{"value": r.model_id, "text": f"{r.name}{r.model_id}"} for r in recs]
return json.dumps(rows, ensure_ascii=False)

View File

@ -0,0 +1,2 @@
from rag.init import engine_cfg_save_handler
return await engine_cfg_save_handler(request, params_kw)

View File

@ -0,0 +1,2 @@
from rag.init import engine_cfg_test_handler
return await engine_cfg_test_handler(request, params_kw)

View File

@ -1,148 +1,9 @@
{
"widgettype": "VBox",
"options": {
"cheight": 40,
"width": "100%",
"padding": "16px",
"spacing": "16px"
},
"options": {"width": "100%", "padding": "16px", "spacing": "16px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "引擎配置",
"cfontsize": 22,
"fontWeight": "bold",
"color": "#333"
}
},
{
"widgettype": "Text",
"options": {
"text": "管理多媒体RAG的各层引擎支持租户级自定义覆盖",
"color": "#888",
"cfontsize": 14
}
},
{
"widgettype": "HBox",
"options": {
"width": "100%",
"spacing": "12px",
"wrap": true
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"cwidth": 20,
"cheight": 10,
"bgcolor": "#f8f9fa",
"padding": "12px",
"css": "card"
},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "🔤 向量化引擎", "cfontsize": 16, "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": "CLIP ViT-H-14", "cfontsize": 14, "color": "#4a90d9"}},
{"widgettype": "Text", "options": {"text": "1024维 · 图文跨模态", "cfontsize": 12, "color": "#888"}},
{"widgettype": "Text", "options": {"text": "状态: ✅ 运行中", "cfontsize": 12, "color": "#50b86c"}},
{"widgettype": "Text", "options": {"text": "端口: 9086", "cfontsize": 12, "color": "#aaa"}}
]
},
{
"widgettype": "VBox",
"options": {
"cwidth": 20,
"cheight": 10,
"bgcolor": "#f8f9fa",
"padding": "12px",
"css": "card"
},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "🗄️ 向量数据库", "cfontsize": 16, "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": "Milvus Lite", "cfontsize": 14, "color": "#4a90d9"}},
{"widgettype": "Text", "options": {"text": "COSINE度量 · HNSW索引", "cfontsize": 12, "color": "#888"}},
{"widgettype": "Text", "options": {"text": "状态: ✅ 运行中", "cfontsize": 12, "color": "#50b86c"}},
{"widgettype": "Text", "options": {"text": "端口: 8886", "cfontsize": 12, "color": "#aaa"}}
]
},
{
"widgettype": "VBox",
"options": {
"cwidth": 20,
"cheight": 10,
"bgcolor": "#f8f9fa",
"padding": "12px",
"css": "card"
},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "📊 重排引擎", "cfontsize": 16, "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": "BGE Reranker v2-m3", "cfontsize": 14, "color": "#4a90d9"}},
{"widgettype": "Text", "options": {"text": "Cross-encoder精排", "cfontsize": 12, "color": "#888"}},
{"widgettype": "Text", "options": {"text": "状态: ✅ 运行中", "cfontsize": 12, "color": "#50b86c"}},
{"widgettype": "Text", "options": {"text": "端口: 9090", "cfontsize": 12, "color": "#aaa"}}
]
},
{
"widgettype": "VBox",
"options": {
"cwidth": 20,
"cheight": 10,
"bgcolor": "#f8f9fa",
"padding": "12px",
"css": "card"
},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "🕸️ 知识图谱", "cfontsize": 16, "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": "NetworkX", "cfontsize": 14, "color": "#4a90d9"}},
{"widgettype": "Text", "options": {"text": "实体关系 · 邻居查询", "cfontsize": 12, "color": "#888"}},
{"widgettype": "Text", "options": {"text": "状态: ✅ 运行中", "cfontsize": 12, "color": "#50b86c"}},
{"widgettype": "Text", "options": {"text": "端口: 9092", "cfontsize": 12, "color": "#aaa"}}
]
},
{
"widgettype": "VBox",
"options": {
"cwidth": 20,
"cheight": 10,
"bgcolor": "#f8f9fa",
"padding": "12px",
"css": "card"
},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "🏷️ 实体识别", "cfontsize": 16, "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": "GLiNER Multitask", "cfontsize": 14, "color": "#4a90d9"}},
{"widgettype": "Text", "options": {"text": "零样本NER · 中文支持", "cfontsize": 12, "color": "#888"}},
{"widgettype": "Text", "options": {"text": "状态: ✅ 运行中", "cfontsize": 12, "color": "#50b86c"}},
{"widgettype": "Text", "options": {"text": "端口: 9093", "cfontsize": 12, "color": "#aaa"}}
]
},
{
"widgettype": "VBox",
"options": {
"cwidth": 20,
"cheight": 10,
"bgcolor": "#f8f9fa",
"padding": "12px",
"css": "card"
},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "👤 人脸识别", "cfontsize": 16, "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": "InsightFace buffalo_l", "cfontsize": 14, "color": "#4a90d9"}},
{"widgettype": "Text", "options": {"text": "检测+识别 · 512维", "cfontsize": 12, "color": "#888"}},
{"widgettype": "Text", "options": {"text": "状态: ✅ 运行中", "cfontsize": 12, "color": "#50b86c"}},
{"widgettype": "Text", "options": {"text": "端口: 9091", "cfontsize": 12, "color": "#aaa"}}
]
}
]
},
{
"widgettype": "Text",
"options": {
"text": "💡 引擎默认全局配置,租户可在订阅设置中覆盖为私有实例",
"color": "#aaa",
"cfontsize": 12
}
}
{"widgettype": "Text", "options": {"text": "引擎配置", "cfontsize": 22, "fontWeight": "bold", "color": "#333"}},
{"widgettype": "Text", "options": {"text": "文本知识库使用产线平台 llm 表中的在线模型(阿里 dashscope。模型留空或停用 = 屏蔽该能力。", "color": "#888", "cfontsize": 13}},
{"widgettype": "urlwidget", "options": {"url": "{{entire_url('./engine_form.dspy')}}", "css": "filler"}}
]
}