refactor: 替换 asyncio.get_event_loop 为 get_running_loop 并优化会话卡片

- 将多处 asyncio.get_event_loop() 替换为更安全的 asyncio.get_running_loop()
- 重构 Feishu 卡片功能,新增 build_sessions_card 方法显示所有会话
- 优化文件路径处理逻辑,支持绝对路径和相对路径
- 在健康检查接口中添加 pending_requests 计数
- 更新会话状态命令以支持卡片显示
This commit is contained in:
Yuyao Huang (Sam) 2026-03-28 14:59:33 +08:00
parent 09b63341cd
commit a3622ce26d
7 changed files with 71 additions and 89 deletions

View File

@ -13,7 +13,7 @@ from agent.manager import manager
from agent.scheduler import scheduler
from agent.task_runner import task_runner
from orchestrator.agent import agent
from orchestrator.tools import set_current_user, get_current_user, get_current_chat
from orchestrator.tools import set_current_user, get_current_chat
logger = logging.getLogger(__name__)
@ -111,6 +111,18 @@ async def _cmd_new(user_id: str, args: str) -> str:
conv_id = data.get("conv_id", "")
agent._active_conv[user_id] = conv_id
cwd = data.get("working_dir", working_dir)
chat_id = get_current_chat()
if chat_id:
from bot.feishu import send_card, send_text, build_sessions_card
sessions = manager.list_sessions(user_id=user_id)
mode = "Direct 🟢" if agent.get_passthrough(user_id) else "Smart ⚪"
card = build_sessions_card(sessions, conv_id, mode)
await send_card(chat_id, "chat_id", card)
if initial_msg and data.get("response"):
await send_text(chat_id, "chat_id", data["response"])
return ""
reply = f"✓ Created session `{conv_id}` in `{cwd}`"
if parsed.timeout:
reply += f" (timeout: {parsed.timeout}s)"
@ -124,17 +136,24 @@ async def _cmd_new(user_id: str, args: str) -> str:
async def _cmd_status(user_id: str) -> str:
"""Show status: sessions and current mode."""
sessions = manager.list_sessions(user_id=user_id)
if not sessions:
return "No active sessions."
active = agent.get_active_conv(user_id)
passthrough = agent.get_passthrough(user_id)
mode = "Direct 🟢" if passthrough else "Smart ⚪"
chat_id = get_current_chat()
if chat_id:
from bot.feishu import send_card, build_sessions_card
card = build_sessions_card(sessions, active, mode)
await send_card(chat_id, "chat_id", card)
return ""
if not sessions:
return "No active sessions."
lines = ["**Your Sessions:**\n"]
for i, s in enumerate(sessions, 1):
marker = "" if s["conv_id"] == active else " "
lines.append(f"{marker}{i}. `{s['conv_id']}` - `{s['cwd']}`")
status = "Direct 🟢" if passthrough else "Smart ⚪"
lines.append(f"\n**Mode:** {status}")
lines.append(f"\n**Mode:** {mode}")
lines.append("Use `/switch <n>` to activate a session.")
lines.append("Use `/direct` or `/smart` to change mode.")
return "\n".join(lines)

View File

