Skip to content

[Feature] delete conversations by sequence range#894

Open
dingyi222666 wants to merge 1 commit into
v1-devfrom
fix/delete-batch-889
Open

[Feature] delete conversations by sequence range#894
dingyi222666 wants to merge 1 commit into
v1-devfrom
fix/delete-batch-889

Conversation

@dingyi222666
Copy link
Copy Markdown
Member

This pr adds batch sequence selectors to the chatluna.delete command.

New Features

  • Support deleting conversations by sequence ranges like 1..3.
  • Support deleting conversations by sequence lists like 1,3,5.

Bug fixes

  • Return the existing target-not-found message when any requested sequence is missing.

Other Changes

  • Add regression coverage for range and list deletion selectors.
  • Validation: yarn lint-fix completed with no errors. Existing max-len warnings remain in read_chat_message.ts and extension-agent/src/sub-agent/builtin.ts.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

Review Change Stack

通读

此 PR 在 ChatLuna 对话管理系统中实现会话批量删除功能。用户现可通过范围格式(如 1..2)或列表格式(如 1,3)在 chatluna.delete 命令中批量删除多个会话,系统自动解析选择器并按序列号匹配会话执行删除。

变更

会话批量删除功能

Layer / File(s) Summary
序列解析与类型定义
packages/core/src/commands/conversation.ts
新增 parseSeqs 函数支持解析逗号/范围格式(如 1..21,3)的序列字符串,并在 chatluna.delete 命令参数处理中集成该解析器;扩展 ChainMiddlewareContextOptions.conversation_manage 新增 targetConversationSeqs?: number[] 字段承载批量删除目标。
批量删除中间件实现
packages/core/src/middlewares/system/conversation_manage.ts
conversation_delete 中间件新增分支处理,当 targetConversationSeqs 存在时调用 deleteBySeqs 函数;该函数列出可管理会话并按 displaySeq 匹配目标,验证数量一致后逐个执行删除,返回包含被删除会话标题列表的成功消息或"目标未找到"提示。
测试覆盖
packages/core/tests/conversation-resolve.spec.ts
新增测试用例 chatluna.delete removes range and list selectors,验证范围选择器 1..2 和列表选择器 1,3 的正确处理,断言删除调用收到的会话 ID 顺序及回传的成功提示消息内容。

🎯 2 (Simple) | ⏱️ ~12 分钟

🐰 序列化的删除,如诗一般展开,
逗号与范围在命令中舞蹈,
批量会话飘然而至,
测试覆盖每一个选择,
对话的列表,整洁如初~

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题清晰准确地概括了主要变更内容——为删除对话命令添加序列范围选择器功能。
Description check ✅ Passed 描述详细说明了新增功能、错误修复及其他改动,与变更集完全相关且信息充分。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/delete-batch-889

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the ability to delete conversations by specifying ranges or lists of sequences (e.g., '1..2' or '1,3'). It adds sequence parsing logic, updates the conversation management middleware to handle batch deletions, and includes corresponding unit tests. The review feedback highlights two important improvements: first, a critical security concern regarding a potential Denial of Service (DoS) vulnerability in parseSeqs when handling extremely large ranges, which should be mitigated by enforcing a maximum sequence limit; second, a performance optimization in deleteBySeqs to use a Set for sequence lookups, reducing the filtering complexity from O(N * M) to O(N + M).

Comment thread packages/core/src/commands/conversation.ts
Comment thread packages/core/src/middlewares/system/conversation_manage.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d3f6bbd40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +430 to +435
for (
let seq = Math.min(start, end);
seq <= Math.max(start, end);
seq += 1
) {
seqs.add(seq)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound sequence ranges before expansion

When a user runs a selector with a very large range, e.g. chatluna.delete 1..1000000000, this loop expands every integer into a Set before the middleware ever checks which conversations exist. Because chatluna.delete is available at authority 1, a short command can block the bot's event loop and/or exhaust memory; validate the range size or defer expansion until after listing the available entries.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/core/src/commands/conversation.ts (1)

243-244: ⚖️ Poor tradeoff

建议内联 parseSeqs 函数

根据代码规范:"a function is justified only when called from multiple distinct call sites with non-trivial logic"。parseSeqs 仅在 244 行被调用一次,建议直接内联到调用处以减少不必要的抽象层级。

♻️ 内联后的建议实现
         const presetLane = options.preset?.trim() || undefined
-        const target = conversation?.trim() || undefined
-        const seqs = target == null ? undefined : parseSeqs(target)
+        const input = conversation?.trim() || undefined
+        let seqs: number[] | undefined
+        if (
+            input != null &&
+            (input.includes(',') || input.includes('..')) &&
+            /^\d+(?:\.\.\d+)?(?:,\d+(?:\.\.\d+)?)*$/.test(input)
+        ) {
+            const set = new Set<number>()
+            for (const part of input.split(',')) {
+                const [start, end = start] = part.split('..').map(Number)
+                for (
+                    let seq = Math.min(start, end);
+                    seq <= Math.max(start, end);
+                    seq += 1
+                ) {
+                    set.add(seq)
+                }
+            }
+            seqs = [...set]
+        }
         const includeArchived =
             options.archived === true || options.all === true
         await chain.receiveCommand(
             session,
             'conversation_delete',
             {
                 allPresetLanes: presetLane == null,
                 conversation_manage: {
-                    targetConversation: seqs == null ? target : undefined,
+                    targetConversation: seqs == null ? input : undefined,
                     targetConversationSeqs: seqs,
                     presetLane,
                     includeArchived
                 }
             },
             ctx
         )
     })

