fix: add newdata_params for kb_id in Tree CRUD

This commit is contained in:
yumoqing 2026-07-23 11:27:01 +08:00
parent f935553f55
commit 94361d7da4
6 changed files with 293 additions and 3 deletions

View File

@ -55,7 +55,14 @@
{"leading": "/api/doc/delete", "registerfunction": "doc_delete"},
{"leading": "/api/dir/create", "registerfunction": "dir_create"},
{"leading": "/api/dir/delete", "registerfunction": "dir_delete"},
{"leading": "/api/dir/list", "registerfunction": "dir_list"}
{"leading": "/api/dir/list", "registerfunction": "dir_list"},
{"leading": "/api/tag/create", "registerfunction": "tag_create"},
{"leading": "/api/tag/list", "registerfunction": "tag_list"},
{"leading": "/api/tag/delete", "registerfunction": "tag_delete"},
{"leading": "/api/tag/assign", "registerfunction": "tag_assign"},
{"leading": "/api/tag/unassign", "registerfunction": "tag_unassign"},
{"leading": "/api/tag/media_tags", "registerfunction": "tag_media_tags"},
{"leading": "/api/tag/search", "registerfunction": "tag_search"}
]
},
"hot_reload": true

210
init.py
View File

@ -14,7 +14,11 @@ async def status_handler(request, params_kw, *args, **kwargs):
return json.dumps({
"service": "ragserver", "version": "0.1.0",
"endpoints": ["/api/status", "/api/kb/list", "/api/doc/upload",
"/api/doc/delete", "/api/search", "/api/engines"]
"/api/doc/delete", "/api/search", "/api/engines",
"/api/dir/create", "/api/dir/delete", "/api/dir/list",
"/api/tag/create", "/api/tag/list", "/api/tag/delete",
"/api/tag/assign", "/api/tag/unassign", "/api/tag/media_tags",
"/api/tag/search"]
}, indent=2, ensure_ascii=False)
@ -329,6 +333,203 @@ async def dir_list_handler(request, params_kw, *args, **kwargs):
return json.dumps({"error": str(e)})
async def tag_create_handler(request, params_kw, *args, **kwargs):
"""创建标签"""
env = request._run_ns
try:
kb_id = params_kw.get("kb_id", "")
name = params_kw.get("name", "").strip()
color = params_kw.get("color", "#3b82f6")
if not kb_id or not name:
return json.dumps({"error": "kb_id and name required"})
userorgid = await env.get_userorgid()
async with get_sor_context(env, 'rag') as sor:
tag_id = uuid.uuid4().hex[:16]
await sor.sqlExe(
"INSERT INTO tags (id, kb_id, name, color, org_id, created_at) "
"VALUES (${id}$, ${kb_id}$, ${name}$, ${color}$, ${org_id}$, NOW())",
{"id": tag_id, "kb_id": kb_id, "name": name, "color": color, "org_id": userorgid})
return json.dumps({"status": "SUCCEEDED", "tag_id": tag_id, "name": name, "color": color})
except Exception as e:
exception(f"tag_create: {e}, {format_exc()}")
return json.dumps({"error": str(e)})
async def tag_list_handler(request, params_kw, *args, **kwargs):
"""列出知识库的所有标签"""
env = request._run_ns
try:
kb_id = params_kw.get("kb_id", "")
if not kb_id:
return json.dumps({"error": "kb_id required"})
userorgid = await env.get_userorgid()
async with get_sor_context(env, 'rag') as sor:
recs = await sor.R("tags", {"kb_id": kb_id, "org_id": userorgid})
tags = [{"id": r.id, "name": r.name, "color": r.color, "created_at": str(r.created_at)} for r in recs]
return json.dumps({"status": "SUCCEEDED", "tags": tags}, ensure_ascii=False, default=str)
except Exception as e:
exception(f"tag_list: {e}, {format_exc()}")
return json.dumps({"error": str(e)})
async def tag_delete_handler(request, params_kw, *args, **kwargs):
"""删除标签(级联删除关联)"""
env = request._run_ns
try:
tag_id = params_kw.get("tag_id", "")
if not tag_id:
return json.dumps({"error": "tag_id required"})
async with get_sor_context(env, 'rag') as sor:
await sor.sqlExe("DELETE FROM media_tags WHERE tag_id=${id}$", {"id": tag_id})
await sor.sqlExe("DELETE FROM tags WHERE id=${id}$", {"id": tag_id})
return json.dumps({"status": "SUCCEEDED", "tag_id": tag_id})
except Exception as e:
exception(f"tag_delete: {e}, {format_exc()}")
return json.dumps({"error": str(e)})
async def tag_assign_handler(request, params_kw, *args, **kwargs):
"""给媒体/人脸/声纹打标签"""
env = request._run_ns
try:
kb_id = params_kw.get("kb_id", "")
media_type = params_kw.get("media_type", "") # document / face / voice
media_id = params_kw.get("media_id", "")
tag_id = params_kw.get("tag_id", "")
if not all([kb_id, media_type, media_id, tag_id]):
return json.dumps({"error": "kb_id, media_type, media_id, tag_id required"})
if media_type not in ("document", "face", "voice"):
return json.dumps({"error": "media_type must be document/face/voice"})
async with get_sor_context(env, 'rag') as sor:
mt_id = uuid.uuid4().hex[:16]
await sor.sqlExe(
"INSERT INTO media_tags (id, kb_id, media_type, media_id, tag_id, created_at) "
"VALUES (${id}$, ${kb_id}$, ${type}$, ${mid}$, ${tid}$, NOW())",
{"id": mt_id, "kb_id": kb_id, "type": media_type, "mid": media_id, "tid": tag_id})
return json.dumps({"status": "SUCCEEDED", "media_tag_id": mt_id})
except Exception as e:
exception(f"tag_assign: {e}, {format_exc()}")
return json.dumps({"error": str(e)})
async def tag_unassign_handler(request, params_kw, *args, **kwargs):
"""取消标签关联"""
env = request._run_ns
try:
media_type = params_kw.get("media_type", "")
media_id = params_kw.get("media_id", "")
tag_id = params_kw.get("tag_id", "")
if not all([media_type, media_id, tag_id]):
return json.dumps({"error": "media_type, media_id, tag_id required"})
async with get_sor_context(env, 'rag') as sor:
await sor.sqlExe(
"DELETE FROM media_tags WHERE media_type=${type}$ AND media_id=${mid}$ AND tag_id=${tid}$",
{"type": media_type, "mid": media_id, "tid": tag_id})
return json.dumps({"status": "SUCCEEDED"})
except Exception as e:
exception(f"tag_unassign: {e}, {format_exc()}")
return json.dumps({"error": str(e)})
async def tag_media_tags_handler(request, params_kw, *args, **kwargs):
"""查询某媒体的所有标签"""
env = request._run_ns
try:
media_type = params_kw.get("media_type", "")
media_id = params_kw.get("media_id", "")
if not all([media_type, media_id]):
return json.dumps({"error": "media_type and media_id required"})
async with get_sor_context(env, 'rag') as sor:
recs = await sor.sqlExe(
"SELECT t.id, t.name, t.color FROM media_tags mt "
"JOIN tags t ON mt.tag_id=t.id "
"WHERE mt.media_type=${type}$ AND mt.media_id=${mid}$",
{"type": media_type, "mid": media_id})
tags = [{"id": r.id, "name": r.name, "color": r.color} for r in recs]
return json.dumps({"status": "SUCCEEDED", "tags": tags})
except Exception as e:
exception(f"tag_media_tags: {e}, {format_exc()}")
return json.dumps({"error": str(e)})
async def tag_search_handler(request, params_kw, *args, **kwargs):
"""组合标签检索:按 tag_ids 过滤,再语义检索"""
env = request._run_ns
try:
query = params_kw.get("query", "")
kb_id = params_kw.get("kb_id", "")
tag_ids_str = params_kw.get("tag_ids", "") # comma-separated tag IDs
top_k = int(params_kw.get("top_k", 5))
match_mode = params_kw.get("match_mode", "any") # any / all
if not kb_id:
return json.dumps({"error": "kb_id required"})
async with get_sor_context(env, 'rag') as sor:
if tag_ids_str:
tag_ids = [t.strip() for t in tag_ids_str.split(",") if t.strip()]
media_ids_by_tag = []
for tid in tag_ids:
recs = await sor.sqlExe(
"SELECT media_type, media_id FROM media_tags WHERE kb_id=${kb_id}$ AND tag_id=${tid}$",
{"kb_id": kb_id, "tid": tid})
mids = {(r.media_type, r.media_id) for r in recs}
media_ids_by_tag.append(mids)
if match_mode == "all":
matched = media_ids_by_tag[0]
for s in media_ids_by_tag[1:]:
matched = matched & s
else:
matched = set()
for s in media_ids_by_tag:
matched |= s
if not matched:
return json.dumps({"status": "SUCCEEDED", "results": [], "message": "no media match tags"})
doc_ids = [mid for mt, mid in matched if mt == "document"]
face_ids = [mid for mt, mid in matched if mt == "face"]
voice_ids = [mid for mt, mid in matched if mt == "voice"]
results = []
if doc_ids:
docs = await sor.sqlExe(
"SELECT id, file_name, file_type, file_size, status, created_at FROM documents WHERE id IN (${ids}$)",
{"ids": doc_ids})
for d in docs:
results.append({"type": "document", "id": d.id, "name": d.file_name, "file_type": d.file_type, "size": d.file_size, "status": d.status, "created_at": str(d.created_at)})
if face_ids:
faces = await sor.sqlExe(
"SELECT id, name, description, face_embedding_id, created_at FROM entities WHERE id IN (${ids}$) AND entity_type='person'",
{"ids": face_ids})
for f in faces:
results.append({"type": "face", "id": f.id, "name": f.name, "description": f.description, "created_at": str(f.created_at)})
if voice_ids:
voices = await sor.sqlExe(
"SELECT id, name, description, voice_embedding_id, created_at FROM entities WHERE id IN (${ids}$) AND entity_type='voice'",
{"ids": voice_ids})
for v in voices:
results.append({"type": "voice", "id": v.id, "name": v.name, "description": v.description, "created_at": str(v.created_at)})
if query:
import urllib.request
tag_filtered_docs = [r for r in results if r["type"] == "document"]
vdb_docs = []
for doc in tag_filtered_docs:
chunks = await sor.sqlExe(
"SELECT content FROM document_chunks WHERE doc_id=${id}$ LIMIT 3",
{"id": doc["id"]})
for c in chunks:
vdb_docs.append({"doc_id": doc["id"], "content": c.content})
results = {"documents": [{"id": r["id"], "name": r["name"], "file_type": r["file_type"]} for r in tag_filtered_docs],
"faces": [r for r in results if r["type"] == "face"],
"voices": [r for r in results if r["type"] == "voice"],
"chunks": vdb_docs,
"query": query,
"tag_ids": tag_ids,
"match_mode": match_mode}
return json.dumps({"status": "SUCCEEDED", "results": results, "tag_ids": tag_ids, "match_mode": match_mode}, ensure_ascii=False, default=str)
else:
return json.dumps({"status": "SUCCEEDED", "results": [], "message": "no tag_ids provided"})
except Exception as e:
exception(f"tag_search: {e}, {format_exc()}")
return json.dumps({"error": str(e)})
def init_ragserver():
env = ServerEnv()
rf = RegisterFunction()
@ -341,3 +542,10 @@ def init_ragserver():
rf.register("dir_create", dir_create_handler)
rf.register("dir_delete", dir_delete_handler)
rf.register("dir_list", dir_list_handler)
rf.register("tag_create", tag_create_handler)
rf.register("tag_list", tag_list_handler)
rf.register("tag_delete", tag_delete_handler)
rf.register("tag_assign", tag_assign_handler)
rf.register("tag_unassign", tag_unassign_handler)
rf.register("tag_media_tags", tag_media_tags_handler)
rf.register("tag_search", tag_search_handler)