@ -67,7 +67,7 @@ async def send_text(receive_id: str, receive_id_type: str, text: str) -> None:
text: message content.
"""
parts = _split_message(text)
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
for i, part in enumerate(parts):
logger.debug(
@ -108,59 +108,16 @@ async def send_text(receive_id: str, receive_id_type: str, text: str) -> None:
await asyncio.sleep(0.3)
async def send_card(receive_id: str, receive_id_type: str, title: str, content: str, buttons: list[dict] | None = None) -> None:
async def send_card(receive_id: str, receive_id_type: str, card: dict) -> None:
"""
Send an interactive card message.
Args:
receive_id: chat_id or open_id
receive_id_type: "chat_id" | "open_id" | "user_id" | "union_id"
title: Card title
content: Card content (markdown supported)
buttons: List of button dicts with "text" and "value" keys
receive_id: chat_id or open_id depending on receive_id_type.
receive_id_type: "chat_id" | "open_id" | "user_id" | "union_id".
card: Card content dict (Feishu card JSON schema).
"""
elements = [
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": content,
},
},
]
if buttons:
actions = []
for btn in buttons:
actions.append({
"tag": "button",
"text": {"tag": "plain_text", "content": btn.get("text", "Button")},
"type": "primary",
"value": btn.get("value", {}),
})
elements.append({"tag": "action", "actions": actions})
card = {
"type": "template",
"data": {
"template_id": "AAqkz9****",
"template_variable": {
"title": title,
"elements": elements,
},
},
}
card_content = {
"config": {"wide_screen_mode": True},
"header": {
"title": {"tag": "plain_text", "content": title},
"template": "blue",
},
"elements": elements,
}
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
request = (
CreateMessageRequest.builder()
.receive_id_type(receive_id_type)
@ -168,7 +125,7 @@ async def send_card(receive_id: str, receive_id_type: str, title: str, content:
CreateMessageRequestBody.builder()
.receive_id(receive_id)
.msg_type("interactive")
.content(json.dumps(card_content, ensure_ascii=False))
.content(json.dumps(card, ensure_ascii=False))
.build()
)
.build()
@ -185,6 +142,32 @@ async def send_card(receive_id: str, receive_id_type: str, title: str, content:
logger.debug("Sent card to %s (%s)", receive_id, receive_id_type)
def build_sessions_card(sessions: list[dict], active_conv_id: str | None, mode: str) -> dict:
"""Build a card showing all sessions with active marker and mode info."""
if sessions:
lines = []
for i, s in enumerate(sessions, 1):
marker = "" if s["conv_id"] == active_conv_id else " "
started = "🟢" if s["started"] else "🟡"
lines.append(f"{marker} {i}. {started} `{s['conv_id']}` — `{s['cwd']}`")
sessions_md = "\n".join(lines)
else:
sessions_md = "_No active sessions_"
content = f"{sessions_md}\n\n**Mode:** {mode}"
return {
"config": {"wide_screen_mode": True},
"header": {
"title": {"tag": "plain_text", "content": "Claude Code Sessions"},
"template": "turquoise",
},
"elements": [
{"tag": "div", "text": {"tag": "lark_md", "content": content}},
],
}
async def send_file(receive_id: str, receive_id_type: str, file_path: str, file_type: str = "stream") -> None:
"""
Upload a local file to Feishu and send it as a file message.
@ -198,7 +181,7 @@ async def send_file(receive_id: str, receive_id_type: str, file_path: str, file_
import os
path = os.path.abspath(file_path)
file_name = os.path.basename(path)
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
# Step 1: Upload file → get file_key
with open(path, "rb") as f:
@ -259,27 +242,3 @@ async def send_file(receive_id: str, receive_id_type: str, file_path: str, file_
)
else:
logger.debug("Sent file %r to %s (%s)", file_name, receive_id, receive_id_type)
def build_session_card(conv_id: str, cwd: str, started: bool) -> dict:
"""Build a session status card."""
status = "🟢 Active" if started else "🟡 Ready"
content = f"**Session ID:** `{conv_id}`\n**Directory:** `{cwd}`\n**Status:** {status}"
return {
"config": {"wide_screen_mode": True},
"header": {
"title": {"tag": "plain_text", "content": "Claude Code Session"},
"template": "turquoise",
},
"elements": [
{"tag": "div", "text": {"tag": "lark_md", "content": content}},
{"tag": "hr"},
{
"tag": "action",
"actions": [
{"tag": "button", "text": {"tag": "plain_text", "content": "Continue"}, "type": "primary", "value": {"action": "continue", "conv_id": conv_id}},
{"tag": "button", "text": {"tag": "plain_text", "content": "Close"}, "type": "default", "value": {"action": "close", "conv_id": conv_id}},
],
},
],
}

View File

@ -130,7 +130,7 @@ class NodeClient:
sessions = manager.list_sessions()
active_sessions = [
{"conv_id": s["conv_id"], "working_dir": s["working_dir"]}
{"conv_id": s["conv_id"], "working_dir": s["cwd"]}
for s in sessions
]

View File

@ -91,7 +91,7 @@ async def startup_event() -> None:
await manager.start()
from agent.scheduler import scheduler
await scheduler.start()
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
start_websocket_client(loop)
logger.info("PhoneWork started")

View File

@ -301,7 +301,8 @@ class FileReadTool(BaseTool):
async def _arun(self, path: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> str:
try:
file_path = _resolve_dir(path)
p = Path(path.strip())
file_path = _resolve_dir(str(p.parent)) / p.name if not p.is_absolute() else p.resolve()
if not file_path.is_file():
return json.dumps({"error": f"Not a file: {path}"}, ensure_ascii=False)
@ -342,7 +343,8 @@ class FileWriteTool(BaseTool):
async def _arun(self, path: str, content: str, mode: Optional[str] = "overwrite") -> str:
try:
file_path = _resolve_dir(path)
p = Path(path.strip())
file_path = (_resolve_dir(str(p.parent)) / p.name) if not p.is_absolute() else p.resolve()
file_path.parent.mkdir(parents=True, exist_ok=True)
write_mode = "a" if mode == "append" else "w"
@ -484,7 +486,8 @@ class FileSendTool(BaseTool):
async def _arun(self, path: str) -> str:
try:
file_path = _resolve_dir(path)
p = Path(path.strip())
file_path = _resolve_dir(str(p.parent)) / p.name if not p.is_absolute() else p.resolve()
if not file_path.is_file():
return json.dumps({"error": f"Not a file: {path}"}, ensure_ascii=False)

View File

@ -43,6 +43,7 @@ def create_app(router_secret: Optional[str] = None) -> FastAPI:
@app.get("/health")
async def health():
from router.rpc import get_pending_count
nodes = registry.list_nodes()
online_nodes = [n for n in nodes if n["status"] == "online"]
return {
@ -50,7 +51,7 @@ def create_app(router_secret: Optional[str] = None) -> FastAPI:
"nodes": nodes,
"online_nodes": len(online_nodes),
"total_nodes": len(nodes),
"pending_requests": 0,
"pending_requests": get_pending_count(),
}
@app.get("/nodes")
@ -64,7 +65,7 @@ def create_app(router_secret: Optional[str] = None) -> FastAPI:
@app.on_event("startup")
async def startup():
import asyncio
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
start_websocket_client(loop)
logger.info("Router started")

View File

@ -52,7 +52,7 @@ async def forward(
raise RuntimeError(f"Node not connected: {node_id}")
request_id = str(uuid.uuid4())
future: asyncio.Future[str] = asyncio.get_event_loop().create_future()
future: asyncio.Future[str] = asyncio.get_running_loop().create_future()
_pending_requests[request_id] = future
request = ForwardRequest(