"""Owner-facing surfaces: tier-1 help, status health, project switch brief.""" from __future__ import annotations import os from pathlib import Path from typing import Any, cast from butler.gateway.owner_surface_ops import ( degradation_brief_line_safe, outbound_brief_line_safe, pending_delegate_line_safe, project_todos_brief_safe, runtime_jobs_lines_safe, ) from butler.ops.butler_inbox import _action_count, collect_inbox_snapshot from butler.project.lead import is_lead_project from butler.project.meta import lifecycle_label from butler.defaults.env_defaults import ENV_PROFILE_DEFAULT _OWNER_HELP_DEFAULT = """Butler — 五个说法就够 查 /状态 /简报 — 什么状况、今天要干嘛 改 /改 <路径> <目标> 或 交给开发代理… — 小改动与测试 批 /确认 /停止 — 流程与中断 记 刚才不对… /反馈 — 纠正我 管 /切换 /项目 /分工 — 项目与和 CC 配合 详细命令:/帮助 高级""" def format_owner_help_default() -> str: return _OWNER_HELP_DEFAULT.strip() def format_owner_help_advanced() -> str: from butler.gateway.commands.help_handlers import _HELP_GROUPS lines = [ "Butler 全部命令(/帮助 <主题> 可看单组)", "", "主题:项目 · 对话 · 记忆 · 权限 · 开发 · 日常 · 管理 · 规划 · 模型", "", ] for group, (title, _body) in _HELP_GROUPS.items(): lines.append(f" /帮助 {group} — {title}") lines.append("") lines.append("运维:/诊断 详细(全量)· /doctor(安全审计)") return "\n".join(lines) def _health_icon(ok: bool, warn: bool = False) -> str: if ok: return "🟢" if warn: return "🟡" return "🔴" def format_owner_status_header( orchestrator: Any, session_key: str, *, health: dict[str, Any] | None = None, ) -> list[str]: """Owner-readable health lines for /状态 (non-technical).""" sk = str(session_key or "").strip() snap = collect_inbox_snapshot(orchestrator, sk, health=health) actions = _action_count(snap) proj_ok = bool(snap.project_name and snap.project_name != "(无)") model_ok = bool(getattr(orchestrator, "_settings", None) and orchestrator._settings.default_provider) pending_delegate = _pending_delegate_line(sk) bool(pending_delegate) lines = [ "健康概览", f" {_health_icon(True)} 网关在线", f" {_health_icon(proj_ok)} 当前项目:{snap.project_name or '未选择'}", f" {_health_icon(model_ok)} 模型通道:{orchestrator._settings.default_provider if model_ok else '未配置'}", f" {_health_icon(actions == 0, warn=actions > 0)} 待处理:" + (f"{actions} 类 → /简报" if actions else "无"), ] if pending_delegate: lines.append(f" {_health_icon(False, warn=True)} {pending_delegate}") prof = os.getenv("BUTLER_ENV_PROFILE", ENV_PROFILE_DEFAULT).strip() if prof: lines.append(f" 环境:{prof}(lead=生产关 terminal · dev-gateway=Linux 沙箱)") return lines def _pending_delegate_line(session_key: str) -> str: return cast(str, pending_delegate_line_safe(session_key)) def format_project_switch_brief( orchestrator: Any, session_key: str, project_name: str, ) -> str: """Three-line (+ lead hint) summary after /切换.""" pm = orchestrator.project_manager proj = pm.get_project(project_name) or pm.get_current(session_key=session_key) lines = ["", "── 项目摘要 ──"] lc = lifecycle_label(proj) if proj else "active" pack = (getattr(proj, "pack", "") or "").strip() if proj else "" state_bits = [f"状态 {lc or 'active'}"] if pack: state_bits.append(f"能力包 {pack}") lines.append(f"· {' · '.join(state_bits)}") ws = Path(getattr(proj, "workspace", "") or "") if proj else None if ws and ws.is_dir(): brief = project_todos_brief_safe(ws) if brief: lines.append(brief) delegate_line = _pending_delegate_line(session_key) if delegate_line: lines.append(f"· {delegate_line}") else: lines.append("· 委派:无进行中的后台任务") if proj and is_lead_project(proj.name, project=proj): lines.append( "· 建议:改代码说「交给开发代理…」;只读巡检用 /运行;Lead 不直接开 terminal" ) return "\n".join(lines) def _owner_outbound_brief_line(*, session_key: str = "", chat_id: str = "") -> str | None: return cast( str | None, outbound_brief_line_safe(session_key=session_key, chat_id=chat_id), ) def _owner_degradation_brief_line( orchestrator: Any, *, session_key: str = "", health: dict[str, Any] | None = None, ) -> str | None: return cast( str | None, degradation_brief_line_safe( orchestrator, session_key=session_key, health=health, ), ) def _owner_memory_degradation_brief_line( orchestrator: Any, *, session_key: str = "", health: dict[str, Any] | None = None, ) -> str | None: """Backward-compat alias — prefer :func:`_owner_degradation_brief_line`.""" return _owner_degradation_brief_line( orchestrator, session_key=session_key, health=health ) def format_owner_diagnostic_brief( orchestrator: Any, session_key: str, *, health: dict[str, Any] | None = None, ) -> str: """Owner-tier /诊断 — three human lines; full ops via ``/诊断 详细``.""" sk = str(session_key or "").strip() snap = collect_inbox_snapshot(orchestrator, sk, health=health) actions = _action_count(snap) proj_ok = bool(snap.project_name and snap.project_name != "(无)") proj_name = snap.project_name or "未选择" delegate = _pending_delegate_line(sk) todo_warn = actions > 0 or bool(delegate) lines = [ "Butler 简要诊断", "", f"网关:{_health_icon(True)} 在线", f"项目:{_health_icon(proj_ok)} {proj_name}", ] if todo_warn: bits: list[str] = [] if actions: bits.append(f"{actions} 类待关注") if delegate: bits.append("委派进行中") lines.append(f"待办:{_health_icon(False, warn=True)} {' · '.join(bits)} → /简报") else: lines.append(f"待办:{_health_icon(True)} 无") outbound = _owner_outbound_brief_line(session_key=sk) if outbound: lines.append(outbound) degradation = _owner_degradation_brief_line( orchestrator, session_key=sk, health=health ) if degradation: lines.append(degradation) lines.append("") lines.append("完整快照:/诊断 详细") return "\n".join(lines) _CC_COMPLEMENT_TEXT = """Butler 与 Claude Code / Cursor 分工 · 微信 Butler(莎丽):远程统筹、多项目记忆、委派派工、门控与验收 · Butler 委派 dev:常规改码、pytest、项目内配置(project.yaml 等) · CC CLI(/cc-bridge):网关宿主机上的 claude 命令,适合重 refactor · 本机 Cursor/CC:人在电脑前时的 IDE + LSP 推荐路径: 查状态 / 读文件 → 直接说 改代码 / 跑测试 → 「交给开发代理…」或 /运行 重编码 / 长环修复 → /cc-bridge …(需 BUTLER_CC_BRIDGE=1) 沙箱 / npm 被挡 → /沙箱 · /批准沙箱外 相关:/今日 · /简报 · /帮助 开发""" def format_cc_complement_message() -> str: return _CC_COMPLEMENT_TEXT.strip() def format_project_today_view( orchestrator: Any, session_key: str, *, health: dict[str, Any] | None = None, ) -> str: """Owner one-screen: health + todos + delegate + reminders + next actions.""" sk = str(session_key or "").strip() snap = collect_inbox_snapshot(orchestrator, sk, health=health) lines = ["📅 今日 · " + (snap.project_name or "未选项目"), ""] lines.extend(format_owner_status_header(orchestrator, sk, health=health)) lines.append("") lines.append("优先事项") any_item = False if snap.project_todos_open: any_item = True lines.append(f"· 待办 {snap.project_todos_open} 项") for item in snap.project_todo_samples[:2]: lines.append(f" - {item}") if snap.project_todos_open > 2: lines.append(" … → /项目待办") delegate = _pending_delegate_line(sk) if delegate: any_item = True lines.append(f"· {delegate}") if snap.reminders_pending: any_item = True lines.append(f"· 提醒 {snap.reminders_pending} 个待触发") for item in snap.reminder_samples[:2]: lines.append(f" - {item}") if snap.memory_pending: any_item = True lines.append(f"· 记忆待审 {snap.memory_pending} 条 → /记忆待审") if snap.workflow_gate: any_item = True lines.append(f"· {snap.workflow_gate[:100]}") if not any_item: lines.append("· 暂无紧急项 ✅") lines.append("") lines.append("快捷入口:/运行 · /工作流 · /委派质量 · /分工") return "\n".join(lines) def _runtime_jobs_owner_lines(workspace: Path) -> list[str]: return cast(list[str], runtime_jobs_lines_safe(workspace)) def format_project_overview_owner( orchestrator: Any, session_key: str, *, health: dict[str, Any] | None = None, ) -> str: """Owner-friendly /项目概况 — action-oriented, not file/git stats.""" pm = orchestrator.project_manager proj = pm.get_current(session_key=str(session_key or "").strip()) if proj is None: return "请先 /切换 到目标项目" ws = Path(getattr(proj, "workspace", "") or "") if not ws.is_dir(): return f"项目 {proj.name} 的工作区不可用" snap = collect_inbox_snapshot(orchestrator, session_key, health=health) lc = lifecycle_label(proj) pack = (getattr(proj, "pack", "") or "").strip() lines = [f"📊 {proj.name} · 项目概况", ""] meta = [f"状态 {lc or 'active'}"] if pack: meta.append(f"能力 {pack}") if is_lead_project(proj.name, project=proj): meta.append("Lead") lines.append("· " + " · ".join(meta)) lines.append("") lines.append("今日焦点") if snap.project_todos_open: lines.append(f"· 待办 {snap.project_todos_open} 项") for item in snap.project_todo_samples[:3]: lines.append(f" - {item}") else: lines.append("· 待办:无未完成项") delegate = _pending_delegate_line(session_key) if delegate: lines.append(f"· {delegate}") if snap.reminders_pending: lines.append(f"· 提醒 {snap.reminders_pending} 个待触发") if snap.memory_pending: lines.append(f"· 记忆待审 {snap.memory_pending} 条 → /记忆待审") if snap.workflow_gate: lines.append(f"· {snap.workflow_gate[:90]}") lines.extend(_runtime_jobs_owner_lines(ws)) dev = dict(getattr(proj, "dev", None) or {}) if dev.get("test_command"): lines.append("· 验收:委派 dev 后会跑 project.yaml 测试命令") if is_lead_project(proj.name, project=proj): lines.append("· 建议:改代码走委派;重编码用本机 CC → /分工") lines.append("") lines.append("更多:/今日 · /简报 · /项目待办") lines.append("运维统计:/项目概况 详细") return "\n".join(lines) __all__ = [ "format_cc_complement_message", "format_owner_diagnostic_brief", "format_owner_help_advanced", "format_owner_help_default", "format_owner_status_header", "format_project_overview_owner", "format_project_switch_brief", "format_project_today_view", ]