22
json/tags.json Normal file
View File

@ -0,0 +1,22 @@
{
"tblname": "tags",
"params": {
"title": "标签管理",
"browserfields": [
{"field": "name", "title": "标签名", "width": "25%"},
{"field": "color", "title": "颜色", "width": "15%"},
{"field": "kb_id", "title": "知识库", "width": "30%"},
{"field": "created_at", "title": "创建时间", "width": "30%"}
],
"editfields": [
{"field": "name", "uitype": "Text", "required": true, "label": "标签名"},
{"field": "kb_id", "uitype": "Text", "required": true, "label": "知识库ID"},
{"field": "color", "uitype": "Text", "default": "#3b82f6", "label": "颜色(#hex)"}
],
"searchfields": ["name"],
"sort": "created_at desc",
"data_filter": {
"org_id": "{{userorgid}}"
}
}
}

28
models/media_tags.json Normal file
View File

@ -0,0 +1,28 @@
{
"summary": [
{
"name": "media_tags",
"title": "媒体标签关联",
"primary": ["id"]
}
],
"fields": [
{"name": "id", "title": "ID", "type": "str", "length": 32},
{"name": "kb_id", "title": "知识库ID", "type": "str", "length": 32},
{"name": "media_type", "title": "媒体类型", "type": "str", "length": 32},
{"name": "media_id", "title": "媒体ID", "type": "str", "length": 32},
{"name": "tag_id", "title": "标签ID", "type": "str", "length": 32},
{"name": "created_at", "title": "创建时间", "type": "datetime"}
],
"codes": [
{"field": "media_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='media_type'"},
{"field": "tag_id", "table": "tags", "valuefield": "id", "textfield": "name"},
{"field": "kb_id", "table": "knowledge_bases", "valuefield": "id", "textfield": "name"}
],
"indexes": [
{"name": "idx_mt_media", "idxtype": "index", "idxfields": ["media_type", "media_id"]},
{"name": "idx_mt_tag", "idxtype": "index", "idxfields": ["tag_id"]},
{"name": "idx_mt_kb", "idxtype": "index", "idxfields": ["kb_id"]},
{"name": "idx_mt_unique", "idxtype": "unique", "idxfields": ["media_type", "media_id", "tag_id"]}
]
}

