From 41fc58d73a9fa1604935d570e2e09fd125932354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Thu, 9 Apr 2026 21:25:23 +0800 Subject: [PATCH] feat(adapters): add /new quick switch and unify project selection - Add matchProject() to http-client for fuzzy project matching by index or name - Extract startNewSession() in both adapters to share reset + match + create logic - Project selection replies now go through startNewSession(), creating a clean new session instead of continuing the old one - Support: /new 2 (by index), /new backend (by name), /new (default) - Add usage hint in /projects list output Co-Authored-By: Claude Opus 4.6 (1M context) --- adapters/common/http-client.ts | 30 +++++++++++++ adapters/feishu/index.ts | 80 ++++++++++++++++++++-------------- adapters/telegram/index.ts | 73 +++++++++++++++++++------------ 3 files changed, 123 insertions(+), 60 deletions(-) diff --git a/adapters/common/http-client.ts b/adapters/common/http-client.ts index 18d233c1..f34e2266 100644 --- a/adapters/common/http-client.ts +++ b/adapters/common/http-client.ts @@ -41,4 +41,34 @@ export class AdapterHttpClient { const data = (await res.json()) as { projects: RecentProject[] } return data.projects } + + /** + * Match a project by index (1-based) or fuzzy name from recent projects. + * Returns { project, ambiguous[] } — ambiguous is set when multiple projects match. + */ + async matchProject(query: string): Promise<{ project?: RecentProject; ambiguous?: RecentProject[] }> { + const projects = await this.listRecentProjects() + + // Try as 1-based index + const num = parseInt(query, 10) + if (!isNaN(num) && num >= 1 && num <= projects.length && String(num) === query.trim()) { + return { project: projects[num - 1] } + } + + const q = query.toLowerCase() + + // Exact project name match + const exact = projects.find(p => p.projectName.toLowerCase() === q) + if (exact) return { project: exact } + + // Fuzzy: name or path contains query + const matches = projects.filter(p => + p.projectName.toLowerCase().includes(q) || + p.realPath.toLowerCase().includes(q) + ) + if (matches.length === 1) return { project: matches[0] } + if (matches.length > 1) return { ambiguous: matches } + + return {} + } } diff --git a/adapters/feishu/index.ts b/adapters/feishu/index.ts index a688a8b3..bd8152ef 100644 --- a/adapters/feishu/index.ts +++ b/adapters/feishu/index.ts @@ -252,12 +252,54 @@ async function showProjectPicker(chatId: string): Promise { `${i + 1}. **${p.projectName}**${p.branch ? ` (${p.branch})` : ''}\n ${p.realPath}` ) pendingProjectSelection.set(chatId, true) - await sendText(chatId, `选择项目(回复编号):\n\n${lines.join('\n\n')}`) + await sendText(chatId, `选择项目(回复编号):\n\n${lines.join('\n\n')}\n\n💡 下次可直接 /new <编号或名称> 快速新建会话`) } catch (err) { await sendText(chatId, `❌ 无法获取项目列表: ${err instanceof Error ? err.message : String(err)}`) } } +async function startNewSession(chatId: string, query?: string): Promise { + bridge.resetSession(chatId) + sessionStore.delete(chatId) + chatStates.delete(chatId) + accumulatedText.delete(chatId) + buffers.get(chatId)?.reset() + buffers.delete(chatId) + pendingProjectSelection.delete(chatId) + + if (query) { + try { + const { project, ambiguous } = await httpClient.matchProject(query) + if (project) { + const ok = await createSessionForChat(chatId, project.realPath) + if (ok) { + await sendText(chatId, + `✅ 已新建会话:**${project.projectName}**${project.branch ? ` (${project.branch})` : ''}`) + } + return + } + if (ambiguous) { + const list = ambiguous.map((p, i) => `${i + 1}. **${p.projectName}** — ${p.realPath}`).join('\n') + await sendText(chatId, `匹配到多个项目,请更精确:\n\n${list}`) + return + } + await sendText(chatId, `未找到匹配 "${query}" 的项目。发送 /projects 查看完整列表。`) + } catch (err) { + await sendText(chatId, `❌ ${err instanceof Error ? err.message : String(err)}`) + } + } else { + const workDir = config.defaultProjectDir + if (workDir) { + const ok = await createSessionForChat(chatId, workDir) + if (ok) { + await sendText(chatId, '✅ 已新建会话,可以开始对话了。') + } + } else { + await showProjectPicker(chatId) + } + } +} + // ---------- server message handler ---------- async function handleServerMessage(chatId: string, msg: ServerMessage): Promise { @@ -436,24 +478,9 @@ async function handleMessage(data: any): Promise { if (!text) return // Handle commands - if (text === '/new' || text === '新会话') { - bridge.resetSession(chatId) - sessionStore.delete(chatId) - chatStates.delete(chatId) - accumulatedText.delete(chatId) - buffers.get(chatId)?.reset() - buffers.delete(chatId) - pendingProjectSelection.delete(chatId) - - const workDir = config.defaultProjectDir - if (workDir) { - const ok = await createSessionForChat(chatId, workDir) - if (ok) { - await sendText(chatId, '✅ 已新建会话,可以开始对话了。') - } - } else { - await showProjectPicker(chatId) - } + if (text === '/new' || text === '新会话' || text.startsWith('/new ')) { + const arg = text.startsWith('/new ') ? text.slice(5).trim() : '' + await startNewSession(chatId, arg || undefined) return } if (text === '/stop' || text === '停止') { @@ -468,20 +495,7 @@ async function handleMessage(data: any): Promise { // Check if user is responding to project selection if (pendingProjectSelection.has(chatId)) { - const num = parseInt(text, 10) - if (num >= 1) { - try { - const projects = await httpClient.listRecentProjects() - const selected = projects[num - 1] - if (selected) { - pendingProjectSelection.delete(chatId) - await createSessionForChat(chatId, selected.realPath) - await sendText(chatId, `✅ 已选择 **${selected.projectName}**。现在可以开始对话了。`) - return - } - } catch { /* fall through */ } - } - await sendText(chatId, '请输入有效的编号。') + await startNewSession(chatId, text.trim()) return } diff --git a/adapters/telegram/index.ts b/adapters/telegram/index.ts index b6fc8c11..cc6cc606 100644 --- a/adapters/telegram/index.ts +++ b/adapters/telegram/index.ts @@ -147,7 +147,7 @@ async function showProjectPicker(chatId: string): Promise { ) pendingProjectSelection.set(chatId, true) await bot.api.sendMessage(numericChatId, - `选择项目(回复编号):\n\n${lines.join('\n\n')}`) + `选择项目(回复编号):\n\n${lines.join('\n\n')}\n\n💡 下次可直接 /new <编号或名称> 快速新建会话`) } catch (err) { await bot.api.sendMessage(numericChatId, `❌ 无法获取项目列表: ${err instanceof Error ? err.message : String(err)}`) @@ -266,14 +266,19 @@ bot.command('start', (ctx) => { ctx.reply( '👋 Claude Code Bot 已就绪。\n\n' + '命令:\n' + - '/projects — 选择/切换项目\n' + - '/new — 新建会话\n' + + '/new — 新建会话(使用默认项目)\n' + + '/new <名称或编号> — 新建会话并指定项目\n' + + '/projects — 查看项目列表\n' + '/stop — 停止生成' ) }) -bot.command('new', async (ctx) => { - const chatId = String(ctx.chat.id) +/** Reset session state and start a new session for chatId. + * If `query` is provided, match a project by index or name; + * otherwise use defaultProjectDir or show the picker. */ +async function startNewSession(chatId: string, query?: string): Promise { + const numericChatId = Number(chatId) + bridge.resetSession(chatId) sessionStore.delete(chatId) placeholders.delete(chatId) @@ -282,15 +287,43 @@ bot.command('new', async (ctx) => { buffers.delete(chatId) pendingProjectSelection.delete(chatId) - const workDir = config.defaultProjectDir - if (workDir) { - const ok = await createSessionForChat(chatId, workDir) - if (ok) { - await bot.api.sendMessage(Number(chatId), '✅ 已新建会话,可以开始对话了。') + if (query) { + try { + const { project, ambiguous } = await httpClient.matchProject(query) + if (project) { + const ok = await createSessionForChat(chatId, project.realPath) + if (ok) { + await bot.api.sendMessage(numericChatId, + `✅ 已新建会话:${project.projectName}${project.branch ? ` (${project.branch})` : ''}`) + } + return + } + if (ambiguous) { + const list = ambiguous.map((p, i) => `${i + 1}. ${p.projectName} — ${p.realPath}`).join('\n') + await bot.api.sendMessage(numericChatId, `匹配到多个项目,请更精确:\n\n${list}`) + return + } + await bot.api.sendMessage(numericChatId, `未找到匹配 "${query}" 的项目。发送 /projects 查看完整列表。`) + } catch (err) { + await bot.api.sendMessage(numericChatId, + `❌ ${err instanceof Error ? err.message : String(err)}`) } } else { - await showProjectPicker(chatId) + const workDir = config.defaultProjectDir + if (workDir) { + const ok = await createSessionForChat(chatId, workDir) + if (ok) { + await bot.api.sendMessage(numericChatId, '✅ 已新建会话,可以开始对话了。') + } + } else { + await showProjectPicker(chatId) + } } +} + +bot.command('new', async (ctx) => { + const chatId = String(ctx.chat.id) + await startNewSession(chatId, ctx.match?.trim() || undefined) }) bot.command('projects', async (ctx) => { @@ -330,23 +363,9 @@ bot.on('message:text', (ctx) => { } enqueue(chatId, async () => { - // Check if user is responding to project selection + // Project selection pending — treat input as /new if (pendingProjectSelection.has(chatId)) { - const num = parseInt(text, 10) - if (num >= 1) { - try { - const projects = await httpClient.listRecentProjects() - const selected = projects[num - 1] - if (selected) { - pendingProjectSelection.delete(chatId) - await createSessionForChat(chatId, selected.realPath) - await bot.api.sendMessage(Number(chatId), - `✅ 已选择 ${selected.projectName}。现在可以开始对话了。`) - return - } - } catch { /* fall through */ } - } - await bot.api.sendMessage(Number(chatId), '请输入有效的编号。') + await startNewSession(chatId, text.trim()) return }