feat: 品牌更名、用户管理增强、画像版本管理、对话体验优化
CI / 后端测试 (push) Has been cancelled
CI / 前端构建 (push) Has been cancelled

- 品牌统一为"左右(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:
selfrelease
2026-08-30 09:05:31 +08:00
co-authored by Devin
parent 038ab17278
commit 4e2b4666aa
24 changed files with 1623 additions and 333 deletions
+138 -11
View File
@@ -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")