feat: initial release v0.3.0

This commit is contained in:
saturn
2026-03-08 03:15:27 +08:00
commit 881ed44996
1311 changed files with 225407 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'
import { getAssistantSkill, isAssistantId } from '@/lib/assistant-platform'
describe('assistant-platform registry', () => {
it('recognizes supported assistant ids', () => {
expect(isAssistantId('api-config-template')).toBe(true)
expect(isAssistantId('tutorial')).toBe(true)
expect(isAssistantId('unknown')).toBe(false)
})
it('returns registered skills', () => {
expect(getAssistantSkill('api-config-template').id).toBe('api-config-template')
expect(getAssistantSkill('tutorial').id).toBe('tutorial')
})
})

View File

@@ -0,0 +1,46 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const getUserModelConfigMock = vi.hoisted(() =>
vi.fn(async () => ({ analysisModel: null })),
)
vi.mock('@/lib/config-service', () => ({
getUserModelConfig: getUserModelConfigMock,
}))
import { AssistantPlatformError } from '@/lib/assistant-platform'
import { createAssistantChatResponse } from '@/lib/assistant-platform/runtime'
describe('assistant-platform runtime', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('throws invalid request when messages payload is malformed', async () => {
await expect(createAssistantChatResponse({
userId: 'user-1',
assistantId: 'api-config-template',
context: {},
messages: { invalid: true },
})).rejects.toMatchObject({
code: 'ASSISTANT_INVALID_REQUEST',
} as Partial<AssistantPlatformError>)
})
it('throws missing model when analysisModel is not configured', async () => {
await expect(createAssistantChatResponse({
userId: 'user-1',
assistantId: 'api-config-template',
context: {
providerId: 'openai-compatible:oa-1',
},
messages: [{
id: 'u1',
role: 'user',
parts: [{ type: 'text', text: 'hello' }],
}],
})).rejects.toMatchObject({
code: 'ASSISTANT_MODEL_NOT_CONFIGURED',
} as Partial<AssistantPlatformError>)
})
})

View File