-function parseSeqs(input: string) {
-    if (!input.includes(',') && !input.includes('..')) return undefined
-    if (!/^\d+(?:\.\.\d+)?(?:,\d+(?:\.\.\d+)?)*$/.test(input)) {
-        return undefined
-    }
-
-    const seqs = new Set<number>()
-    for (const part of input.split(',')) {
-        const [start, end = start] = part.split('..').map(Number)
-        for (
-            let seq = Math.min(start, end);
-            seq <= Math.max(start, end);
-            seq += 1
-        ) {
-            seqs.add(seq)
-        }
-    }
-
-    return [...seqs]
-}
-
 declare module '../chains/chain' {

根据代码规范:"Prefer inline code over extracting into functions; a function is justified only when called from multiple distinct call sites with non-trivial logic"。

Also applies to: 421-440

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/commands/conversation.ts` around lines 243 - 244, The
helper function parseSeqs should be inlined where it's only used: replace the
call sites that assign seqs (e.g., const target = conversation?.trim() ||
undefined; const seqs = target == null ? undefined : parseSeqs(target)) by
moving parseSeqs's logic directly into that expression so seqs is computed
without an extra function; do the same for the other occurrence around the
421–440 block—remove the parseSeqs declaration and update callers (reference
symbols: parseSeqs, target, seqs) so there is no separate function call and
behavior remains identical.
packages/core/src/middlewares/system/conversation_manage.ts (1)

283-293: ⚖️ Poor tradeoff

建议内联 deleteBySeqs 函数

deleteBySeqs 函数仅在 287 行被调用一次,根据代码规范应该内联到调用处。

♻️ 内联后的建议实现
     middleware('conversation_delete', async (session, context) => {
         try {
             const { presetLane, includeArchived, allPresetLanes } =
                 getManageOptions(context)
             const seqs =
                 context.options.conversation_manage?.targetConversationSeqs

             if (seqs != null) {
-                context.message = await deleteBySeqs(ctx, session, seqs, {
-                    presetLane,
-                    includeArchived,
-                    allPresetLanes
-                })
+                const entries = await ctx.chatluna.conversation.listConversationEntries(
+                    session,
+                    { presetLane, includeArchived, allPresetLanes }
+                )
+                const targets = entries.filter((item) => seqs.includes(item.displaySeq))
+                if (targets.length !== seqs.length) {
+                    context.message = session.text('chatluna.conversation.messages.target_not_found')
+                } else {
+                    const deleted: ConversationRecord[] = []
+                    for (const target of targets) {
+                        deleted.push(
+                            await ctx.chatluna.conversation.deleteConversation(session, {
+                                conversationId: target.conversation.id,
+                                presetLane,
+                                includeArchived,
+                                allPresetLanes
+                            })
+                        )
+                    }
+                    context.message = session.text('chatluna.conversation.messages.delete_success', [
+                        deleted.map((item) => item.title).join('\n')
+                    ])
+                }
                 return ChainMiddlewareRunStatus.STOP
             }

             const conversation =
                 await ctx.chatluna.conversation.deleteConversation(session, {
                     conversationId: resolvedConversationId(context),
                     presetLane,
                     includeArchived,
                     allPresetLanes
                 })

             context.message = session.text(
                 'chatluna.conversation.messages.delete_success',
                 conversationSummary(conversation)
             )
         } catch (error) {
             context.message = session.text(
                 'chatluna.conversation.messages.delete_failed',
                 [formatConversationError(session, error, 'delete')]
             )
         }

         return ChainMiddlewareRunStatus.STOP
     })

-async function deleteBySeqs(
-    ctx: Context,
-    session: Session,
-    seqs: number[],
-    opts: ReturnType<typeof getManageOptions>
-) {
-    const entries = await ctx.chatluna.conversation.listConversationEntries(
-        session,
-        opts
-    )
-    const targets = entries.filter((item) => seqs.includes(item.displaySeq))
-    if (targets.length !== seqs.length) {
-        return session.text('chatluna.conversation.messages.target_not_found')
-    }
-
-    const deleted: ConversationRecord[] = []
-    for (const target of targets) {
-        deleted.push(
-            await ctx.chatluna.conversation.deleteConversation(session, {
-                conversationId: target.conversation.id,
-                ...opts
-            })
-        )
-    }
-
-    return session.text('chatluna.conversation.messages.delete_success', [
-        deleted.map((item) => item.title).join('\n')
-    ])
-}

根据代码规范:"a function is justified only when called from multiple distinct call sites with non-trivial logic"。

Also applies to: 780-808

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/middlewares/system/conversation_manage.ts` around lines 283
- 293, The helper function deleteBySeqs is only invoked once at the call site
guarded by context.options.conversation_manage?.targetConversationSeqs (the
block that sets context.message and returns ChainMiddlewareRunStatus.STOP), so
inline its logic directly into that block and remove the now-unnecessary
deleteBySeqs function; locate the other similar single-use helper at the region
referenced (around the other occurrence between lines 780-808) and apply the
same inlining there, ensuring you preserve parameters used (ctx, session, seqs,
presetLane, includeArchived, allPresetLanes) and the exact behavior and return
value when assigning context.message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/middlewares/system/conversation_manage.ts`:
- Around line 805-807: The bulk-delete branch uses
session.text('chatluna.conversation.messages.delete_success', [...]) with a
single parameter (deleted.map(...).join('\n')) while the single-delete flow
passes three parameters via conversationSummary ([title, seq, id]); unify these
so the i18n placeholders align by either (A) using a dedicated i18n key for bulk
deletes (e.g. 'chatluna.conversation.messages.delete_success_multi') and call
session.text with that key and the single joined title string, or (B) keep the
existing 'delete_success' key and pass the same three-argument shape from the
bulk branch (for example [joinedTitles, '', ''] or a concatenated seq/id string)
so the parameter count matches; update the call site (the session.text
invocation in the deleted.map(...).join(...) line) accordingly and add the new
i18n entry if you choose option A.

---

Nitpick comments:
In `@packages/core/src/commands/conversation.ts`:
- Around line 243-244: The helper function parseSeqs should be inlined where
it's only used: replace the call sites that assign seqs (e.g., const target =
conversation?.trim() || undefined; const seqs = target == null ? undefined :
parseSeqs(target)) by moving parseSeqs's logic directly into that expression so
seqs is computed without an extra function; do the same for the other occurrence
around the 421–440 block—remove the parseSeqs declaration and update callers
(reference symbols: parseSeqs, target, seqs) so there is no separate function
call and behavior remains identical.

In `@packages/core/src/middlewares/system/conversation_manage.ts`:
- Around line 283-293: The helper function deleteBySeqs is only invoked once at
the call site guarded by
context.options.conversation_manage?.targetConversationSeqs (the block that sets
context.message and returns ChainMiddlewareRunStatus.STOP), so inline its logic
directly into that block and remove the now-unnecessary deleteBySeqs function;
locate the other similar single-use helper at the region referenced (around the
other occurrence between lines 780-808) and apply the same inlining there,
ensuring you preserve parameters used (ctx, session, seqs, presetLane,
includeArchived, allPresetLanes) and the exact behavior and return value when
assigning context.message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 83c3b84e-0a56-4955-96bb-b9daaf600ef9

📥 Commits

Reviewing files that changed from the base of the PR and between 2229393 and 1d3f6bb.

📒 Files selected for processing (3)
  • packages/core/src/commands/conversation.ts
  • packages/core/src/middlewares/system/conversation_manage.ts
  • packages/core/tests/conversation-resolve.spec.ts

Comment on lines +805 to +807
return session.text('chatluna.conversation.messages.delete_success', [
deleted.map((item) => item.title).join('\n')
])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

i18n 消息参数不一致

批量删除和单个删除使用同一个 i18n 键 delete_success,但传递的参数不一致:

  • 单个删除(303-305 行):通过 conversationSummary 传递 [title, seq, id] 三个参数
  • 批量删除(805-807 行):仅传递 [joined_titles] 一个参数

如果 i18n 模板期望三个占位符(如 {0}, {1}, {2}),批量删除时访问 {1}{2} 可能导致显示错误或 undefined。

建议统一参数格式,或为批量删除使用独立的 i18n 键。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/middlewares/system/conversation_manage.ts` around lines 805
- 807, The bulk-delete branch uses
session.text('chatluna.conversation.messages.delete_success', [...]) with a
single parameter (deleted.map(...).join('\n')) while the single-delete flow
passes three parameters via conversationSummary ([title, seq, id]); unify these
so the i18n placeholders align by either (A) using a dedicated i18n key for bulk
deletes (e.g. 'chatluna.conversation.messages.delete_success_multi') and call
session.text with that key and the single joined title string, or (B) keep the
existing 'delete_success' key and pass the same three-argument shape from the
bulk branch (for example [joinedTitles, '', ''] or a concatenated seq/id string)
so the parameter count matches; update the call site (the session.text
invocation in the deleted.map(...).join(...) line) accordingly and add the new
i18n entry if you choose option A.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant