From b618bcc30d5c3541be4282eb8018cebb9387c361 Mon Sep 17 00:00:00 2001 From: liuwei Date: Wed, 6 May 2026 11:39:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E7=BE=A4=E8=BF=90=E8=90=A5?= =?UTF-8?q?=E5=88=86=E6=9E=902.0=E9=A6=96=E7=89=88=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更项: 1. 在现有群详情接口中追加群画像摘要、成员分层、动作建议和最近群总结数据,保留原有健康度、趋势、排行与运营建议结构。 2. 为后台服务补充 message_summary 数据访问对象,复用现有群总结数据作为群运营分析输入。 3. 在通讯录管理的群详情面板中新增群画像摘要、成员分层和可执行动作建议卡片,保持旧页面内容不删除,仅做加法增强。 --- admin/dashboard/blueprints/robot.py | 354 ++++++++++++++++++ admin/dashboard/server.py | 6 + .../templates/contacts_management.html | 293 +++++++++++++++ 3 files changed, 653 insertions(+) diff --git a/admin/dashboard/blueprints/robot.py b/admin/dashboard/blueprints/robot.py index f5c21cd..438a31a 100644 --- a/admin/dashboard/blueprints/robot.py +++ b/admin/dashboard/blueprints/robot.py @@ -4,11 +4,336 @@ from loguru import logger from utils.robot_cmd.robot_command import GroupBotManager, Feature, PermissionStatus from datetime import datetime, timedelta +from db.base import BaseDBOperator +from plugins.ai_auto_response.memory.group_memory_profile import GroupMemoryService + # 创建群管理兼容蓝图(旧入口保留做跳转,实际功能已并入通讯录) robot_bp = Blueprint('robot', __name__, url_prefix='/robot') LOG = logger +def _build_group_ops_profile(server, group_id: str, group_name: str, peak_hours: list, plugin_stats: list) -> dict: + """构建群运营 2.0 所需的群画像摘要。 + + 设计说明: + 1. 这里优先复用现有群画像快照与消息总结能力,不重新发明一套分析链路; + 2. 若历史画像数据不足,也必须返回可展示的兜底结构,避免前端出现大片空白; + 3. 输出结构专门面向后台运营视图,强调“群是什么群、最近聊什么、适合怎么运营”。 + """ + profile_service = GroupMemoryService(server.db_manager, {}) + profile = profile_service.build_group_memory_profile(group_id, group_name=group_name) or {} + + style_profile = profile.get("style_profile", {}) or {} + focus_topics = [str(item).strip() for item in (profile.get("focus_topics") or []) if str(item).strip()] + structured_summary = profile.get("structured_summary", {}) or {} + recent_key_points = [ + str(item).strip() for item in (structured_summary.get("recent_key_points") or []) if str(item).strip() + ][:4] + unresolved_points = [ + str(item).strip() for item in (structured_summary.get("unresolved_points") or []) if str(item).strip() + ][:3] + stable_topics = [ + str(item).strip() for item in (structured_summary.get("stable_topics") or []) if str(item).strip() + ][:4] + + peak_hour_labels = [str(item.get("label") or "").strip() for item in peak_hours if str(item.get("label") or "").strip()] + dominant_plugin_names = [] + for item in plugin_stats[:3]: + plugin_name = str(item.get("plugin_name") or "").strip() + if plugin_name and plugin_name not in dominant_plugin_names: + dominant_plugin_names.append(plugin_name) + + # 群定位摘要优先使用群画像服务的领域判断,再结合真实主题给出一段更像“运营结论”的描述。 + inferred_domain = str(profile.get("inferred_domain") or "general").strip() + domain_label_map = { + "openclaw": "工作流 / 编排讨论型群", + "robotics": "机器人 / 自动化协作型群", + "dota": "Dota / 游戏讨论型群", + "tech": "技术排障 / 工具协作型群", + "casual": "熟人闲聊 / 日常水群型群", + "general": "综合交流型群", + } + group_identity = domain_label_map.get(inferred_domain, "综合交流型群") + + profile_tags = [ + tag for tag in [ + group_identity, + str(style_profile.get("interaction_tone") or "").strip(), + str(style_profile.get("humor_style") or "").strip(), + str(style_profile.get("expressiveness_style") or "").strip(), + ] if tag + ] + + summary_lines = [] + if focus_topics: + summary_lines.append(f"近期高频主题集中在:{'、'.join(focus_topics[:4])}") + if peak_hour_labels: + summary_lines.append(f"活跃时段主要落在:{'、'.join(peak_hour_labels[:2])}") + if dominant_plugin_names: + summary_lines.append(f"当前最容易被接受的机器人能力偏向:{'、'.join(dominant_plugin_names[:2])}") + if not summary_lines: + summary_lines.append("当前历史数据仍偏少,建议先继续积累群消息、总结与插件使用记录。") + + return { + "group_identity": group_identity, + "profile_tags": profile_tags, + "focus_topics": focus_topics[:6], + "stable_topics": stable_topics, + "recent_key_points": recent_key_points, + "unresolved_points": unresolved_points, + "summary_text": ";".join(summary_lines), + "interaction_tone": str(style_profile.get("interaction_tone") or "").strip(), + "humor_style": str(style_profile.get("humor_style") or "").strip(), + "expressiveness_style": str(style_profile.get("expressiveness_style") or "").strip(), + "peak_hours": peak_hour_labels[:3], + "dominant_plugins": dominant_plugin_names[:3], + "cache_status": str(profile.get("cache_status") or "").strip(), + "last_generated_at": str(profile.get("last_generated_at") or "").strip(), + } + + +def _load_recent_group_summaries(server, group_id: str, limit: int = 3) -> list: + """读取最近几条群总结,供运营面板直接展示。 + + 设计说明: + 1. 群画像适合给“结论”,但运营者往往还想看到最近几期总结到底说了什么; + 2. 这里不把整段长文原样吐给前端,而是裁成可扫读的短摘要,避免弹窗过重; + 3. 若没有总结记录,返回空列表,由前端优雅降级。 + """ + summary_db = getattr(server, "message_summary_db", None) + if summary_db is None: + return [] + + rows = summary_db.execute_query( + """ + SELECT period_key, summary_type, source_message_count, summary_text, last_generated_at + FROM t_message_summary + WHERE chatroom_id = %s + ORDER BY period_end DESC, update_time DESC + LIMIT %s + """, + (group_id, int(limit)), + ) or [] + + result = [] + for row in rows: + raw_summary = str(row.get("summary_text") or "").strip() + normalized_summary = " ".join(raw_summary.split()) + result.append({ + "period_key": str(row.get("period_key") or "").strip(), + "summary_type": str(row.get("summary_type") or "").strip(), + "source_message_count": int(row.get("source_message_count") or 0), + "last_generated_at": str(row.get("last_generated_at") or "").strip(), + "summary_excerpt": normalized_summary[:180] + ("..." if len(normalized_summary) > 180 else ""), + }) + return result + + +def _load_value_rank_top_members(server, group_id: str, limit: int = 5) -> list: + """读取群内最近一期高价值成员快照。 + + 说明: + 1. 该榜单直接复用已落库的身价快照,而不是运行时重新算分; + 2. 若群里尚未开启或尚未产出这类数据,则静默返回空列表; + 3. 返回结构会补齐成员展示名,避免前端出现一串 wxid。 + """ + db = BaseDBOperator(server.db_manager) + latest_row = db.execute_query( + """ + SELECT MAX(stat_date) AS latest_stat_date + FROM t_value_rank_snapshot + WHERE group_id = %s + """, + (group_id,), + fetch_one=True, + ) or {} + latest_stat_date = latest_row.get("latest_stat_date") + if not latest_stat_date: + return [] + + rows = db.execute_query( + """ + SELECT user_id, score, rank_no, title, points_total, msg_count_7d, active_days_30, inactive_days + FROM t_value_rank_snapshot + WHERE group_id = %s AND stat_date = %s + ORDER BY rank_no ASC + LIMIT %s + """, + (group_id, latest_stat_date, int(limit)), + ) or [] + + result = [] + for row in rows: + user_id = str(row.get("user_id") or "").strip() + result.append({ + "wxid": user_id, + "display_name": server.contact_manager.get_group_name(group_id, user_id) or user_id, + "score": float(row.get("score") or 0.0), + "rank_no": int(row.get("rank_no") or 0), + "title": str(row.get("title") or "").strip(), + "points_total": int(row.get("points_total") or 0), + "msg_count_7d": int(row.get("msg_count_7d") or 0), + "active_days_30": int(row.get("active_days_30") or 0), + "inactive_days": int(row.get("inactive_days") or 0), + }) + return result + + +def _load_activation_candidates(server, group_id: str, limit: int = 6) -> list: + """识别“待激活成员”。 + + 口径选择: + 1. 不直接拿最沉默的人,因为那类成员很多已经处于长期静默,激活收益未必最高; + 2. 这里优先找“7~30 天未发言,但仍不是完全流失”的边缘成员,更适合做召回动作; + 3. 这类成员通常对低门槛互动、固定时段提醒更敏感,运营价值更高。 + """ + db = BaseDBOperator(server.db_manager) + rows = db.execute_query( + """ + SELECT + wxid, + COALESCE(NULLIF(display_name, ''), nick_name, wxid) AS display_name, + latest_active_time, + TIMESTAMPDIFF(DAY, latest_active_time, NOW()) AS inactivity_days + FROM t_chatroom_member + WHERE chatroom_id = %s + AND status = 1 + AND latest_active_time IS NOT NULL + AND latest_active_time < DATE_SUB(NOW(), INTERVAL 7 DAY) + AND latest_active_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) + ORDER BY latest_active_time DESC + LIMIT %s + """, + (group_id, int(limit)), + ) or [] + + result = [] + for row in rows: + result.append({ + "wxid": str(row.get("wxid") or "").strip(), + "display_name": str(row.get("display_name") or "").strip(), + "latest_active_time": str(row.get("latest_active_time") or "").strip(), + "inactivity_days": int(row.get("inactivity_days") or 0), + }) + return result + + +def _build_ops_member_segments(server, group_id: str, speaker_ranking: list, inactive_members: list) -> dict: + """构建群成员分层结果。 + + 设计目标: + 1. 保留原有“沉默成员 / 发言排行”表格; + 2. 在此基础上额外产出“核心成员 / 高价值成员 / 待激活成员”三类运营视角; + 3. 前端展示时以卡片为主,让运营者更快找到“该看谁”。 + """ + value_rank_members = _load_value_rank_top_members(server, group_id, limit=5) + activation_candidates = _load_activation_candidates(server, group_id, limit=6) + member_context_map = { + str(item.get("wxid") or "").strip(): item + for item in (server.member_context_db.list_group_member_contexts(group_id) or []) + } + + core_members = [] + for item in speaker_ranking[:5]: + wxid = str(item.get("sender") or "").strip() + context = member_context_map.get(wxid, {}) or {} + core_members.append({ + "wxid": wxid, + "display_name": str(item.get("display_name") or wxid).strip(), + "message_count": int(item.get("message_count") or 0), + "last_message_time": str(item.get("last_message_time") or "").strip(), + "activity_level": str(context.get("activity_level") or "").strip(), + "summary_text": str(context.get("summary_text") or "").strip(), + }) + + dormant_members = [] + for item in inactive_members[:5]: + wxid = str(item.get("wxid") or "").strip() + context = member_context_map.get(wxid, {}) or {} + dormant_members.append({ + "wxid": wxid, + "display_name": str(item.get("display_name") or wxid).strip(), + "inactivity_days": int(item.get("inactivity_days") or 0), + "latest_active_time": str(item.get("latest_active_time") or "").strip(), + "response_style_hint": str(context.get("response_style_hint") or "").strip(), + }) + + return { + "core_members": core_members, + "value_rank_members": value_rank_members, + "activation_candidates": activation_candidates, + "dormant_members": dormant_members, + } + + +def _build_ops_action_cards(group_name: str, ops_profile: dict, ops_members: dict, plugin_stats: list) -> list: + """生成更偏“可执行动作”的运营建议卡片。 + + 与旧版 `operation_suggestions` 的区别: + 1. 旧版更像诊断提示;新版更强调“下一步怎么做”; + 2. 仍然采用规则组合,避免引入高成本、不可控的 LLM 依赖; + 3. 结构尽量稳定,方便前端做卡片展示与后续继续增强。 + """ + action_cards = [] + peak_hours = ops_profile.get("peak_hours") or [] + focus_topics = ops_profile.get("focus_topics") or [] + dominant_plugins = ops_profile.get("dominant_plugins") or [] + activation_candidates = ops_members.get("activation_candidates") or [] + core_members = ops_members.get("core_members") or [] + + if peak_hours: + action_cards.append({ + "type": "time", + "title": "建议把互动动作放到高峰时段", + "summary": f"优先考虑在 {peak_hours[0]} 发起签到、话题或菜单引导。", + "detail": f"{group_name} 最近高峰主要集中在 {'、'.join(peak_hours[:2])},在这个时间段发起动作更容易拿到首轮反馈。", + }) + + if dominant_plugins: + action_cards.append({ + "type": "feature", + "title": "建议优先强化已被接受的功能入口", + "summary": f"先围绕 {dominant_plugins[0]} 做菜单、公告或欢迎语曝光。", + "detail": f"当前群里真实被用起来的功能更偏向 {'、'.join(dominant_plugins[:3])},说明这类能力更容易被接受,适合作为下一阶段主推入口。", + }) + + if activation_candidates: + candidate_names = [str(item.get("display_name") or "").strip() for item in activation_candidates[:3] if str(item.get("display_name") or "").strip()] + action_cards.append({ + "type": "activation", + "title": "建议优先激活边缘活跃成员", + "summary": f"可以先针对 {'、'.join(candidate_names) if candidate_names else '近 7~30 天边缘成员'} 做轻量召回。", + "detail": "这批成员不是长期沉默用户,仍有回流可能,更适合通过低门槛互动、固定提醒或轻福利重新拉回讨论。", + }) + + if focus_topics: + action_cards.append({ + "type": "topic", + "title": "建议围绕当前高频主题设计话题入口", + "summary": f"优先从 {focus_topics[0]}、{focus_topics[1] if len(focus_topics) > 1 else focus_topics[0]} 这类主题切入。", + "detail": "与其硬推通用活动,不如顺着群里原本就在讨论的内容做问题、投票、接龙或功能引导,转化更自然。", + }) + + if core_members: + core_names = [str(item.get("display_name") or "").strip() for item in core_members[:3] if str(item.get("display_name") or "").strip()] + action_cards.append({ + "type": "core", + "title": "建议借核心成员带动扩散", + "summary": f"可以优先让 {'、'.join(core_names) if core_names else '核心活跃成员'} 参与首轮互动。", + "detail": "核心成员往往能决定首轮响应速度和讨论氛围,先让他们接住话题,比单纯增加曝光更有效。", + }) + + if not action_cards: + action_cards.append({ + "type": "fallback", + "title": "建议先继续积累群画像与使用数据", + "summary": "当前数据不足以给出更细的动作建议。", + "detail": "可以先保持基础运营动作,等群总结、成员画像和插件使用数据更完整后,再做更细的策略推荐。", + }) + + return action_cards[:4] + + # 旧群权限页入口兼容跳转 @robot_bp.route('/') @login_required @@ -373,6 +698,31 @@ def api_group_detail(group_id): "desc": "当前活跃、消息量、插件调用都比较均衡,建议继续围绕高峰时段和高频插件做强化。" }) + # 群运营分析 2.0 采用“增量增强”策略: + # 1. 现有 overview / diagnosis / operation_suggestions 等结构继续保留,避免影响旧页面; + # 2. 新增 ops_profile / ops_members / ops_actions 三类字段,供现有详情页逐步增强使用; + # 3. 即使某些增强数据暂时为空,也不应影响老内容展示。 + ops_profile = _build_group_ops_profile( + server=server, + group_id=group_id, + group_name=group_name, + peak_hours=peak_hours, + plugin_stats=plugin_stats, + ) + ops_members = _build_ops_member_segments( + server=server, + group_id=group_id, + speaker_ranking=speaker_ranking, + inactive_members=inactive_members, + ) + recent_summaries = _load_recent_group_summaries(server, group_id, limit=3) + ops_actions = _build_ops_action_cards( + group_name=group_name, + ops_profile=ops_profile, + ops_members=ops_members, + plugin_stats=plugin_stats, + ) + return jsonify({ "success": True, "data": { @@ -442,6 +792,10 @@ def api_group_detail(group_id): "plugin_summary": plugin_summary, "plugin_stats": plugin_stats, "operation_suggestions": operation_suggestions, + "ops_profile": ops_profile, + "ops_members": ops_members, + "ops_actions": ops_actions, + "recent_summaries": recent_summaries, } }) except Exception as e: diff --git a/admin/dashboard/server.py b/admin/dashboard/server.py index 02c2b6b..9248d2d 100644 --- a/admin/dashboard/server.py +++ b/admin/dashboard/server.py @@ -18,6 +18,7 @@ from db.admin_account_db import AdminAccountDBOperator from db.emoji_asset_db import EmojiAssetDB from db.member_context_db import MemberContextDBOperator from db.message_storage import MessageStorageDB +from db.message_summary_db import MessageSummaryDBOperator from db.stats_db import StatsDBOperator from db.task_db import TaskDBOperator from db.fun_command_rule_db import FunCommandRuleDBOperator @@ -61,6 +62,11 @@ class DashboardServer: # 3. 因此这里优先复用 Robot 已初始化的 message_storage,没有则再安全回退到 DB 层对象。 self.message_storage = getattr(robot_instance, "message_storage", None) or MessageStorageDB(self.db_manager) self.emoji_asset_db = getattr(self.message_storage, "emoji_asset_db", None) or EmojiAssetDB(self.db_manager) + # 群运营分析 2.0 会直接复用群消息总结表: + # 1. 这类数据已经由现有插件产出,不需要另起一套采集逻辑; + # 2. 统一在 DashboardServer 上挂载,便于多个后台蓝图复用; + # 3. 即使对应插件未在当前请求时运行,数据库读能力也应保持可用。 + self.message_summary_db = MessageSummaryDBOperator(self.db_manager) self.contact_db: ContactsDBOperator = ContactsDBOperator(self.db_manager) self.member_context_db = MemberContextDBOperator(self.db_manager) self.task_db: TaskDBOperator = TaskDBOperator(self.db_manager) diff --git a/admin/dashboard/templates/contacts_management.html b/admin/dashboard/templates/contacts_management.html index a3f226d..5788610 100644 --- a/admin/dashboard/templates/contacts_management.html +++ b/admin/dashboard/templates/contacts_management.html @@ -262,6 +262,140 @@ + +
+
+

