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
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,11 +473,28 @@ Feishu WebSocket connected
#### 使用 PM2 守护进程

```bash
# 安装 PM2
npm install -g pm2

# 构建并启动
npm run build
pm2 start dist/index.js --name feishu-claude
pm2 start npm --name "feishu-claude" -- start

# 持久化:保存进程列表 + 开机自启
pm2 save
pm2 startup # 设置开机自启
pm2 startup # 按提示执行输出的命令
```

#### PM2 常用命令

```bash
pm2 status # 查看所有进程状态
pm2 logs feishu-claude # 查看实时日志 (Ctrl+C 退出)
pm2 logs feishu-claude --lines 50 # 查看最近 50 行日志
pm2 restart feishu-claude # 重启服务
pm2 stop feishu-claude # 停止服务
pm2 delete feishu-claude # 删除进程(从 PM2 列表移除)
pm2 monit # 终端监控面板(CPU/内存)
```

---
Expand Down
32 changes: 24 additions & 8 deletions src/feishu/event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ interface ParsedMessage {
chatId: string;
chatType: string;
mentionedBot: boolean;
rootId?: string;
}

/**
Expand All @@ -138,9 +139,9 @@ async function handleMessageEvent(data: MessageEventData): Promise<void> {
return;
}

const { text, messageId, userId, chatId, chatType, mentionedBot } = parsed;
const { text, messageId, userId, chatId, chatType, mentionedBot, rootId } = parsed;

logger.info({ userId, chatId, chatType, text: text.slice(0, 100) }, 'Received message');
logger.info({ userId, chatId, chatType, rootId, text: text.slice(0, 100) }, 'Received message');

// 群聊中需要 @机器人 才响应
if (chatType === 'group' && !mentionedBot) {
Expand All @@ -155,7 +156,7 @@ async function handleMessageEvent(data: MessageEventData): Promise<void> {
}

// 处理斜杠命令
const commandResult = await handleSlashCommand(text, chatId, userId, messageId);
const commandResult = await handleSlashCommand(text, chatId, userId, messageId, rootId);
if (commandResult) return;

// 安全检查
Expand All @@ -165,7 +166,7 @@ async function handleMessageEvent(data: MessageEventData): Promise<void> {
}

// 执行 Claude Agent
await executeClaudeTask(text, chatId, userId, messageId);
await executeClaudeTask(text, chatId, userId, messageId, rootId);
}

/**
Expand All @@ -176,12 +177,14 @@ async function handleSlashCommand(
chatId: string,
userId: string,
messageId: string,
rootId?: string,
): Promise<boolean> {
const trimmed = text.trim();

// 获取当前会话的话题锚点消息 ID(用于将命令响应发到话题内)
// 如果用户在话题内发消息,优先使用该话题的 rootId
const currentSession = sessionManager.get(chatId, userId);
const threadRootMsgId = currentSession?.threadRootMessageId;
const threadRootMsgId = rootId || currentSession?.threadRootMessageId;

// /project <path> - 切换工作目录
if (trimmed.startsWith('/project ')) {
Expand Down Expand Up @@ -276,17 +279,28 @@ async function ensureThread(
chatId: string,
userId: string,
messageId: string,
rootId?: string,
): Promise<string | undefined> {
const session = sessionManager.getOrCreate(chatId, userId);

// 1. 用户在已有话题内发消息 — 直接复用该话题,无需发送问候
if (rootId) {
// 更新 session 的话题信息,确保后续回复也发到这个话题
sessionManager.setThread(chatId, userId, rootId, rootId);
return rootId;
}

// 2. session 已有话题信息(用户在话题外发消息,但之前的话题仍在)
if (session.threadId && session.threadRootMessageId) {
return session.threadRootMessageId;
}

// 首次交互:reply_in_thread 创建话题
// 3. 全新场景 — 创建话题,根据是否有 conversationId 判断是新建还是恢复
const isResumed = !!session.conversationId;
const greeting = isResumed ? '🤖 会话已恢复' : '🤖 新会话已创建';
const { messageId: botMsgId, threadId } = await feishuClient.replyInThread(
messageId,
'🤖 新会话已创建',
greeting,
);

if (threadId && botMsgId) {
Expand All @@ -307,12 +321,13 @@ async function executeClaudeTask(
chatId: string,
userId: string,
messageId: string,
rootId?: string,
): Promise<void> {
const session = sessionManager.getOrCreate(chatId, userId);
const sessionKey = `${chatId}:${userId}`;

// 确保话题存在,返回话题锚点消息 ID
const threadRootMsgId = await ensureThread(chatId, userId, messageId);
const threadRootMsgId = await ensureThread(chatId, userId, messageId, rootId);

// 发送 "处理中" 卡片(优先发到话题内)
let progressMsgId: string | undefined;
Expand Down Expand Up @@ -425,6 +440,7 @@ function parseMessage(data: MessageEventData): ParsedMessage | null {
chatId: message.chat_id,
chatType: message.chat_type,
mentionedBot,
rootId: message.root_id || undefined,
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/session/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ export class SessionManager {
}

/**
* 清理过期会话 (超过 2 小时不活跃)
* 清理过期会话 (超过 24 小时不活跃)
*/
cleanup(maxIdleMs: number = 2 * 60 * 60 * 1000): number {
cleanup(maxIdleMs: number = 24 * 60 * 60 * 1000): number {
const cleaned = this.db.deleteExpired(maxIdleMs);
if (cleaned > 0) {
logger.info({ cleaned }, 'Cleaned up idle sessions');
Expand Down