feat: 老板台2.0 + 手机端适配 + 专家库 + 智囊入口
CI / 后端测试 (push) Has been cancelled
CI / 前端构建 (push) Has been cancelled

- 老板台: AI执行摘要卡 + 待办优先级排序 + 时间线视图 + 环形健康度
- 手机端: 事项看板折叠列表 + 7个页面响应式修复 + 底部安全边距
- 专家库: 新增用户端专家浏览页(参考管理端卡片风格)
- 智囊入口: 底部Tab加"智囊" + 4个智囊页场景切换栏 + "我"页分组入口
- FAB弹窗: 场景快速入口 + 指令卡片优化 + 输入区sticky
- 后端: /dashboard/summary + /dashboard/timeline API
- 后端: life场景事实维度扩充(2→5) + 17个生活关键词优先级
- 前端: 抑制React DevTools startTime噪音错误

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-31 14:11:09 +08:00
co-authored by Devin
parent 4de36547cd
commit dd01d94ade
24 changed files with 1088 additions and 300 deletions
+166
View File
@@ -2410,6 +2410,172 @@ def dashboard_today(authorization: str | None = Header(default=None)):
conn.close()
@app.get("/dashboard/summary")
def dashboard_summary(authorization: str | None = Header(default=None)):
"""老板台执行摘要:AI 用一句话总结当前状态和建议行动。
降级策略:Kimi 不可用时返回规则摘要。
"""
phone = get_current_phone(authorization)
if not phone:
raise HTTPException(401, "未登录")
conn = _matter_dsn()
try:
cur = conn.cursor()
cur.execute(
"SELECT id, title, risk_level, scene, status FROM matters "
"WHERE user_phone = %s AND status != 'closed' ORDER BY updated_at DESC LIMIT 10",
(phone,),
)
rows = cur.fetchall()
open_matters = [{"id": r[0], "title": r[1], "risk_level": r[2], "scene": r[3], "status": r[4]} for r in rows]
cur.execute(
"SELECT COUNT(*) FROM matters WHERE user_phone = %s AND status = 'pending_confirm'",
(phone,),
)
pending_count = cur.fetchone()[0]
cur.execute(
"SELECT COUNT(*) FROM expert_consultations WHERE user_phone = %s AND status = 'replied'",
(phone,),
)
reply_count = cur.fetchone()[0]
finally:
conn.close()
red_count = sum(1 for m in open_matters if m["risk_level"] == "red")
orange_count = sum(1 for m in open_matters if m["risk_level"] == "orange")
# 规则摘要(兜底)
parts = []
if red_count > 0:
parts.append(f"有 {red_count} 项高风险事项需立即处理")
if orange_count > 0:
parts.append(f"{orange_count} 项中风险事项需关注")
if pending_count > 0:
parts.append(f"{pending_count} 个事项待你确认")
if reply_count > 0:
parts.append(f"{reply_count} 条专家回复待查看")
if not parts:
rule_summary = "当前无待办事项,经营状况良好。可主动发起咨询或查看经营健康度。"
rule_action = "浏览智囊或查看事项看板"
else:
rule_summary = "、".join(parts) + "。"
if red_count > 0:
rule_action = "建议优先处理高风险事项"
elif pending_count > 0:
rule_action = "建议先确认待办事项"
elif reply_count > 0:
rule_action = "建议查看专家回复"
else:
rule_action = "建议关注中风险事项"
# 尝试 AI 生成更自然的摘要
try:
if kimi and open_matters:
matter_desc = "\n".join(
f"- {m['title']}(风险:{m['risk_level']},场景:{m['scene']},状态:{m['status']})"
for m in open_matters[:5]
)
prompt = (
f"你是中小企业老板的AI助手。请根据以下数据,用一句话(不超过50字)总结当前经营状态,"
f"并给出一个最优先的行动建议(不超过20字)。\n\n"
f"待办事项:\n{matter_desc}\n"
f"待确认:{pending_count}个,专家回复:{reply_count}条\n\n"
f"返回 JSON 格式:{{\"summary\": \"...\", \"action\": \"...\"}}"
)
answer = kimi.chat(
[{"role": "system", "content": "你是经营顾问助手,只返回JSON。"},
{"role": "user", "content": prompt}],
temperature=0.3, timeout=15,
)
answer = answer.strip()
if answer.startswith("```"):
answer = answer.split("\n", 1)[1] if "\n" in answer else answer[3:]
if answer.endswith("```"):
answer = answer[:-3]
answer = answer.replace("\u201c", '"').replace("\u201d", '"').strip()
start = answer.find("{")
end = answer.rfind("}")
if start != -1 and end != -1:
import json as _json
parsed = _json.loads(answer[start:end + 1])
return {
"summary": parsed.get("summary", rule_summary),
"action": parsed.get("action", rule_action),
"ai_generated": True,
}
except Exception:
pass
return {"summary": rule_summary, "action": rule_action, "ai_generated": False}
@app.get("/dashboard/timeline")
def dashboard_timeline(authorization: str | None = Header(default=None)):
"""老板台时间线:按时间倒序聚合最近活动(案卡变更、专家回复、风险推送)。"""
phone = get_current_phone(authorization)
if not phone:
raise HTTPException(401, "未登录")
conn = _matter_dsn()
try:
cur = conn.cursor()
items = []
# 案卡最近更新
cur.execute(
"SELECT id, title, scene, status, risk_level, updated_at "
"FROM matters WHERE user_phone = %s ORDER BY updated_at DESC LIMIT 8",
(phone,),
)
for r in cur.fetchall():
items.append({
"type": "matter",
"id": r[0],
"title": r[1],
"scene": r[2],
"status": r[3],
"risk_level": r[4],
"time": r[5].isoformat() if r[5] else None,
})
# 专家回复
cur.execute(
"SELECT c.id, e.name, e.title, c.reply, c.replied_at "
"FROM expert_consultations c JOIN experts e ON c.expert_id = e.id "
"WHERE c.user_phone = %s AND c.status = 'replied' "
"ORDER BY c.replied_at DESC LIMIT 5",
(phone,),
)
for r in cur.fetchall():
items.append({
"type": "expert_reply",
"id": r[0],
"expert_name": r[1],
"expert_title": r[2],
"snippet": (r[3] or "")[:60],
"time": r[4].isoformat() if r[4] else None,
})
# 风险推送
try:
cur.execute(
"SELECT id, title, level, created_at FROM risk_alerts "
"WHERE user_phone = %s ORDER BY created_at DESC LIMIT 5",
(phone,),
)
for r in cur.fetchall():
items.append({
"type": "risk_alert",
"id": r[0],
"title": r[1],
"level": r[2],
"time": r[3].isoformat() if r[3] else None,
})
except Exception:
pass # risk_alerts 表可能不存在
# 按时间倒序
items.sort(key=lambda x: x.get("time") or "", reverse=True)
return {"results": items[:15]}
finally:
conn.close()
@app.get("/team")
def my_team(authorization: str | None = Header(default=None)):
"""我的团队:咨询过的专家自动沉淀"""