feat(sync): 添加 Vault 所有的用户 Skill 记录

This commit is contained in:
2026-09-09 08:39:14 +08:00
parent bdd1543a4d
commit b0783c9356
25 changed files with 1507 additions and 29 deletions
@@ -0,0 +1,42 @@
import { beforeEach, expect, it, vi } from 'vitest'
import { apiClient } from './apiClient'
import * as service from './skillService'
vi.mock('./apiClient', () => {
const client = { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), postBinary: vi.fn() }
return { apiClient: client, default: client }
})
const request = {
revision: '', name: 'Review', description: '', prompt: 'Review carefully', tools: ['notes.read'],
permissions: ['notes.read'], retrieval: { top_k: 10, rerank: true, citation: true }, required_capabilities: ['chat'],
}
beforeEach(() => vi.clearAllMocks())
it('uses dedicated user Skill routes and stable idempotency keys', async () => {
const createOperation = '00000000-0000-4000-8000-000000000001'
const updateOperation = '00000000-0000-4000-8000-000000000002'
const deleteOperation = '00000000-0000-4000-8000-000000000003'
vi.mocked(apiClient.get).mockResolvedValue({ items: [] })
vi.mocked(apiClient.post).mockResolvedValue({})
vi.mocked(apiClient.put).mockResolvedValue({})
vi.mocked(apiClient.delete).mockResolvedValue({ status: 'completed' })
await service.listUserSkills()
await service.createUserSkill(request, createOperation)
await service.updateUserSkill('user_skill_' + '1'.repeat(32), { ...request, revision: 'a'.repeat(64) }, updateOperation)
await service.deleteUserSkill('user_skill_' + '1'.repeat(32), 'b'.repeat(64), deleteOperation)
expect(apiClient.get).toHaveBeenCalledWith('/api/user-skills', { params: { limit: 100, offset: 0 } })
expect(apiClient.post).toHaveBeenCalledWith('/api/user-skills', request, { headers: { 'Idempotency-Key': createOperation } })
expect(apiClient.put).toHaveBeenCalledWith('/api/user-skills/user_skill_' + '1'.repeat(32), expect.objectContaining({ revision: 'a'.repeat(64) }), { headers: { 'Idempotency-Key': updateOperation } })
expect(apiClient.delete).toHaveBeenCalledWith('/api/user-skills/user_skill_' + '1'.repeat(32), { params: { revision: 'b'.repeat(64) }, headers: { 'Idempotency-Key': deleteOperation } })
})
it('loads every bounded Host page instead of silently truncating user Skills', async () => {
const items = Array.from({ length: 101 }, (_, index) => ({ skill_id: `user_skill_${String(index).padStart(32, '0')}` }))
vi.mocked(apiClient.get)
.mockResolvedValueOnce({ items: items.slice(0, 100), page: { total: 101 } })
.mockResolvedValueOnce({ items: items.slice(100), page: { total: 101 } })
expect(await service.listUserSkills()).toHaveLength(101)
expect(apiClient.get).toHaveBeenNthCalledWith(2, '/api/user-skills', { params: { limit: 100, offset: 100 } })
})
+25 -1
View File
@@ -1,5 +1,5 @@
import apiClient from './apiClient'
import type { ApiSkill, OperationResponse, Skill } from '@/contracts'
import type { ApiSkill, OperationResponse, Skill, UserSkill, UserSkillWriteRequest } from '@/contracts'
function toSkill(skill: ApiSkill): Skill {
const { manifest } = skill
@@ -45,3 +45,27 @@ export async function disableSkill(skillId: string): Promise<Skill> {
export async function uninstallSkill(skillId: string): Promise<OperationResponse> {
return apiClient.delete(`/api/skills/${skillId}`)
}
export async function listUserSkills(): Promise<UserSkill[]> {
const items: UserSkill[] = []
for (let page = 0; page < 100; page += 1) {
const response = await apiClient.get<{ items: UserSkill[]; page?: { total: number } }>('/api/user-skills', { params: { limit: 100, offset: items.length } })
items.push(...response.items)
if (!response.items.length || items.length >= (response.page?.total ?? items.length)) return items
}
throw new Error('USER_SKILL_LIST_LIMIT_EXCEEDED')
}
export async function createUserSkill(request: UserSkillWriteRequest, operationId: string = crypto.randomUUID()): Promise<UserSkill> {
return apiClient.post('/api/user-skills', request, { headers: { 'Idempotency-Key': operationId } })
}
export async function updateUserSkill(skillId: string, request: UserSkillWriteRequest, operationId: string = crypto.randomUUID()): Promise<UserSkill> {
return apiClient.put(`/api/user-skills/${skillId}`, request, { headers: { 'Idempotency-Key': operationId } })
}
export async function deleteUserSkill(skillId: string, revision: string, operationId: string = crypto.randomUUID()): Promise<OperationResponse> {
return apiClient.delete(`/api/user-skills/${skillId}`, {
params: { revision }, headers: { 'Idempotency-Key': operationId },
})
}