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:
Binary file not shown.
File diff suppressed because it is too large
Load Diff
363
backend/main.py
363
backend/main.py
@@ -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")
|
||||
|
||||
|
||||
@@ -75,6 +75,94 @@ CREATE TABLE IF NOT EXISTS glossary (
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id)
|
||||
);
|
||||
|
||||
-- Phase 5: 属性模板表
|
||||
CREATE TABLE IF NOT EXISTS attribute_templates (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL, -- text/number/date/select/multiselect
|
||||
description TEXT,
|
||||
options TEXT, -- JSON 数组,用于 select/multiselect 类型
|
||||
is_required INTEGER DEFAULT 0,
|
||||
default_value TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id)
|
||||
);
|
||||
|
||||
-- Phase 5: 实体属性值表
|
||||
CREATE TABLE IF NOT EXISTS entity_attributes (
|
||||
id TEXT PRIMARY KEY,
|
||||
entity_id TEXT NOT NULL,
|
||||
template_id TEXT,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL, -- text/number/date/select/multiselect
|
||||
value TEXT, -- 存储实际值
|
||||
options TEXT, -- JSON 数组,用于 select/multiselect
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (template_id) REFERENCES attribute_templates(id) ON DELETE SET NULL,
|
||||
UNIQUE(entity_id, name)
|
||||
);
|
||||
|
||||
-- Phase 5: 属性变更历史表
|
||||
CREATE TABLE IF NOT EXISTS attribute_history (
|
||||
id TEXT PRIMARY KEY,
|
||||
entity_id TEXT NOT NULL,
|
||||
attribute_name TEXT NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_by TEXT, -- 用户ID或系统
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
change_reason TEXT,
|
||||
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Phase 5: 属性模板表(项目级自定义属性定义)
|
||||
CREATE TABLE IF NOT EXISTS attribute_templates (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL, -- 属性名称,如"年龄"、"职位"
|
||||
type TEXT NOT NULL, -- 属性类型: text, number, date, select, multiselect, boolean
|
||||
options TEXT, -- JSON 数组,用于 select/multiselect 类型
|
||||
default_value TEXT, -- 默认值
|
||||
description TEXT, -- 属性描述
|
||||
is_required BOOLEAN DEFAULT 0, -- 是否必填
|
||||
display_order INTEGER DEFAULT 0, -- 显示顺序
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id)
|
||||
);
|
||||
|
||||
-- Phase 5: 实体属性值表
|
||||
CREATE TABLE IF NOT EXISTS entity_attributes (
|
||||
id TEXT PRIMARY KEY,
|
||||
entity_id TEXT NOT NULL,
|
||||
template_id TEXT NOT NULL,
|
||||
value TEXT, -- 属性值(以JSON或字符串形式存储)
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (template_id) REFERENCES attribute_templates(id) ON DELETE CASCADE,
|
||||
UNIQUE(entity_id, template_id) -- 每个实体每个属性只能有一个值
|
||||
);
|
||||
|
||||
-- Phase 5: 属性变更历史表
|
||||
CREATE TABLE IF NOT EXISTS attribute_history (
|
||||
id TEXT PRIMARY KEY,
|
||||
entity_id TEXT NOT NULL,
|
||||
template_id TEXT NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_by TEXT, -- 用户ID或"system"
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
change_reason TEXT, -- 变更原因
|
||||
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (template_id) REFERENCES attribute_templates(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建索引以提高查询性能
|
||||
CREATE INDEX IF NOT EXISTS idx_entities_project ON entities(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
||||
@@ -83,3 +171,15 @@ CREATE INDEX IF NOT EXISTS idx_mentions_entity ON entity_mentions(entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mentions_transcript ON entity_mentions(transcript_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_relations_project ON entity_relations(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_glossary_project ON glossary(project_id);
|
||||
|
||||
-- Phase 5: 属性相关索引
|
||||
CREATE INDEX IF NOT EXISTS idx_attr_templates_project ON attribute_templates(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_entity_attributes_entity ON entity_attributes(entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_entity_attributes_template ON entity_attributes(template_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_attr_history_entity ON attribute_history(entity_id);
|
||||
|
||||
-- Phase 5: 属性相关索引
|
||||
CREATE INDEX IF NOT EXISTS idx_attr_templates_project ON attribute_templates(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_entity_attributes_entity ON entity_attributes(entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_entity_attributes_template ON entity_attributes(template_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_attr_history_entity ON attribute_history(entity_id);
|
||||
|
||||
Reference in New Issue
Block a user