mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Preserve user-invoked skill command metadata during session restore, WebSocket replay, and session search while continuing to hide malformed command breadcrumbs. Tested: - bun test src/server/__tests__/sessions.test.ts - bun test src/server/__tests__/ws-memory-events.test.ts - bun test src/server/__tests__/searchService.sessions.test.ts - cd desktop && bun run test -- --run src/stores/chatStore.test.ts - bun run check:server - cd desktop && bun run build - git diff --check Not-tested: - bun run check:desktop did not complete because an existing MessageList timestamp assertion fails independently of this change. Confidence: high Scope-risk: narrow
104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import {
|
|
COMMAND_ARGS_TAG,
|
|
COMMAND_MESSAGE_TAG,
|
|
COMMAND_NAME_TAG,
|
|
LOCAL_COMMAND_CAVEAT_TAG,
|
|
} from '../constants/xml.js'
|
|
|
|
const COMMAND_METADATA_TAGS = new Set([
|
|
COMMAND_NAME_TAG,
|
|
COMMAND_MESSAGE_TAG,
|
|
COMMAND_ARGS_TAG,
|
|
LOCAL_COMMAND_CAVEAT_TAG,
|
|
'skill-format',
|
|
])
|
|
|
|
const XML_TAG_BLOCK_PATTERN = /<([a-z][\w-]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>\s*/gi
|
|
|
|
function decodeXmlText(text: string): string {
|
|
return text
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'")
|
|
.replace(/&/g, '&')
|
|
}
|
|
|
|
function readXmlTag(xml: string, tag: string): string | undefined {
|
|
const match = xml.match(new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${tag}>`, 'i'))
|
|
const value = match?.[1]?.trim()
|
|
return value ? decodeXmlText(value) : undefined
|
|
}
|
|
|
|
function extractTextBlocks(content: unknown): string[] {
|
|
if (typeof content === 'string') return [content]
|
|
if (!Array.isArray(content)) return []
|
|
|
|
return content
|
|
.flatMap((block) => {
|
|
if (!block || typeof block !== 'object') return []
|
|
const record = block as Record<string, unknown>
|
|
return record.type === 'text' && typeof record.text === 'string'
|
|
? [record.text]
|
|
: []
|
|
})
|
|
.map((text) => text.trim())
|
|
.filter(Boolean)
|
|
}
|
|
|
|
function hasCommandMetadataTag(text: string): boolean {
|
|
return (
|
|
text.includes(`<${COMMAND_NAME_TAG}>`) ||
|
|
text.includes(`<${COMMAND_MESSAGE_TAG}>`) ||
|
|
text.includes(`<${COMMAND_ARGS_TAG}>`) ||
|
|
text.includes(`<${LOCAL_COMMAND_CAVEAT_TAG}>`) ||
|
|
text.includes('<skill-format>')
|
|
)
|
|
}
|
|
|
|
function isOnlyKnownCommandMetadata(text: string): boolean {
|
|
const remainder = text.replace(XML_TAG_BLOCK_PATTERN, (match, tag: string) => (
|
|
COMMAND_METADATA_TAGS.has(tag.toLowerCase()) ? '' : match
|
|
))
|
|
return remainder.trim().length === 0
|
|
}
|
|
|
|
function formatCommandDisplayText(commandName: string, args: string, skillFormat: boolean, commandMessage?: string): string {
|
|
if (skillFormat) {
|
|
return `Skill(${commandMessage || commandName.replace(/^\//, '')})`
|
|
}
|
|
|
|
const normalizedName = commandName.startsWith('/') ? commandName : `/${commandName}`
|
|
return [normalizedName, args.trim()].filter(Boolean).join(' ')
|
|
}
|
|
|
|
function parseCommandMetadataText(text: string): string | null {
|
|
const trimmed = text.trim()
|
|
if (!hasCommandMetadataTag(trimmed)) return null
|
|
if (!isOnlyKnownCommandMetadata(trimmed)) return null
|
|
|
|
const commandName = readXmlTag(trimmed, COMMAND_NAME_TAG)
|
|
if (!commandName) return null
|
|
|
|
const args = readXmlTag(trimmed, COMMAND_ARGS_TAG) ?? ''
|
|
const commandMessage = readXmlTag(trimmed, COMMAND_MESSAGE_TAG)
|
|
const skillFormat = readXmlTag(trimmed, 'skill-format') === 'true'
|
|
return formatCommandDisplayText(commandName, args, skillFormat, commandMessage)
|
|
}
|
|
|
|
export function getCommandMetadataDisplayText(content: unknown): string | null {
|
|
const textBlocks = extractTextBlocks(content)
|
|
if (textBlocks.length === 0) return null
|
|
|
|
const displayBlocks = textBlocks.map(parseCommandMetadataText)
|
|
if (displayBlocks.some((text) => text === null)) return null
|
|
return displayBlocks.join('\n')
|
|
}
|
|
|
|
export function shouldHideCommandMetadataContent(content: unknown): boolean {
|
|
const textBlocks = extractTextBlocks(content)
|
|
if (textBlocks.length === 0) return false
|
|
if (!textBlocks.some(hasCommandMetadataTag)) return false
|
|
return getCommandMetadataDisplayText(content) === null
|
|
}
|