24
models/tags.json Normal file
View File

@ -0,0 +1,24 @@
{
"summary": [
{
"name": "tags",
"title": "标签",
"primary": ["id"]
}
],
"fields": [
{"name": "id", "title": "ID", "type": "str", "length": 32},
{"name": "kb_id", "title": "知识库ID", "type": "str", "length": 32},
{"name": "name", "title": "标签名", "type": "str", "length": 64},
{"name": "color", "title": "颜色", "type": "str", "length": 16, "default": "#3b82f6"},
{"name": "org_id", "title": "所属机构", "type": "str", "length": 32},
{"name": "created_at", "title": "创建时间", "type": "datetime"}
],
"codes": [
{"field": "kb_id", "table": "knowledge_bases", "valuefield": "id", "textfield": "name"}
],
"indexes": [
{"name": "idx_tag_kb", "idxtype": "index", "idxfields": ["kb_id"]},
{"name": "idx_tag_name", "idxtype": "unique", "idxfields": ["kb_id", "name"]}
]
}

View File

@ -38,11 +38,12 @@
"idField": "id",
"textField": "label",
"dataurl": "{{entire_url('./get_tree_data.dspy')}}?kb_id=kb1",
"newdata_params": {"kb_id": "kb1"},
"editable": {
"fields": [
{"name": "name", "title": "名称", "type": "str", "length": 255, "uitype": "str", "label": "名称"}
],
"add_url": "{{entire_url('./new_tree_item.dspy')}}?kb_id=kb1",
"add_url": "{{entire_url('./new_tree_item.dspy')}}",
"update_url": "{{entire_url('./update_tree_item.dspy')}}",
"delete_url": "{{entire_url('./delete_tree_item.dspy')}}"
}