feat(artifact): add extractArtifacts message scanner

Scans Message[] for artifact tool_use/tool_result pairs, parses URL/id/expires
from the upload response string, and returns ArtifactInfo[] newest-first.

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-06-20 16:01:30 +08:00
parent 5a715b504a
commit bdd023d0af
2 changed files with 217 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
import { describe, expect, test } from 'bun:test'
import { extractArtifacts } from '../scanner.js'
import type { Message } from 'src/types/message.js'
function assistantToolUse(id: string, input: Record<string, unknown>): Message {
return {
type: 'assistant',
uuid: crypto.randomUUID(),
message: {
role: 'assistant',
content: [{ type: 'tool_use' as const, id, name: 'artifact', input }],
},
}
}
function userToolResult(id: string, content: string, isError = false): Message {
return {
type: 'user',
uuid: crypto.randomUUID(),
message: {
role: 'user',
content: [
{
type: 'tool_result' as const,
tool_use_id: id,
content,
is_error: isError,
},
],
},
}
}
describe('extractArtifacts', () => {
test('returns empty list when no artifact tool_use messages', () => {
expect(extractArtifacts([])).toEqual([])
expect(
extractArtifacts([
{
type: 'user',
uuid: crypto.randomUUID(),
message: {
role: 'user',
content: [{ type: 'text' as const, text: 'hi' }],
},
},
]),
).toEqual([])
})
test('pairs a successful tool_use with its tool_result and returns parsed fields', () => {
const messages: Message[] = [
assistantToolUse('tu1', { file_path: '/tmp/report.html', ttl: 7 }),
userToolResult(
'tu1',
'Artifact uploaded: https://x.test/7d/abc.html (id: abc, expires: 2026-06-27T10:00:00.000Z)',
),
]
const result = extractArtifacts(messages)
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
filePath: '/tmp/report.html',
hash: 'abc',
url: 'https://x.test/7d/abc.html',
expiresAt: '2026-06-27T10:00:00.000Z',
basename: 'report.html',
isError: false,
})
})
test('skips artifact tool_use without a matching tool_result', () => {
const messages: Message[] = [
assistantToolUse('tu1', { file_path: '/tmp/report.html', ttl: 7 }),
]
expect(extractArtifacts(messages)).toEqual([])
})
test('keeps error results with isError=true and no parsed fields', () => {
const messages: Message[] = [
assistantToolUse('tu1', { file_path: '/tmp/missing.html', ttl: 7 }),
userToolResult(
'tu1',
'File does not exist or is not readable: /tmp/missing.html',
true,
),
]
const result = extractArtifacts(messages)
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
filePath: '/tmp/missing.html',
basename: 'missing.html',
isError: true,
})
expect(result[0].url).toBeUndefined()
})
test('orders newest first (last in conversation appears at top)', () => {
const messages: Message[] = [
assistantToolUse('tu1', { file_path: '/tmp/a.html', ttl: 7 }),
userToolResult(
'tu1',
'Artifact uploaded: https://x.test/7d/a.html (id: a, expires: 2026-06-27T10:00:00.000Z)',
),
assistantToolUse('tu2', { file_path: '/tmp/b.html', ttl: 7 }),
userToolResult(
'tu2',
'Artifact uploaded: https://x.test/7d/b.html (id: b, expires: 2026-06-27T10:00:00.000Z)',
),
]
const result = extractArtifacts(messages)
expect(result.map(r => r.basename)).toEqual(['b.html', 'a.html'])
})
})

View File

@@ -0,0 +1,97 @@
import { basename } from 'path'
import type { Message } from 'src/types/message.js'
export type ArtifactInfo = {
toolUseId: string
filePath: string
basename: string
hash?: string
url?: string
expiresAt?: string
rawContent: string
isError: boolean
}
const URL_REGEX = /https?:\/\/\S+\.html\b/
const ID_REGEX = /\bid:\s*([A-Za-z0-9_-]+)/
const EXPIRES_REGEX = /\bexpires:\s*([0-9T:.Z+-]+)/
export function extractArtifacts(messages: Message[]): ArtifactInfo[] {
const results: ArtifactInfo[] = []
for (const message of messages) {
if (message.type !== 'assistant') continue
const content = message.message?.content
if (!Array.isArray(content)) continue
for (const block of content) {
if (typeof block !== 'object' || block === null) continue
if (!('type' in block)) continue
const b = block as Record<string, unknown>
if (b.type !== 'tool_use') continue
if (b.name !== 'artifact') continue
const toolUseId = b.id as string
const input = b.input as { file_path?: string } | undefined
const filePath = input?.file_path ?? '<unknown>'
const resultBlock = findToolResult(messages, toolUseId)
if (!resultBlock) continue
const rawContent =
typeof resultBlock.content === 'string'
? resultBlock.content
: Array.isArray(resultBlock.content)
? resultBlock.content
.map(c =>
typeof c === 'string'
? c
: 'text' in c
? (c as { text: string }).text
: '',
)
.join('')
: ''
const isError = resultBlock.is_error === true
const urlMatch = rawContent.match(URL_REGEX)
const idMatch = rawContent.match(ID_REGEX)
const expiresMatch = rawContent.match(EXPIRES_REGEX)
results.push({
toolUseId,
filePath,
basename: basename(filePath),
hash: idMatch?.[1],
url: urlMatch?.[0],
expiresAt: expiresMatch?.[1],
rawContent,
isError,
})
}
}
// newest first
return results.reverse()
}
function findToolResult(
messages: Message[],
toolUseId: string,
): { content: unknown; is_error?: boolean } | null {
for (const message of messages) {
if (message.type !== 'user') continue
const content = message.message?.content
if (!Array.isArray(content)) continue
for (const block of content) {
if (typeof block !== 'object' || block === null) continue
if (!('type' in block)) continue
const b = block as Record<string, unknown>
if (b.type !== 'tool_result') continue
if (b.tool_use_id !== toolUseId) continue
return { content: b.content, is_error: b.is_error as boolean | undefined }
}
}
return null
}