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:
parent
09b63341cd
commit
a3622ce26d
@ -13,7 +13,7 @@ from agent.manager import manager
|
|||||||
from agent.scheduler import scheduler
|
from agent.scheduler import scheduler
|
||||||
from agent.task_runner import task_runner
|
from agent.task_runner import task_runner
|
||||||
from orchestrator.agent import agent
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -111,6 +111,18 @@ async def _cmd_new(user_id: str, args: str) -> str:
|
|||||||
conv_id = data.get("conv_id", "")
|
conv_id = data.get("conv_id", "")
|
||||||
agent._active_conv[user_id] = conv_id
|
agent._active_conv[user_id] = conv_id
|
||||||
cwd = data.get("working_dir", working_dir)
|
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}`"
|
reply = f"✓ Created session `{conv_id}` in `{cwd}`"
|
||||||
if parsed.timeout:
|
if parsed.timeout:
|
||||||
reply += f" (timeout: {parsed.timeout}s)"
|
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:
|
async def _cmd_status(user_id: str) -> str:
|
||||||
"""Show status: sessions and current mode."""
|
"""Show status: sessions and current mode."""
|
||||||
sessions = manager.list_sessions(user_id=user_id)
|
sessions = manager.list_sessions(user_id=user_id)
|
||||||
if not sessions:
|
|
||||||
return "No active sessions."
|
|
||||||
|
|
||||||
active = agent.get_active_conv(user_id)
|
active = agent.get_active_conv(user_id)
|
||||||
passthrough = agent.get_passthrough(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"]
|
lines = ["**Your Sessions:**\n"]
|
||||||
for i, s in enumerate(sessions, 1):
|
for i, s in enumerate(sessions, 1):
|
||||||
marker = "→ " if s["conv_id"] == active else " "
|
marker = "→ " if s["conv_id"] == active else " "
|
||||||
lines.append(f"{marker}{i}. `{s['conv_id']}` - `{s['cwd']}`")
|
lines.append(f"{marker}{i}. `{s['conv_id']}` - `{s['cwd']}`")
|
||||||
status = "Direct 🟢" if passthrough else "Smart ⚪"
|
lines.append(f"\n**Mode:** {mode}")
|
||||||
lines.append(f"\n**Mode:** {status}")
|
|
||||||
lines.append("Use `/switch <n>` to activate a session.")
|
lines.append("Use `/switch <n>` to activate a session.")
|
||||||
lines.append("Use `/direct` or `/smart` to change mode.")
|
lines.append("Use `/direct` or `/smart` to change mode.")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
109
bot/feishu.py
109
bot/feishu.py
@ -67,7 +67,7 @@ async def send_text(receive_id: str, receive_id_type: str, text: str) -> None:
|
|||||||
text: message content.
|
text: message content.
|
||||||
"""
|
"""
|
||||||
parts = _split_message(text)
|
parts = _split_message(text)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
for i, part in enumerate(parts):
|
for i, part in enumerate(parts):
|
||||||
logger.debug(
|
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)
|
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.
|
Send an interactive card message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
receive_id: chat_id or open_id
|
receive_id: chat_id or open_id depending on receive_id_type.
|
||||||
receive_id_type: "chat_id" | "open_id" | "user_id" | "union_id"
|
receive_id_type: "chat_id" | "open_id" | "user_id" | "union_id".
|
||||||
title: Card title
|
card: Card content dict (Feishu card JSON schema).
|
||||||
content: Card content (markdown supported)
|
|
||||||
buttons: List of button dicts with "text" and "value" keys
|
|
||||||
"""
|
"""
|
||||||
elements = [
|
loop = asyncio.get_running_loop()
|
||||||
{
|
|
||||||
"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()
|
|
||||||
request = (
|
request = (
|
||||||
CreateMessageRequest.builder()
|
CreateMessageRequest.builder()
|
||||||
.receive_id_type(receive_id_type)
|
.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()
|
CreateMessageRequestBody.builder()
|
||||||
.receive_id(receive_id)
|
.receive_id(receive_id)
|
||||||
.msg_type("interactive")
|
.msg_type("interactive")
|
||||||
.content(json.dumps(card_content, ensure_ascii=False))
|
.content(json.dumps(card, ensure_ascii=False))
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.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)
|
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:
|
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.
|
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
|
import os
|
||||||
path = os.path.abspath(file_path)
|
path = os.path.abspath(file_path)
|
||||||
file_name = os.path.basename(path)
|
file_name = os.path.basename(path)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
# Step 1: Upload file → get file_key
|
# Step 1: Upload file → get file_key
|
||||||
with open(path, "rb") as f:
|
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:
|
else:
|
||||||
logger.debug("Sent file %r to %s (%s)", file_name, receive_id, receive_id_type)
|
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}},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|||||||
@ -130,7 +130,7 @@ class NodeClient:
|
|||||||
|
|
||||||
sessions = manager.list_sessions()
|
sessions = manager.list_sessions()
|
||||||
active_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
|
for s in sessions
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
2
main.py
2
main.py
@ -91,7 +91,7 @@ async def startup_event() -> None:
|
|||||||
await manager.start()
|
await manager.start()
|
||||||
from agent.scheduler import scheduler
|
from agent.scheduler import scheduler
|
||||||
await scheduler.start()
|
await scheduler.start()
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
start_websocket_client(loop)
|
start_websocket_client(loop)
|
||||||
logger.info("PhoneWork started")
|
logger.info("PhoneWork started")
|
||||||
|
|
||||||
|
|||||||
@ -301,7 +301,8 @@ class FileReadTool(BaseTool):
|
|||||||
|
|
||||||
async def _arun(self, path: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> str:
|
async def _arun(self, path: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> str:
|
||||||
try:
|
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():
|
if not file_path.is_file():
|
||||||
return json.dumps({"error": f"Not a file: {path}"}, ensure_ascii=False)
|
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:
|
async def _arun(self, path: str, content: str, mode: Optional[str] = "overwrite") -> str:
|
||||||
try:
|
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)
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
write_mode = "a" if mode == "append" else "w"
|
write_mode = "a" if mode == "append" else "w"
|
||||||
@ -484,7 +486,8 @@ class FileSendTool(BaseTool):
|
|||||||
|
|
||||||
async def _arun(self, path: str) -> str:
|
async def _arun(self, path: str) -> str:
|
||||||
try:
|
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():
|
if not file_path.is_file():
|
||||||
return json.dumps({"error": f"Not a file: {path}"}, ensure_ascii=False)
|
return json.dumps({"error": f"Not a file: {path}"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,7 @@ def create_app(router_secret: Optional[str] = None) -> FastAPI:
|
|||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health():
|
async def health():
|
||||||
|
from router.rpc import get_pending_count
|
||||||
nodes = registry.list_nodes()
|
nodes = registry.list_nodes()
|
||||||
online_nodes = [n for n in nodes if n["status"] == "online"]
|
online_nodes = [n for n in nodes if n["status"] == "online"]
|
||||||
return {
|
return {
|
||||||
@ -50,7 +51,7 @@ def create_app(router_secret: Optional[str] = None) -> FastAPI:
|
|||||||
"nodes": nodes,
|
"nodes": nodes,
|
||||||
"online_nodes": len(online_nodes),
|
"online_nodes": len(online_nodes),
|
||||||
"total_nodes": len(nodes),
|
"total_nodes": len(nodes),
|
||||||
"pending_requests": 0,
|
"pending_requests": get_pending_count(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/nodes")
|
@app.get("/nodes")
|
||||||
@ -64,7 +65,7 @@ def create_app(router_secret: Optional[str] = None) -> FastAPI:
|
|||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup():
|
async def startup():
|
||||||
import asyncio
|
import asyncio
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
start_websocket_client(loop)
|
start_websocket_client(loop)
|
||||||
logger.info("Router started")
|
logger.info("Router started")
|
||||||
|
|
||||||
|
|||||||
@ -52,7 +52,7 @@ async def forward(
|
|||||||
raise RuntimeError(f"Node not connected: {node_id}")
|
raise RuntimeError(f"Node not connected: {node_id}")
|
||||||
|
|
||||||
request_id = str(uuid.uuid4())
|
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
|
_pending_requests[request_id] = future
|
||||||
|
|
||||||
request = ForwardRequest(
|
request = ForwardRequest(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user