feat: 品牌更名、用户管理增强、画像版本管理、对话体验优化
- 品牌统一为"左右(BossAi)":网页标签、侧边栏、登录页、manifest、后端 - 用户管理:搜索、对话统计、重置密码、激活/停用账号 - 用户画像:确认事实步骤(带入内容+追问内容)、版本历史管理、PUT /profile/me - 对话历史:按场景分组、保密模式标题处理 - AI助手页面:话题区可收起、模式选择紧凑化、footer免责声明 - 设置页:toggle宽度修复、工单文案改为专家咨询 - 去掉工单功能(前后端) Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -76,7 +76,7 @@ class Metrics:
|
||||
}
|
||||
|
||||
|
||||
app = FastAPI(title="BossAi MVP", version="1.1.0")
|
||||
app = FastAPI(title="左右(BossAi)", version="1.1.0")
|
||||
sessions = SessionManager()
|
||||
knowledge = KnowledgeStore()
|
||||
settings_store = SettingsStore()
|
||||
@@ -264,7 +264,7 @@ class SummarySaveRequest(BaseModel):
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "service": "bossai-mvp", "version": "1.1.0", "db": "connected"}
|
||||
return {"status": "ok", "service": "左右(BossAi)", "version": "1.1.0", "db": "connected"}
|
||||
|
||||
|
||||
class SampleRequest(BaseModel):
|
||||
@@ -434,6 +434,7 @@ def ask(sid: str, req: AskRequest, authorization: str | None = Header(default=No
|
||||
risk_escalation=card.escalation,
|
||||
inference_location=inference_location, model_name=model_name,
|
||||
fallback_used=fallback_used,
|
||||
rag_context=rag_context,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"chat_store save failed: {e}")
|
||||
@@ -492,6 +493,27 @@ def ask_analyze(sid: str, req: AskRequest, authorization: str | None = Header(de
|
||||
req.question, req.scene, answers, kimi_client=kimi,
|
||||
)
|
||||
|
||||
# 收集用户画像中的已知事实(带入内容)
|
||||
profile_facts: list[dict] = []
|
||||
if req.phone:
|
||||
try:
|
||||
profile = profile_store.get(req.phone)
|
||||
if profile.industry:
|
||||
profile_facts.append({"dim_key": "industry", "dim_desc": "行业", "answer": profile.industry})
|
||||
if profile.company_size:
|
||||
profile_facts.append({"dim_key": "company_size", "dim_desc": "企业规模", "answer": profile.company_size})
|
||||
if profile.role:
|
||||
profile_facts.append({"dim_key": "role", "dim_desc": "角色", "answer": profile.role})
|
||||
if profile.risk_tolerance:
|
||||
profile_facts.append({"dim_key": "risk_tolerance", "dim_desc": "风险偏好", "answer": profile.risk_tolerance})
|
||||
if profile.decision_style:
|
||||
profile_facts.append({"dim_key": "decision_style", "dim_desc": "决策风格", "answer": profile.decision_style})
|
||||
if profile.key_facts:
|
||||
for k, v in profile.key_facts.items():
|
||||
profile_facts.append({"dim_key": f"fact_{k}", "dim_desc": k, "answer": str(v)})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if meta["need_ask"]:
|
||||
return {
|
||||
"need_ask": True,
|
||||
@@ -501,6 +523,7 @@ def ask_analyze(sid: str, req: AskRequest, authorization: str | None = Header(de
|
||||
"question_type": meta["analysis"]["question_type"],
|
||||
"risk_level": card.level,
|
||||
"source": meta.get("source", "rule"),
|
||||
"profile_facts": profile_facts,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
@@ -510,6 +533,7 @@ def ask_analyze(sid: str, req: AskRequest, authorization: str | None = Header(de
|
||||
"fact_summary": meta["fact_summary"],
|
||||
"question_type": meta["analysis"]["question_type"],
|
||||
"risk_level": card.level,
|
||||
"profile_facts": profile_facts,
|
||||
}
|
||||
|
||||
|
||||
@@ -786,6 +810,56 @@ def get_my_profile(authorization: str | None = Header(default=None)):
|
||||
}
|
||||
|
||||
|
||||
@app.put("/profile/me")
|
||||
def update_my_profile(body: dict, authorization: str | None = Header(default=None)):
|
||||
"""更新当前登录用户的画像(用户确认事实后调用)。
|
||||
|
||||
Body:
|
||||
{
|
||||
"industry": "...",
|
||||
"company_size": "...",
|
||||
"role": "...",
|
||||
"risk_tolerance": "...",
|
||||
"decision_style": "...",
|
||||
"key_facts": {"事实名": "事实值", ...}
|
||||
}
|
||||
"""
|
||||
phone = get_current_phone(authorization)
|
||||
if not phone:
|
||||
raise HTTPException(401, "未登录或登录已过期")
|
||||
# 构造 signals,只更新非空字段
|
||||
signals: dict = {}
|
||||
for key in ("industry", "company_size", "role", "risk_tolerance",
|
||||
"communication_style", "decision_style"):
|
||||
val = body.get(key)
|
||||
if val:
|
||||
signals[key] = val
|
||||
if body.get("key_facts") and isinstance(body["key_facts"], dict):
|
||||
signals["key_facts"] = body["key_facts"]
|
||||
if not signals:
|
||||
return {"updated": False}
|
||||
signals["_source"] = "manual" # 标记为用户手动确认
|
||||
profile = profile_store.update(phone, signals)
|
||||
return {
|
||||
"updated": True,
|
||||
"industry": profile.industry,
|
||||
"company_size": profile.company_size,
|
||||
"role": profile.role,
|
||||
"risk_tolerance": profile.risk_tolerance,
|
||||
"decision_style": profile.decision_style,
|
||||
"key_facts": profile.key_facts,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/profile/me/history")
|
||||
def get_my_profile_history(authorization: str | None = Header(default=None)):
|
||||
"""获取当前登录用户的画像历史版本。"""
|
||||
phone = get_current_phone(authorization)
|
||||
if not phone:
|
||||
raise HTTPException(401, "未登录或登录已过期")
|
||||
return {"results": profile_store.list_history(phone)}
|
||||
|
||||
|
||||
# ===== 用户设置持久化 =====
|
||||
|
||||
@app.get("/settings")
|
||||
@@ -1194,18 +1268,71 @@ async def admin_upload_expert_avatar(expert_id: int, file: UploadFile = File(...
|
||||
|
||||
|
||||
@app.get("/admin/users")
|
||||
def admin_list_users():
|
||||
"""管理端:列出所有注册用户。"""
|
||||
def admin_list_users(q: str | None = None, limit: int = 50):
|
||||
"""管理端:列出所有注册用户,支持搜索,含对话统计和状态。"""
|
||||
with user_store._conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id, phone, name, created_at FROM users ORDER BY created_at DESC"
|
||||
)
|
||||
if q:
|
||||
cur.execute(
|
||||
"SELECT id, phone, name, status, created_at FROM users "
|
||||
"WHERE phone LIKE %s OR name LIKE %s "
|
||||
"ORDER BY created_at DESC LIMIT %s",
|
||||
(f"%{q}%", f"%{q}%", limit),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"SELECT id, phone, name, status, created_at FROM users "
|
||||
"ORDER BY created_at DESC LIMIT %s",
|
||||
(limit,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
return {"results": [
|
||||
{"id": r[0], "phone": r[1][:3] + "****" + r[1][-4:], "name": r[2], "created_at": str(r[3])}
|
||||
for r in rows
|
||||
]}
|
||||
# 批量查询每个用户的对话数和最后活跃时间
|
||||
results = []
|
||||
for r in rows:
|
||||
uid, phone, name, status, created_at = r
|
||||
cur.execute(
|
||||
"SELECT COUNT(*), MAX(created_at), "
|
||||
"(array_agg(meta_data->>'scene' ORDER BY created_at DESC) FILTER (WHERE role = 'assistant'))[1] "
|
||||
"FROM chat_messages WHERE phone = %s",
|
||||
(phone,),
|
||||
)
|
||||
stat = cur.fetchone()
|
||||
msg_count = stat[0] or 0
|
||||
last_active = stat[1]
|
||||
last_scene = stat[2]
|
||||
results.append({
|
||||
"id": uid,
|
||||
"phone": phone[:3] + "****" + phone[-4:],
|
||||
"name": name,
|
||||
"status": status,
|
||||
"created_at": str(created_at),
|
||||
"message_count": msg_count,
|
||||
"last_active": str(last_active) if last_active else None,
|
||||
"last_scene": last_scene,
|
||||
})
|
||||
return {"results": results}
|
||||
|
||||
|
||||
@app.post("/admin/users/{user_id}/reset-password")
|
||||
def admin_reset_password(user_id: int, body: dict):
|
||||
"""管理端:重置用户密码。"""
|
||||
new_password = body.get("password", "")
|
||||
if not new_password or len(new_password) < 6:
|
||||
raise HTTPException(400, "密码至少 6 位")
|
||||
if not user_store.reset_password(user_id, new_password):
|
||||
raise HTTPException(404, "用户不存在")
|
||||
return {"reset": True}
|
||||
|
||||
|
||||
@app.post("/admin/users/{user_id}/status")
|
||||
def admin_update_user_status(user_id: int, body: dict):
|
||||
"""管理端:激活/停用用户。"""
|
||||
status = body.get("status", "")
|
||||
if status not in ("active", "inactive"):
|
||||
raise HTTPException(400, "状态应为 active 或 inactive")
|
||||
if not user_store.update_status(user_id, status):
|
||||
raise HTTPException(404, "用户不存在")
|
||||
return {"status": status}
|
||||
|
||||
|
||||
@app.get("/admin/stats")
|
||||
|
||||
Reference in New Issue
Block a user