From 4a559c3cf9ae72eb1b5b36a540af5c66dc40e4f1 Mon Sep 17 00:00:00 2001 From: one-ea Date: Mon, 29 Jun 2026 20:27:27 +0000 Subject: [PATCH] feat(conversations): add /mine command and assignee filter on web - Add ConversationService.listByAssignee for quick lookup by admin - Extend listConversations with assignedTo filter parameter - Add /mine command in Topic: lists conversations assigned to caller - Add assigned_to filter input on /operations/conversations web page - Filter links preserve assigned_to when switching status tabs - Pagination preserves both status and assigned_to filters Co-authored-by: monkeycode-ai --- src/channels/telegram/commands.ts | 16 +++++++++++++++ src/domain/conversations.ts | 18 +++++++++++++++-- src/runtime/main.ts | 1 + src/runtime/web-console.ts | 33 ++++++++++++++++++++++--------- test/core.test.ts | 20 +++++++++++++++++++ 5 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/channels/telegram/commands.ts b/src/channels/telegram/commands.ts index 458bcb8..e9149cf 100644 --- a/src/channels/telegram/commands.ts +++ b/src/channels/telegram/commands.ts @@ -44,6 +44,7 @@ export function topicHelpText(): string { "会话处理:", "/priority low|normal|high|urgent - 设置优先级", "/assign - 分配负责人", + "/mine - 查看分配给自己的会话列表", "/close - 关闭会话;用户再发消息会自动重开", "/reopen 或 /open - 重新打开会话", "/mute <时长> - 静音提醒,例如 /mute 2h、/mute 1d", @@ -413,6 +414,21 @@ export async function handleTopicCommand( await ctx.reply("已对该会话关闭 AI 草稿。该会话不再自动生成回复草稿。"); return true; } + case "mine": { + const items = deps.conversations.listByAssignee(adminId, 20); + if (items.length === 0) { + await ctx.reply("当前没有分配给你的会话。使用 /assign <你的 user_id> 分配给自己。"); + return true; + } + const lines = items.map((c) => { + const name = c.contactDisplayName ?? c.topicName ?? `#${c.id}`; + const status = c.status === "open" ? "" : " [closed]"; + const last = c.lastMessageAt ? ` 最后:${c.lastMessageAt}` : ""; + return `#${c.id} ${name}${status}${last}`; + }); + await ctx.reply([`分配给你的会话 (${items.length}):`, ...lines].join("\n")); + return true; + } case "search": { if (!args) { await ctx.reply("用法:/search <关键词>,在当前会话历史消息中搜索。"); diff --git a/src/domain/conversations.ts b/src/domain/conversations.ts index 21faa6f..24657b9 100644 --- a/src/domain/conversations.ts +++ b/src/domain/conversations.ts @@ -567,11 +567,21 @@ export class ConversationService { listConversations(opts: { status?: "open" | "closed"; + assignedTo?: string; limit: number; offset: number; }): { items: ConversationListItem[]; total: number } { - const where = opts.status ? "WHERE c.status = ?" : ""; - const params = opts.status ? [opts.status] : []; + const conditions: string[] = []; + const params: Array = []; + if (opts.status) { + conditions.push("c.status = ?"); + params.push(opts.status); + } + if (opts.assignedTo !== undefined) { + conditions.push("c.assigned_admin_id = ?"); + params.push(opts.assignedTo); + } + const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; const rows = this.db .prepare( `SELECT c.id, c.status, c.priority, c.assigned_admin_id, c.created_at, c.last_message_at, @@ -590,6 +600,10 @@ export class ConversationService { return { items: rows.map(conversationListItemFromRow), total: countRow.cnt }; } + listByAssignee(adminId: string, limit: number): ConversationListItem[] { + return this.listConversations({ assignedTo: adminId, limit, offset: 0 }).items; + } + searchMessagesInConversation(conversationId: number, query: string, limit: number): Message[] { const pattern = `%${query.replace(/[%_]/g, (m) => "\\" + m)}%`; const rows = this.db diff --git a/src/runtime/main.ts b/src/runtime/main.ts index 7b460ab..3710f35 100644 --- a/src/runtime/main.ts +++ b/src/runtime/main.ts @@ -333,6 +333,7 @@ await startWebConsole({ ); const result = conversations.listConversations({ status: opts.status === "open" || opts.status === "closed" ? opts.status : undefined, + assignedTo: opts.assignedTo || undefined, limit: opts.pageSize, offset: (opts.page - 1) * opts.pageSize, }); diff --git a/src/runtime/web-console.ts b/src/runtime/web-console.ts index 175cde1..7cd4181 100644 --- a/src/runtime/web-console.ts +++ b/src/runtime/web-console.ts @@ -277,7 +277,7 @@ export interface WebConsoleOptions { dbHealthCheck: () => boolean; collectMetrics: () => MetricsSnapshot; collectOperationsOverview: () => OperationsOverview; - listConversations: (opts: { page: number; status?: string; pageSize: number }) => { items: ConversationListItemView[]; total: number }; + listConversations: (opts: { page: number; status?: string; assignedTo?: string; pageSize: number }) => { items: ConversationListItemView[]; total: number }; listFailedDeliveries: (opts: { page: number; pageSize: number }) => { items: DeliveryView[]; total: number }; scheduleRetry: (deliveryId: number) => Promise; listAuditLogs: (opts: { page: number; adminId?: string; action?: string; pageSize: number }) => { items: AuditLogView[]; total: number }; @@ -376,8 +376,9 @@ export async function startWebConsole(options: WebConsoleOptions): Promise { const label = s === "" ? "全部" : s === "open" ? "open" : "closed"; const active = (status ?? "") === s ? "current" : ""; - const qs = s ? `?status=${s}` : ""; - return `${label}`; + const params = new URLSearchParams(); + if (s) params.set("status", s); + if (assignedTo) params.set("assigned_to", assignedTo); + const qs = params.toString(); + return `${label}`; }) .join(""); + const assignedValue = assignedTo ? escapeHtml(assignedTo) : ""; + const buildQs = (p: number): string => { + const params = new URLSearchParams(); + if (status) params.set("status", status); + if (assignedTo) params.set("assigned_to", assignedTo); + if (p > 1) params.set("page", String(p)); + const qs = params.toString(); + return qs ? `?${qs}` : ""; + }; const rows = data.items.length ? data.items .map( @@ -945,14 +959,15 @@ function renderConversations( .join("") : `没有符合条件的会话。`; const pager = Array.from({ length: Math.min(totalPages, 10) }, (_, i) => i + 1) - .map( - (p) => - `${p}`, - ) + .map((p) => `${p}`) .join(""); const body = `

会话列表 (${data.total})

${filterLinks}
+
+

+ +
${rows} diff --git a/test/core.test.ts b/test/core.test.ts index fdb15eb..3c0094f 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -712,6 +712,26 @@ describe("conversation service", () => { const paged = service.listConversations({ limit: 2, offset: 0 }); assert.equal(paged.items.length, 2); }); + + it("lists conversations by assignee", async () => { + const service = new ConversationService(handle.db, 30); + const b1 = await service.getOrCreateConversation({ platform: "telegram", externalUserId: "600", displayName: "A" }); + const b2 = await service.getOrCreateConversation({ platform: "telegram", externalUserId: "601", displayName: "B" }); + const b3 = await service.getOrCreateConversation({ platform: "telegram", externalUserId: "602", displayName: "C" }); + service.assign(b1.conversation.id, "100"); + service.assign(b2.conversation.id, "100"); + service.assign(b3.conversation.id, "200"); + + const mine = service.listByAssignee("100", 20); + assert.equal(mine.length, 2); + assert.ok(mine.every((c) => c.assignedAdminId === "100")); + + const combined = service.listConversations({ assignedTo: "100", status: "open", limit: 50, offset: 0 }); + assert.equal(combined.total, 2); + + const empty = service.listByAssignee("999", 20); + assert.equal(empty.length, 0); + }); }); describe("permissions and rate limits", () => {
IDTopic联系人状态优先级负责人最后消息