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

File diff suppressed because it is too large Load Diff

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")

View File

@@ -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);