Phase 5: 实体属性扩展功能

- 数据库层:
  - 新增 entity_attributes 表存储自定义属性
  - 新增 attribute_templates 表管理属性模板
  - 新增 attribute_history 表记录属性变更历史

- 后端 API:
  - GET/POST /api/v1/projects/{id}/attribute-templates - 属性模板管理
  - GET/POST/PUT/DELETE /api/v1/entities/{id}/attributes - 实体属性 CRUD
  - GET /api/v1/entities/{id}/attributes/history - 属性变更历史
  - GET /api/v1/projects/{id}/entities/search-by-attributes - 属性筛选搜索

- 前端 UI:
  - 实体详情面板添加属性展示
  - 属性编辑表单(支持文本、数字、日期、单选、多选)
  - 属性模板管理界面
  - 属性变更历史查看
  - 知识库实体卡片显示属性预览
  - 属性筛选搜索栏
This commit is contained in:
OpenClaw Bot
2026-02-20 00:10:49 +08:00
parent 626fa7e1c0
commit 7b67f3756e
7 changed files with 2209 additions and 27 deletions

View File

@@ -15,7 +15,7 @@ from fastapi import FastAPI, File, UploadFile, HTTPException, Form
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from typing import List, Optional
from typing import List, Optional, Union
from datetime import datetime
# Add backend directory to path for imports
@@ -755,14 +755,18 @@ async def get_knowledge_base(project_id: str):
# 获取术语表
glossary = db.list_glossary(project_id)
# 构建实体统计
# 构建实体统计和属性
entity_stats = {}
entity_attributes = {}
for ent in entities:
mentions = db.get_entity_mentions(ent.id)
entity_stats[ent.id] = {
"mention_count": len(mentions),
"transcript_ids": list(set([m.transcript_id for m in mentions]))
}
# Phase 5: 获取实体属性
attrs = db.get_entity_attributes(ent.id)
entity_attributes[ent.id] = attrs
# 构建实体名称映射
entity_map = {e.id: e.name for e in entities}
@@ -787,7 +791,8 @@ async def get_knowledge_base(project_id: str):
"definition": e.definition,
"aliases": e.aliases,
"mention_count": entity_stats.get(e.id, {}).get("mention_count", 0),
"appears_in": entity_stats.get(e.id, {}).get("transcript_ids", [])
"appears_in": entity_stats.get(e.id, {}).get("transcript_ids", []),
"attributes": entity_attributes.get(e.id, []) # Phase 5: 包含属性
}
for e in entities
],
@@ -1498,9 +1503,361 @@ async def project_summary(project_id: str, req: SummaryRequest):
"project_id": project_id,
"summary_type": req.summary_type,
**summary
**summary
}
# ==================== Phase 5: 实体属性扩展 API ====================
class AttributeTemplateCreate(BaseModel):
name: str
type: str # text, number, date, select, multiselect, boolean
options: Optional[List[str]] = None
default_value: Optional[str] = ""
description: Optional[str] = ""
is_required: bool = False
display_order: int = 0
class AttributeTemplateUpdate(BaseModel):
name: Optional[str] = None
type: Optional[str] = None
options: Optional[List[str]] = None
default_value: Optional[str] = None
description: Optional[str] = None
is_required: Optional[bool] = None
display_order: Optional[int] = None
class EntityAttributeSet(BaseModel):
template_id: str
value: str
change_reason: Optional[str] = ""
class EntityAttributeBatchSet(BaseModel):
attributes: List[EntityAttributeSet]
change_reason: Optional[str] = ""
# 属性模板管理 API
@app.post("/api/v1/projects/{project_id}/attribute-templates")
async def create_attribute_template_endpoint(project_id: str, template: AttributeTemplateCreate):
"""创建属性模板"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
from db_manager import AttributeTemplate
db = get_db_manager()
project = db.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
new_template = AttributeTemplate(
id=str(uuid.uuid4())[:8],
project_id=project_id,
name=template.name,
type=template.type,
options=template.options or [],
default_value=template.default_value or "",
description=template.description or "",
is_required=template.is_required,
display_order=template.display_order
)
db.create_attribute_template(new_template)
return {
"id": new_template.id,
"name": new_template.name,
"type": new_template.type,
"success": True
}
@app.get("/api/v1/projects/{project_id}/attribute-templates")
async def list_attribute_templates_endpoint(project_id: str):
"""列出项目的所有属性模板"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
db = get_db_manager()
templates = db.list_attribute_templates(project_id)
return [
{
"id": t.id,
"name": t.name,
"type": t.type,
"options": t.options,
"default_value": t.default_value,
"description": t.description,
"is_required": t.is_required,
"display_order": t.display_order
}
for t in templates
]
@app.get("/api/v1/attribute-templates/{template_id}")
async def get_attribute_template_endpoint(template_id: str):
"""获取属性模板详情"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
db = get_db_manager()
template = db.get_attribute_template(template_id)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
return {
"id": template.id,
"name": template.name,
"type": template.type,
"options": template.options,
"default_value": template.default_value,
"description": template.description,
"is_required": template.is_required,
"display_order": template.display_order
}
@app.put("/api/v1/attribute-templates/{template_id}")
async def update_attribute_template_endpoint(template_id: str, update: AttributeTemplateUpdate):
"""更新属性模板"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
db = get_db_manager()
template = db.get_attribute_template(template_id)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
update_data = {k: v for k, v in update.dict().items() if v is not None}
updated = db.update_attribute_template(template_id, **update_data)
return {
"id": updated.id,
"name": updated.name,
"type": updated.type,
"success": True
}
@app.delete("/api/v1/attribute-templates/{template_id}")
async def delete_attribute_template_endpoint(template_id: str):
"""删除属性模板"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
db = get_db_manager()
db.delete_attribute_template(template_id)
return {"success": True, "message": f"Template {template_id} deleted"}
# 实体属性值管理 API
@app.post("/api/v1/entities/{entity_id}/attributes")
async def set_entity_attribute_endpoint(entity_id: str, attr: EntityAttributeSet):
"""设置实体属性值"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
from db_manager import EntityAttribute
db = get_db_manager()
entity = db.get_entity(entity_id)
if not entity:
raise HTTPException(status_code=404, detail="Entity not found")
# 验证模板存在
template = db.get_attribute_template(attr.template_id)
if not template:
raise HTTPException(status_code=404, detail="Attribute template not found")
new_attr = EntityAttribute(
id=str(uuid.uuid4())[:8],
entity_id=entity_id,
template_id=attr.template_id,
value=attr.value
)
db.set_entity_attribute(new_attr, changed_by="user", change_reason=attr.change_reason)
return {
"entity_id": entity_id,
"template_id": attr.template_id,
"template_name": template.name,
"value": attr.value,
"success": True
}
@app.post("/api/v1/entities/{entity_id}/attributes/batch")
async def batch_set_entity_attributes_endpoint(entity_id: str, batch: EntityAttributeBatchSet):
"""批量设置实体属性值"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
from db_manager import EntityAttribute
db = get_db_manager()
entity = db.get_entity(entity_id)
if not entity:
raise HTTPException(status_code=404, detail="Entity not found")
results = []
for attr_data in batch.attributes:
template = db.get_attribute_template(attr_data.template_id)
if template:
new_attr = EntityAttribute(
id=str(uuid.uuid4())[:8],
entity_id=entity_id,
template_id=attr_data.template_id,
value=attr_data.value
)
db.set_entity_attribute(new_attr, changed_by="user",
change_reason=batch.change_reason or "批量更新")
results.append({
"template_id": attr_data.template_id,
"template_name": template.name,
"value": attr_data.value
})
return {
"entity_id": entity_id,
"updated_count": len(results),
"attributes": results,
"success": True
}
@app.get("/api/v1/entities/{entity_id}/attributes")
async def get_entity_attributes_endpoint(entity_id: str):
"""获取实体的所有属性值"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
db = get_db_manager()
entity = db.get_entity(entity_id)
if not entity:
raise HTTPException(status_code=404, detail="Entity not found")
attrs = db.get_entity_attributes(entity_id)
return [
{
"id": a.id,
"template_id": a.template_id,
"template_name": a.template_name,
"template_type": a.template_type,
"value": a.value
}
for a in attrs
]
@app.delete("/api/v1/entities/{entity_id}/attributes/{template_id}")
async def delete_entity_attribute_endpoint(entity_id: str, template_id: str,
reason: Optional[str] = ""):
"""删除实体属性值"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
db = get_db_manager()
db.delete_entity_attribute(entity_id, template_id,
changed_by="user", change_reason=reason)
return {"success": True, "message": "Attribute deleted"}
# 属性历史 API
@app.get("/api/v1/entities/{entity_id}/attributes/history")
async def get_entity_attribute_history_endpoint(entity_id: str, limit: int = 50):
"""获取实体的属性变更历史"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
db = get_db_manager()
history = db.get_attribute_history(entity_id=entity_id, limit=limit)
return [
{
"id": h.id,
"template_id": h.template_id,
"template_name": h.template_name,
"old_value": h.old_value,
"new_value": h.new_value,
"changed_by": h.changed_by,
"changed_at": h.changed_at,
"change_reason": h.change_reason
}
for h in history
]
@app.get("/api/v1/attribute-templates/{template_id}/history")
async def get_template_history_endpoint(template_id: str, limit: int = 50):
"""获取属性模板的所有变更历史(跨实体)"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
db = get_db_manager()
history = db.get_attribute_history(template_id=template_id, limit=limit)
return [
{
"id": h.id,
"entity_id": h.entity_id,
"template_name": h.template_name,
"old_value": h.old_value,
"new_value": h.new_value,
"changed_by": h.changed_by,
"changed_at": h.changed_at,
"change_reason": h.change_reason
}
for h in history
]
# 属性筛选搜索 API
@app.get("/api/v1/projects/{project_id}/entities/search-by-attributes")
async def search_entities_by_attributes_endpoint(
project_id: str,
attribute_filter: Optional[str] = None # JSON 格式: {"职位": "经理", "部门": "技术部"}
):
"""根据属性筛选搜索实体"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
db = get_db_manager()
project = db.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
filters = {}
if attribute_filter:
try:
filters = json.loads(attribute_filter)
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="Invalid attribute_filter JSON")
entities = db.search_entities_by_attributes(project_id, filters)
return [
{
"id": e.id,
"name": e.name,
"type": e.type,
"definition": e.definition,
"attributes": e.attributes
}
for e in entities
]
# Serve frontend - MUST be last to not override API routes
app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")