Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 51 additions & 6 deletions bot/vikingbot/channels/feishu.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
GetMessageResourceRequest,
P2ImMessageReceiveV1,
ReplyMessageRequest,
ReplyMessageRequestBody,
ReplyMessageRequestBody
)

FEISHU_AVAILABLE = True
Expand Down Expand Up @@ -172,8 +172,9 @@ async def _download_feishu_image(self, image_key: str, message_id: str | None =

# Handle failed response
if not response.success():
raw_detail = getattr(getattr(response, 'raw', None), 'content', response.msg)
raise Exception(
f"Failed to download image: code={response.code}, msg={response.msg}, log_id={response.get_log_id()}"
f"Failed to download image: code={response.code}, msg={raw_detail}, log_id={response.get_log_id()}"
)

# Read the image bytes from the response file
Expand Down Expand Up @@ -658,10 +659,7 @@ async def _on_message(self, data: "P2ImMessageReceiveV1") -> None:
chat_type = message.chat_type # "p2p" or "group"
msg_type = message.message_type

# Add reaction to indicate "seen"
await self._add_reaction(message_id, "MeMeMe")

# Parse message content and media
# Parse message content and media first to check mentions
content = ""
media = []

Expand Down Expand Up @@ -746,7 +744,54 @@ async def _on_message(self, data: "P2ImMessageReceiveV1") -> None:

import re

# 检查是否@了机器人
is_mentioned = False
mention_pattern = re.compile(r"@_user_\d+")
bot_open_id = self.config.open_id
bot_app_id = self.config.app_id

# 优先从message的mentions字段提取@信息(text和post类型都适用)
if hasattr(message, 'mentions') and message.mentions and bot_open_id:
for mention in message.mentions:
if hasattr(mention, 'id') and hasattr(mention.id, 'open_id'):
at_id = mention.id.open_id
if at_id == bot_open_id:
is_mentioned = True
break
continue
# 兼容其他可能的ID格式
at_id = getattr(mention, 'id', '') or getattr(mention, 'user_id', '')
if at_id == f"app_{bot_app_id}" or at_id == bot_app_id:
is_mentioned = True
break

# 话题群@检查逻辑
should_process = True
if chat_type == "group":
chat_mode = await self._get_chat_mode(chat_id)
if chat_mode == "thread":
# 判断是否是话题的首条消息(root_id等于message_id说明是话题发起消息)
is_topic_starter = message.root_id == message.message_id or not message.root_id

if self.config.thread_require_mention:
# 模式1:默认True,所有消息都需要@才处理
if not is_mentioned:
logger.info(f"Skipping thread message: thread_require_mention is True and not mentioned")
should_process = False
else:
# 模式2:False,仅话题首条消息不需要@,后续回复需要@
if not is_topic_starter and not is_mentioned:
logger.info(f"Skipping thread message: not topic starter and not mentioned")
should_process = False

# 不需要处理的消息直接跳过
if not should_process:
return

# 确认需要处理后再添加"已读"表情
await self._add_reaction(message_id, "MeMeMe")

# 替换所有@占位符
content = mention_pattern.sub(f"@{sender_id}", content)

# Forward to message bus
Expand Down
33 changes: 22 additions & 11 deletions bot/vikingbot/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ def gateway(
True, "--agent/--no-agent", help="Enable agent loop for OpenAPI/chat"
),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
config_path: str = typer.Option(None, "--config", "-c", help="ov.conf path"),
):
"""Start the vikingbot gateway with OpenAPI chat enabled by default."""

Expand All @@ -243,7 +244,8 @@ def gateway(
logging.basicConfig(level=logging.DEBUG)

bus = MessageBus()
config = ensure_config()
path = Path(config_path).expanduser() if config_path is not None else None
config = ensure_config(path)
_init_bot_data(config)
session_manager = SessionManager(config.bot_data_path)

Expand Down Expand Up @@ -557,18 +559,9 @@ def chat(
),
config_path: str = typer.Option(
None, "--config", "-c", help="Path to ov.conf, default .openviking/ov.conf"
)
),
):
"""Interact with the agent directly."""
if message is not None:
# Single-turn mode: only show error logs
logger.remove()
logger.add(sys.stderr, level="ERROR")
elif logs:
logger.enable("vikingbot")
else:
logger.disable("vikingbot")

path = Path(config_path).expanduser() if config_path is not None else None

bus = MessageBus()
Expand All @@ -586,6 +579,24 @@ def chat(
config, bus, session_manager, cron, quiet=is_single_turn, eval=eval
)

logger.remove()

log_file = get_data_dir() / f"vikingbot.debug.{os.getpid()}.log"
logger.add(
log_file,
level="DEBUG",
rotation="10 MB",
retention="7 days",
encoding="utf-8",
backtrace=True,
diagnose=True,
)

if logs:
logger.add(sys.stderr, level="DEBUG")
else:
logger.add(sys.stderr, level="ERROR")

async def run():
if is_single_turn:
# Single-turn mode: run channels and agent, exit after response
Expand Down
2 changes: 1 addition & 1 deletion bot/vikingbot/config/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def _merge_ov_server_config(bot_data: dict, ov_data: dict) -> None:
bot_data["server_url"] = f"http://{host}:{port}"
if "root_api_key" not in bot_data or not bot_data["root_api_key"]:
bot_data["root_api_key"] = ov_data.get("root_api_key", "")
if "root_api_key" in ov_data and ov_data["root_api_key"]:
if "root_api_key" in bot_data and bot_data["root_api_key"]:
bot_data["mode"] = "remote"
else:
bot_data["mode"] = "local"
Expand Down
4 changes: 4 additions & 0 deletions bot/vikingbot/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ class SandboxMode(str, Enum):

PER_SESSION = "per-session"
SHARED = "shared"
PER_CHANNEL = "per-channel"

class AgentMemoryMode(str, Enum):
"""Agent memory mode enumeration."""
PER_SESSION = "per-session"
SHARED = "shared"
PER_CHANNEL = "per-channel"


class BaseChannelConfig(BaseModel):
Expand Down Expand Up @@ -102,10 +104,12 @@ class FeishuChannelConfig(BaseChannelConfig):

type: ChannelType = ChannelType.FEISHU
app_id: str = ""
open_id: str = ""
app_secret: str = ""
encrypt_key: str = ""
verification_token: str = ""
allow_from: list[str] = Field(default_factory=list) ## 允许更新Agent对话的Feishu用户ID列表
thread_require_mention: bool = Field(default=True, description="话题群模式下是否需要@才响应:默认True=所有消息必须@才响应;False=新话题首条消息无需@,后续回复必须@")

def channel_id(self) -> str:
# Use app_id directly as the ID
Expand Down
4 changes: 3 additions & 1 deletion bot/vikingbot/sandbox/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ def get_workspace_path(self, session_key: SessionKey) -> Path:
def to_workspace_id(self, session_key: SessionKey):
if self.config.sandbox.mode == "shared":
return "shared"
else:
elif self.config.sandbox.mode == "per-channel":
return session_key.channel_key()
else: # per-session
return session_key.safe_name()

async def get_sandbox_cwd(self, session_key: SessionKey) -> str:
Expand Down
Loading