feat(sync): 显式导入 Vault 前预览旧版人设
This commit is contained in:
@@ -69,3 +69,48 @@ it('sends the loaded persona revision and refuses a form from another Vault', as
|
||||
expect(wrapper.text()).toContain('工作区已切换')
|
||||
} finally { wrapper.unmount(); desktopMode.mockRestore() }
|
||||
})
|
||||
|
||||
|
||||
it('previews legacy content without writing and preserves the Vault CAS when copied', async () => {
|
||||
const desktopMode = vi.spyOn(desktop, 'isDesktop').mockReturnValue(true)
|
||||
useWorkspaceStore().vaultId = 'import-target'
|
||||
vi.mocked(apiClient.put).mockClear()
|
||||
vi.mocked(apiClient.get).mockImplementation(async url => url.endsWith('/legacy')
|
||||
? { available: true, persona: { version: 90, name: 'Old', system_prompt: 'Legacy prompt', dialogue_pairs: [{ user: 'Question', assistant: 'Answer' }] } }
|
||||
: { version: 3, revision: 'c'.repeat(64), name: 'Current', system_prompt: 'Current prompt', dialogue_pairs: [] })
|
||||
const wrapper = mount(ChatPersonaDialog)
|
||||
try {
|
||||
await flushPromises()
|
||||
await wrapper.findAll('button').find(button => button.text().includes('查看旧全局人设'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('Legacy prompt')
|
||||
expect((wrapper.get('.persona-prompt').element as HTMLTextAreaElement).value).toBe('Current prompt')
|
||||
expect(apiClient.put).not.toHaveBeenCalled()
|
||||
await wrapper.findAll('button').find(button => button.text().includes('填入当前表单'))!.trigger('click')
|
||||
expect(apiClient.put).not.toHaveBeenCalled()
|
||||
expect((wrapper.get('.persona-prompt').element as HTMLTextAreaElement).value).toBe('Legacy prompt')
|
||||
await wrapper.get('form').trigger('submit')
|
||||
await flushPromises()
|
||||
expect(apiClient.put).toHaveBeenCalledWith('/api/settings/persona', expect.objectContaining({ version: 3, revision: 'c'.repeat(64), name: 'Old', system_prompt: 'Legacy prompt' }))
|
||||
} finally { wrapper.unmount(); desktopMode.mockRestore() }
|
||||
})
|
||||
|
||||
it('discards a late legacy preview after switching Vaults', async () => {
|
||||
const desktopMode = vi.spyOn(desktop, 'isDesktop').mockReturnValue(true)
|
||||
const workspace = useWorkspaceStore()
|
||||
workspace.vaultId = 'before'
|
||||
let complete!: (value: unknown) => void
|
||||
vi.mocked(apiClient.get).mockImplementation(async url => url.endsWith('/legacy')
|
||||
? await new Promise(resolve => { complete = resolve })
|
||||
: { version: 0, revision: '', name: '', system_prompt: '', dialogue_pairs: [] })
|
||||
const wrapper = mount(ChatPersonaDialog)
|
||||
try {
|
||||
await flushPromises()
|
||||
await wrapper.findAll('button').find(button => button.text().includes('查看旧全局人设'))!.trigger('click')
|
||||
workspace.vaultId = 'after'
|
||||
complete({ available: true, persona: { version: 1, name: 'Late', system_prompt: 'Stale source', dialogue_pairs: [] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).not.toContain('Stale source')
|
||||
expect(wrapper.findAll('button').some(button => button.text().includes('填入当前表单'))).toBe(false)
|
||||
} finally { wrapper.unmount(); desktopMode.mockRestore() }
|
||||
})
|
||||
|
||||
@@ -14,9 +14,12 @@ const error = ref('')
|
||||
const remote = reactive<GlobalPersona>({version:0,name:'',system_prompt:'',dialogue_pairs:[]})
|
||||
const workspace = useWorkspaceStore()
|
||||
let loadedVault = workspace.vaultId
|
||||
watch(() => workspace.vaultId, () => { if (isDesktop()) { ready.value = false; error.value = t('工作区已切换,请重新打开人设设置。', 'Workspace changed. Reopen persona settings.') } })
|
||||
watch(() => workspace.vaultId, () => { if (isDesktop()) { ready.value = false; legacy.value = null; error.value = t('工作区已切换,请重新打开人设设置。', 'Workspace changed. Reopen persona settings.') } })
|
||||
const ready = ref(false)
|
||||
const saving = ref(false)
|
||||
const legacy = ref<GlobalPersona | null>(null)
|
||||
const legacyLoading = ref(false)
|
||||
const legacyMessage = ref('')
|
||||
const loading = ref(0)
|
||||
const dialog = ref<HTMLDialogElement>()
|
||||
const previousFocus = document.activeElement as HTMLElement | null
|
||||
@@ -29,6 +32,26 @@ async function loadGlobal() {
|
||||
try { const result = await apiClient.get<GlobalPersona>('/api/settings/persona'); if (active && (!isDesktop() || vault === workspace.vaultId)) { Object.assign(remote,result); loadedVault = vault; ready.value = true } }
|
||||
catch { if (active) error.value = t('无法加载全局人设,请重试。', 'Could not load global persona. Retry.') }
|
||||
}
|
||||
async function previewLegacy() {
|
||||
if (!ready.value || saving.value || legacyLoading.value) return
|
||||
const vault = workspace.vaultId
|
||||
legacyLoading.value = true; legacyMessage.value = ''; legacy.value = null
|
||||
try {
|
||||
const result = await apiClient.get<{ available: boolean; persona: GlobalPersona | null }>('/api/settings/persona/legacy')
|
||||
if (!active || vault !== workspace.vaultId || loadedVault !== vault) return
|
||||
legacy.value = result.available ? result.persona : null
|
||||
if (!legacy.value) legacyMessage.value = t('没有可导入的旧全局人设。', 'No legacy global persona is available.')
|
||||
} catch { if (active && vault === workspace.vaultId) legacyMessage.value = t('无法读取旧人设,请重试。', 'Could not read the legacy persona. Retry.') }
|
||||
finally { legacyLoading.value = false }
|
||||
}
|
||||
function useLegacy() {
|
||||
if (!legacy.value || !ready.value || saving.value || loadedVault !== workspace.vaultId) return
|
||||
remote.name = legacy.value.name
|
||||
remote.system_prompt = legacy.value.system_prompt
|
||||
remote.dialogue_pairs = legacy.value.dialogue_pairs.map(pair => ({ ...pair }))
|
||||
legacy.value = null
|
||||
legacyMessage.value = t('已填入表单;点击保存才会写入当前工作区,旧数据保留。', 'Copied into the form. Save to write to this workspace; legacy data is retained.')
|
||||
}
|
||||
onMounted(() => { if (dialog.value) restoreScroll = lockDialogScroll(dialog.value); dialog.value?.showModal(); void loadGlobal() })
|
||||
onBeforeUnmount(() => { active = false; dialog.value?.close(); restoreScroll?.(); previousFocus?.focus() })
|
||||
async function chooseAvatar(event: Event, field: 'aiAvatar' | 'userAvatar') {
|
||||
@@ -74,10 +97,20 @@ async function save() {
|
||||
<div class="persona-heading"><h2 id="persona-title">{{ t('人设与头像', 'Persona and avatars') }}</h2><button type="button" class="button-secondary" @click="emit('close')">{{ t('关闭', 'Close') }}</button></div>
|
||||
<p class="notice-banner">{{ isDesktop() ? t('工作区人设 · 随当前 Vault 同步,应用于此工作区的对话与智能体。旧全局人设不会自动导入。', 'Workspace persona · Syncs with this Vault and applies to its chats and agents. Legacy global personas are not imported automatically.') : t('全局人设 · 应用于连接此 AI Core 的所有对话与智能体。留空的提示词和对话示例不会拼入请求。', 'Global persona · Applies to all chats and agents connected to this AI Core. Empty prompts and examples are omitted.') }}</p>
|
||||
<p v-if="!ready" role="status">{{ t('正在加载全局设置', 'Loading global settings') }} <button type="button" class="button-secondary" @click="loadGlobal">{{ t('重试', 'Retry') }}</button></p>
|
||||
<section v-if="isDesktop()" class="legacy-persona">
|
||||
<button type="button" class="button-secondary" :disabled="!ready || saving || legacyLoading" @click="previewLegacy">{{ t('查看旧全局人设', 'Preview legacy global persona') }}</button>
|
||||
<p v-if="legacyMessage" role="status">{{ legacyMessage }}</p>
|
||||
<div v-if="legacy" class="item-card">
|
||||
<p>{{ t('预览旧数据;填入表单会替换当前草稿,保存后才生效。', 'Preview only. Copying replaces the current draft; changes take effect after saving.') }}</p>
|
||||
<strong>{{ legacy.name }}</strong><pre>{{ legacy.system_prompt }}</pre>
|
||||
<article v-for="(pair, index) in legacy.dialogue_pairs" :key="index"><pre>{{ pair.user }}</pre><pre>{{ pair.assistant }}</pre></article>
|
||||
<button type="button" class="button-secondary" :disabled="!ready || saving" @click="useLegacy">{{ t('填入当前表单', 'Copy into current form') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
<fieldset :disabled="!ready || saving" class="persona-columns">
|
||||
<div class="persona-primary">
|
||||
<label class="field"><span>{{ t('人设名称', 'Persona name') }}</span><input v-model="remote.name" class="input" maxlength="128" :placeholder="t('例如:知识助理', 'For example: Knowledge assistant')" /></label>
|
||||
<label class="field"><span>{{ t('全局系统提示词', 'Global system prompt') }}</span><textarea v-model="remote.system_prompt" class="textarea persona-prompt" maxlength="16000" :placeholder="t('描述 AI 的身份、语气及回答要求;留空则不添加', 'Identity, tone and response requirements; leave blank to omit')" /></label>
|
||||
<label class="field"><span>{{ t('系统提示词', 'System prompt') }}</span><textarea v-model="remote.system_prompt" class="textarea persona-prompt" maxlength="16000" :placeholder="t('描述 AI 的身份、语气及回答要求;留空则不添加', 'Identity, tone and response requirements; leave blank to omit')" /></label>
|
||||
</div>
|
||||
<div class="persona-secondary">
|
||||
<details open class="ui-disclosure"><summary>{{ t('预设对话', 'Example dialogue') }}</summary>
|
||||
@@ -109,6 +142,7 @@ async function save() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.legacy-persona pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 180px; overflow: auto; }
|
||||
.persona-dialog { width: min(1120px, calc(100vw - 32px)); max-height: calc(100dvh - 48px); box-sizing: border-box; margin: auto; overflow: auto; color: var(--color-text-primary); background: var(--color-surface-primary); }
|
||||
.persona-columns { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 24px; border: 0; margin: 0; padding: 0; min-width: 0; }
|
||||
.persona-primary, .persona-secondary { min-width: 0; }
|
||||
|
||||
Reference in New Issue
Block a user