From 5f3b45695534c0fe59f1f6259f66b5a235e7b599 Mon Sep 17 00:00:00 2001 From: ymq Date: Tue, 1 Sep 2026 12:32:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(rag):=20=E5=9C=A8=E7=BA=BFembedding/rerank?= =?UTF-8?q?(=E9=98=BF=E9=87=8Cllm=E8=A1=A8=E6=A8=A1=E5=9E=8B)+=E5=BC=95?= =?UTF-8?q?=E6=93=8E=E9=85=8D=E7=BD=AE=E7=95=8C=E9=9D=A2,=E7=A9=BA=3D?= =?UTF-8?q?=E5=B1=8F=E8=94=BD;NER/=E5=9B=BE=E8=B0=B1=E4=BF=9D=E6=8C=81?= =?UTF-8?q?=E9=99=8D=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rag/init.py | 276 ++++++++++++++---- scripts/load_path.py | 4 + wwwroot/engine_configs_list/engine_form.dspy | 46 +++ .../engine_configs_list/engine_options.dspy | 7 + wwwroot/engine_configs_list/engine_save.dspy | 2 + wwwroot/engine_configs_list/engine_test.dspy | 2 + wwwroot/engine_configs_list/index.ui | 147 +--------- 7 files changed, 286 insertions(+), 198 deletions(-) create mode 100644 wwwroot/engine_configs_list/engine_form.dspy create mode 100644 wwwroot/engine_configs_list/engine_options.dspy create mode 100644 wwwroot/engine_configs_list/engine_save.dspy create mode 100644 wwwroot/engine_configs_list/engine_test.dspy diff --git a/rag/init.py b/rag/init.py index 3ea8aa7..f724112 100644 --- a/rag/init.py +++ b/rag/init.py @@ -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): + """阿里在线 embedding(OpenAI 兼容 /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): + """阿里在线 rerank(dashscope 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 留空=沿用旧 key;model_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) diff --git a/scripts/load_path.py b/scripts/load_path.py index b22f62a..66474e3 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -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", ] diff --git a/wwwroot/engine_configs_list/engine_form.dspy b/wwwroot/engine_configs_list/engine_form.dspy new file mode 100644 index 0000000..20cb33c --- /dev/null +++ b/wwwroot/engine_configs_list/engine_form.dspy @@ -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) diff --git a/wwwroot/engine_configs_list/engine_options.dspy b/wwwroot/engine_configs_list/engine_options.dspy new file mode 100644 index 0000000..d608d8d --- /dev/null +++ b/wwwroot/engine_configs_list/engine_options.dspy @@ -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) diff --git a/wwwroot/engine_configs_list/engine_save.dspy b/wwwroot/engine_configs_list/engine_save.dspy new file mode 100644 index 0000000..a3e7626 --- /dev/null +++ b/wwwroot/engine_configs_list/engine_save.dspy @@ -0,0 +1,2 @@ +from rag.init import engine_cfg_save_handler +return await engine_cfg_save_handler(request, params_kw) diff --git a/wwwroot/engine_configs_list/engine_test.dspy b/wwwroot/engine_configs_list/engine_test.dspy new file mode 100644 index 0000000..b96b25f --- /dev/null +++ b/wwwroot/engine_configs_list/engine_test.dspy @@ -0,0 +1,2 @@ +from rag.init import engine_cfg_test_handler +return await engine_cfg_test_handler(request, params_kw) diff --git a/wwwroot/engine_configs_list/index.ui b/wwwroot/engine_configs_list/index.ui index f976585..55dcf10 100644 --- a/wwwroot/engine_configs_list/index.ui +++ b/wwwroot/engine_configs_list/index.ui @@ -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"}} ] }