@@ -0,0 +1,230 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AssistantRuntimeContext } from '@/lib/assistant-platform'
const saveModelTemplateConfigurationMock = vi.hoisted(() =>
vi.fn(async () => ({ modelKey: 'openai-compatible:oa-1::veo3.1' })),
)
vi.mock('@/lib/user-api/model-template/save', () => ({
saveModelTemplateConfiguration: saveModelTemplateConfigurationMock,
}))
import { apiConfigTemplateSkill } from '@/lib/assistant-platform/skills/api-config-template'
function buildRuntimeContext(): AssistantRuntimeContext {
return {
userId: 'user-1',
assistantId: 'api-config-template',
context: {
providerId: 'openai-compatible:oa-1',
},
analysisModelKey: 'openrouter::gpt-5-mini',
resolvedModel: {
providerId: 'openrouter',
providerKey: 'openrouter',
modelId: 'gpt-5-mini',
},
}
}
describe('assistant-platform api-config-template skill', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns invalid when template fails schema validation', async () => {
const tools = apiConfigTemplateSkill.tools?.(buildRuntimeContext())
expect(tools).toBeTruthy()
const saveTool = tools?.saveModelTemplate
expect(saveTool).toBeTruthy()
if (!saveTool?.execute) {
throw new Error('saveModelTemplate.execute is required for test')
}
const result = await saveTool.execute({
modelId: 'veo3.1',
name: 'Veo 3.1',
type: 'video',
compatMediaTemplate: {
version: 1,
mediaType: 'video',
mode: 'async',
create: {
method: 'POST',
path: '/v2/videos/generations',
},
response: {
taskIdPath: '$.task_id',
},
},
}, {} as never)
expect(result.status).toBe('invalid')
expect(result.code).toBe('MODEL_TEMPLATE_INVALID')
expect(saveModelTemplateConfigurationMock).not.toHaveBeenCalled()
})
it('saves template when payload is valid', async () => {
const tools = apiConfigTemplateSkill.tools?.(buildRuntimeContext())
expect(tools).toBeTruthy()
const saveTool = tools?.saveModelTemplate
expect(saveTool).toBeTruthy()
if (!saveTool?.execute) {
throw new Error('saveModelTemplate.execute is required for test')
}
const result = await saveTool.execute({
modelId: 'veo3.1',
name: 'Veo 3.1',
type: 'video',
compatMediaTemplate: {
version: 1,
mediaType: 'video',
mode: 'async',
create: {
method: 'POST',
path: '/v2/videos/generations',
contentType: 'application/json',
bodyTemplate: {
model: '{{model}}',
prompt: '{{prompt}}',
},
},
status: {
method: 'GET',
path: '/v2/videos/generations/{{task_id}}',
},
response: {
taskIdPath: '$.task_id',
statusPath: '$.status',
outputUrlPath: '$.video_url',
},
polling: {
intervalMs: 3000,
timeoutMs: 180000,
doneStates: ['done'],
failStates: ['failed'],
},
},
}, {} as never)
expect(result.status).toBe('saved')
expect(result.savedModelKey).toBe('openai-compatible:oa-1::veo3.1')
expect(saveModelTemplateConfigurationMock).toHaveBeenCalledWith({
userId: 'user-1',
providerId: 'openai-compatible:oa-1',
modelId: 'veo3.1',
name: 'Veo 3.1',
type: 'video',
template: expect.objectContaining({
mediaType: 'video',
}),
source: 'ai',
})
})
it('saves multiple templates when batch payload is valid', async () => {
const tools = apiConfigTemplateSkill.tools?.(buildRuntimeContext())
expect(tools).toBeTruthy()
const batchTool = tools?.saveModelTemplates
expect(batchTool).toBeTruthy()
if (!batchTool?.execute) {
throw new Error('saveModelTemplates.execute is required for test')
}
const result = await batchTool.execute({
models: [
{
modelId: 'veo3-fast',
name: 'Veo 3 Fast',
type: 'video',
compatMediaTemplate: {
version: 1,
mediaType: 'video',
mode: 'async',
create: {
method: 'POST',
path: '/video/create',
contentType: 'application/json',
bodyTemplate: {
model: '{{model}}',
prompt: '{{prompt}}',
images: ['{{image}}'],
},
},
status: {
method: 'GET',
path: '/video/query?id={{task_id}}',
},
response: {
taskIdPath: '$.id',
statusPath: '$.status',
outputUrlPath: '$.video_url',
},
polling: {
intervalMs: 5000,
timeoutMs: 600000,
doneStates: ['completed'],
failStates: ['failed'],
},
},
},
{
modelId: 'veo3.1-fast',
name: 'Veo 3.1 Fast',
type: 'video',
compatMediaTemplate: {
version: 1,
mediaType: 'video',
mode: 'async',
create: {
method: 'POST',
path: '/video/create',
contentType: 'application/json',
bodyTemplate: {
model: '{{model}}',
prompt: '{{prompt}}',
images: ['{{image}}'],
},
},
status: {
method: 'GET',
path: '/video/query?id={{task_id}}',
},
response: {
taskIdPath: '$.id',
statusPath: '$.status',
outputUrlPath: '$.video_url',
},
polling: {
intervalMs: 5000,
timeoutMs: 600000,
doneStates: ['completed'],
failStates: ['failed'],
},
},
},
],
}, {} as never)
expect(result.status).toBe('saved')
expect(result.savedModelKeys).toHaveLength(2)
expect(saveModelTemplateConfigurationMock).toHaveBeenCalledTimes(2)
expect(saveModelTemplateConfigurationMock).toHaveBeenNthCalledWith(1, expect.objectContaining({
modelId: 'veo3-fast',
name: 'Veo 3 Fast',
providerId: 'openai-compatible:oa-1',
userId: 'user-1',
type: 'video',
source: 'ai',
}))
expect(saveModelTemplateConfigurationMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
modelId: 'veo3.1-fast',
name: 'Veo 3.1 Fast',
providerId: 'openai-compatible:oa-1',
userId: 'user-1',
type: 'video',
source: 'ai',
}))
})
})

View File

@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest'
import { renderAssistantSystemPrompt } from '@/lib/assistant-platform/system-prompts'
describe('assistant-platform system prompts', () => {
it('loads api-config-template prompt from lib/prompts/skills and injects providerId', () => {
const prompt = renderAssistantSystemPrompt('api-config-template', {
providerId: 'openai-compatible:oa-1',
})
expect(prompt).toContain('你是 API 配置助手')
expect(prompt).toContain('当前 providerId=openai-compatible:oa-1')
expect(prompt).not.toContain('{{providerId}}')
})
it('loads tutorial prompt from lib/prompts/skills', () => {
const prompt = renderAssistantSystemPrompt('tutorial')
expect(prompt).toContain('你是产品教程助手')
expect(prompt).toContain('禁止编造不存在的页面')
})
})