feat: 添加 summary 命令 TypeScript 重写与其他命令增强

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
unraid
2026-04-22 22:38:09 +08:00
parent 6c5df395c3
commit 94c4b37eed
9 changed files with 179 additions and 8 deletions

View File

@@ -0,0 +1,91 @@
import { describe, test, expect, mock, beforeEach } from 'bun:test'
const mockManuallyExtract = mock(
(): Promise<any> => Promise.resolve({ success: true }),
)
const mockGetContent = mock(
(): Promise<any> => Promise.resolve('# Session Summary\n\nDid some work.'),
)
mock.module(
require.resolve('../../../services/SessionMemory/sessionMemory.js'),
() => ({
manuallyExtractSessionMemory: mockManuallyExtract,
}),
)
mock.module(
require.resolve('../../../services/SessionMemory/sessionMemoryUtils.js'),
() => ({
getSessionMemoryContent: mockGetContent,
}),
)
const { default: summaryCommand } = await import('../index.js')
const baseContext = {
messages: [{ type: 'user', role: 'user', content: 'hello' }],
options: { tools: [], mainLoopModel: 'test' },
setMessages: () => {},
onChangeAPIKey: () => {},
} as any
async function callSummary(ctx = baseContext) {
const mod = await summaryCommand.load()
return mod.call('', ctx)
}
beforeEach(() => {
mockManuallyExtract.mockReset()
mockGetContent.mockReset()
mockManuallyExtract.mockImplementation(() =>
Promise.resolve({ success: true }),
)
mockGetContent.mockImplementation(() =>
Promise.resolve('# Session Summary\n\nDid some work.'),
)
})
describe('summary command', () => {
test('command metadata', () => {
expect(summaryCommand.name).toBe('summary')
expect(summaryCommand.type).toBe('local')
expect(summaryCommand.isHidden).toBe(false)
expect(typeof summaryCommand.load).toBe('function')
})
test('refreshes and displays summary', async () => {
const result = await callSummary()
expect(result.type).toBe('text')
expect((result as any).value).toContain('Session summary updated.')
expect((result as any).value).toContain('Did some work.')
expect(mockManuallyExtract).toHaveBeenCalled()
})
test('handles extraction failure', async () => {
mockManuallyExtract.mockImplementation(() =>
Promise.resolve({ success: false, error: 'timeout' }),
)
const result = await callSummary()
expect((result as any).value).toContain(
'Failed to generate session summary',
)
expect((result as any).value).toContain('timeout')
})
test('handles empty content after extraction', async () => {
mockGetContent.mockImplementation(() => Promise.resolve(''))
const result = await callSummary()
expect((result as any).value).toContain('content is empty')
})
test('handles null content after extraction', async () => {
mockGetContent.mockImplementation(() => Promise.resolve(null))
const result = await callSummary()
expect((result as any).value).toContain('content is empty')
})
test('handles no messages', async () => {
const result = await callSummary({ ...baseContext, messages: [] })
expect((result as any).value).toBe('No messages to summarize.')
})
})

View File

@@ -0,0 +1,78 @@
/**
* /summary — Generate and display a session summary.
*
* Triggers a manual Session Memory extraction (bypassing automatic thresholds),
* then reads and displays the updated summary.md file.
*/
import type { Command, LocalCommandCall } from '../../types/command.js'
import type { Message } from '../../types/message.js'
/** Only user/assistant/system messages are valid for API calls. */
const API_SAFE_TYPES = new Set(['user', 'assistant', 'system'])
const call: LocalCommandCall = async (_args, context) => {
const { messages } = context
// Filter to API-safe message types only.
// context.messages includes progress/attachment/etc. that crash the API
// call chain (normalizeMessagesForAPI → addCacheBreakpoints expects
// only user/assistant). The automatic extraction path uses
// createCacheSafeParams(REPLHookContext) which already has clean
// messages; the manual path via /summary does not.
const safeMessages = (messages ?? []).filter(
(m): m is Message => m != null && API_SAFE_TYPES.has(m.type),
)
if (safeMessages.length === 0) {
return { type: 'text', value: 'No messages to summarize.' }
}
try {
const { manuallyExtractSessionMemory } = await import(
'../../services/SessionMemory/sessionMemory.js'
)
const { getSessionMemoryContent } = await import(
'../../services/SessionMemory/sessionMemoryUtils.js'
)
const safeContext = { ...context, messages: safeMessages }
const result = await manuallyExtractSessionMemory(safeMessages, safeContext)
if (!result.success) {
return {
type: 'text',
value: `Failed to generate session summary: ${result.error ?? 'unknown error'}`,
}
}
const content = await getSessionMemoryContent()
if (!content || content.trim().length === 0) {
return {
type: 'text',
value: 'Session summary was updated, but the content is empty.',
}
}
return {
type: 'text',
value: `Session summary updated.\n\n${content}`,
}
} catch (error) {
return {
type: 'text',
value: `Failed to generate session summary: ${error instanceof Error ? error.message : String(error)}`,
}
}
}
const summary = {
type: 'local',
name: 'summary',
description: 'Generate and display a session summary',
supportsNonInteractive: true,
isHidden: false,
load: () => Promise.resolve({ call }),
} satisfies Command
export default summary