群画像摘要

+
+ + + +
+ 群定位与讨论风格 + 2.0 增强摘要 +
+
{% raw %}{{ groupInsight.ops_profile.group_identity || '综合交流型群' }}{% endraw %}
+
+ + {% raw %}{{ tag }}{% endraw %} + + 暂无群画像标签 +
+
{% raw %}{{ groupInsight.ops_profile.summary_text || '暂无群画像摘要' }}{% endraw %}
+
+
高频主题
+
+ + {% raw %}{{ topic }}{% endraw %} + + 暂无高频主题 +
+
+
+
待跟进问题
+
+
+ {% raw %}{{ item }}{% endraw %} +
+
+
+
+
+ + +
+ 最近群总结 + 复用现有 message_summary +
+
+
+
+ {% raw %}{{ item.period_key || '-' }}{% endraw %} + {% raw %}{{ item.source_message_count || 0 }}{% endraw %} 条消息 +
+
{% raw %}{{ item.summary_excerpt || '暂无摘要内容' }}{% endraw %}
+
+
+
当前还没有可展示的群总结记录
+
+
+
+
+ + +
+
+

成员分层

+
+ + + +
+ 核心成员 + 近30天发言核心 +
+
+
+
{% raw %}{{ item.display_name }}{% endraw %}
+
消息数 {% raw %}{{ item.message_count }}{% endraw %} · {% raw %}{{ item.activity_level || '未分层' }}{% endraw %}
+
+
+
暂无核心成员数据
+
+
+ + +
+ 高价值成员 + 最近一期身价快照 +
+
+
+
{% raw %}{{ item.display_name }}{% endraw %}
+
#{% raw %}{{ item.rank_no }}{% endraw %} · {% raw %}{{ item.title || '成员' }}{% endraw %} · 分值 {% raw %}{{ item.score }}{% endraw %}
+
+
+
暂无高价值成员快照
+
+
+ + +
+ 待激活成员 + 近7~30天边缘活跃 +
+
+
+
{% raw %}{{ item.display_name }}{% endraw %}
+
距今 {% raw %}{{ item.inactivity_days }}{% endraw %} 天未发言
+
+
+
暂无待激活成员
+
+
+
+
+

运营建议

@@ -280,6 +414,27 @@
+ +
+
+

可执行动作建议

+
+ + + +
{% raw %}{{ item.type || 'action' }}{% endraw %}
+
{% raw %}{{ item.title }}{% endraw %}
+
{% raw %}{{ item.summary }}{% endraw %}
+
{% raw %}{{ item.detail }}{% endraw %}
+
+
+
+
+ @@ -1810,11 +1965,87 @@ display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; } .detail-panels .el-col { margin-bottom: 16px; } + .detail-panels--triple .el-col, + .detail-panels--double .el-col { margin-bottom: 16px; } .detail-card { border-radius: 18px; } + .detail-card--profile { + background: linear-gradient(180deg, rgba(14,165,233,0.08), rgba(255,255,255,0.98)); + } .detail-card-header { display: flex; align-items: center; justify-content: space-between; font-weight: 600; color: #0f172a; } .detail-card-sub { font-size: 12px; color: #94a3b8; font-weight: 500; } + .ops-profile-title { + font-size: 22px; + font-weight: 700; + color: #0f172a; + margin-bottom: 12px; + } + .ops-profile-summary { + margin-top: 14px; + padding: 14px 16px; + border-radius: 14px; + background: rgba(255,255,255,0.78); + border: 1px solid rgba(148,163,184,0.12); + color: #334155; + line-height: 1.8; + font-size: 13px; + } + .ops-topic-block { + margin-top: 16px; + } + .ops-subtitle { + font-size: 13px; + font-weight: 700; + color: #475569; + margin-bottom: 10px; + } + .ops-bullet-list { + display: flex; + flex-direction: column; + gap: 8px; + } + .ops-bullet-item { + padding: 10px 12px; + border-radius: 12px; + background: rgba(248,250,252,0.9); + color: #475569; + border: 1px solid rgba(148,163,184,0.12); + line-height: 1.6; + font-size: 12px; + } + .ops-summary-timeline { + display: flex; + flex-direction: column; + gap: 12px; + } + .ops-summary-item { + padding: 14px 14px 12px; + border-radius: 14px; + background: linear-gradient(180deg, rgba(248,250,252,0.9), rgba(255,255,255,0.98)); + border: 1px solid rgba(148,163,184,0.12); + } + .ops-summary-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; + } + .ops-summary-period { + font-size: 13px; + font-weight: 700; + color: #0f172a; + } + .ops-summary-meta { + font-size: 11px; + color: #94a3b8; + } + .ops-summary-excerpt { + font-size: 12px; + color: #475569; + line-height: 1.7; + } .feature-chip-list { display: flex; gap: 8px; flex-wrap: wrap; min-height: 60px; align-items: flex-start; } .empty-inline, .detail-inline-note { font-size: 12px; color: #64748b; } .group-announcement-wrap { display: flex; gap: 12px; align-items: flex-start; justify-content: space-between; } @@ -1822,6 +2053,66 @@ .detail-inline-note { margin-top: 12px; line-height: 1.6; } .suggestion-list { display: flex; flex-direction: column; gap: 12px; } .suggestion-item { border-radius: 14px; } + .ops-member-list { + display: flex; + flex-direction: column; + gap: 10px; + } + .ops-member-item { + padding: 12px 14px; + border-radius: 14px; + background: rgba(248,250,252,0.9); + border: 1px solid rgba(148,163,184,0.12); + } + .ops-member-name { + font-size: 13px; + font-weight: 700; + color: #0f172a; + margin-bottom: 6px; + } + .ops-member-meta { + font-size: 12px; + color: #64748b; + line-height: 1.6; + } + .action-card { + min-height: 180px; + background: linear-gradient(180deg, rgba(249,115,22,0.06), rgba(255,255,255,0.98)); + } + .action-card-type { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 24px; + padding: 0 10px; + border-radius: 999px; + background: rgba(249,115,22,0.12); + color: #c2410c; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .04em; + margin-bottom: 10px; + } + .action-card-title { + font-size: 16px; + font-weight: 700; + color: #0f172a; + margin-bottom: 10px; + line-height: 1.5; + } + .action-card-summary { + font-size: 13px; + font-weight: 600; + color: #334155; + line-height: 1.7; + margin-bottom: 10px; + } + .action-card-detail { + font-size: 12px; + color: #64748b; + line-height: 1.8; + } .peak-hour-list { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; } .peak-hour-item { min-width: 132px; padding: 12px 14px; border-radius: 14px; @@ -1946,6 +2237,8 @@ .page-hero-actions, .detail-tags { justify-content: flex-start; } .hero-search, .group-search { width: 100%; } .diagnosis-grid { grid-template-columns: 1fr; } + .detail-panels--triple .el-col, + .detail-panels--double .el-col { width: 100%; } .chat-header-card { flex-direction: column; align-items: flex-start; } .chat-header-actions { justify-content: flex-start; } .message-content { max-width: 92%; }