From 94361d7da43eaa0bfc3f797a2e47a12f8e5f3eb1 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 23 Jul 2026 11:27:01 +0800 Subject: [PATCH] fix: add newdata_params for kb_id in Tree CRUD --- conf/config.json | 9 +- init.py | 210 ++++++++++++++++++++++++- json/tags.json | 22 +++ models/media_tags.json | 28 ++++ models/tags.json | 24 +++ wwwroot/knowledge_bases_list/detail.ui | 3 +- 6 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 json/tags.json create mode 100644 models/media_tags.json create mode 100644 models/tags.json diff --git a/conf/config.json b/conf/config.json index 54532a8..aa2e773 100644 --- a/conf/config.json +++ b/conf/config.json @@ -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 diff --git a/init.py b/init.py index ad4c055..33d8e57 100644 --- a/init.py +++ b/init.py @@ -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) diff --git a/json/tags.json b/json/tags.json new file mode 100644 index 0000000..acdf5dd --- /dev/null +++ b/json/tags.json @@ -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}}" + } + } +} diff --git a/models/media_tags.json b/models/media_tags.json new file mode 100644 index 0000000..1e34090 --- /dev/null +++ b/models/media_tags.json @@ -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"]} + ] +} diff --git a/models/tags.json b/models/tags.json new file mode 100644 index 0000000..3953f1b --- /dev/null +++ b/models/tags.json @@ -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"]} + ] +} diff --git a/wwwroot/knowledge_bases_list/detail.ui b/wwwroot/knowledge_bases_list/detail.ui index 63f07d5..b9501d8 100644 --- a/wwwroot/knowledge_bases_list/detail.ui +++ b/wwwroot/knowledge_bases_list/detail.ui @@ -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')}}" }