fix(sync): 端到端强制人设与布局显式选择加入
This commit is contained in:
@@ -95,7 +95,7 @@ async function save() {
|
||||
<dialog ref="dialog" class="modal persona-dialog" aria-labelledby="persona-title" @cancel.prevent="emit('close')" @click="($event.target === dialog) && emit('close')">
|
||||
<form @submit.prevent="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 class="notice-banner">{{ isDesktop() ? t('工作区人设 · 可在同步设置中选择随当前 Vault 同步,应用于此工作区的对话与智能体。旧全局人设不会自动导入。', 'Workspace persona · Optionally 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>
|
||||
|
||||
@@ -81,3 +81,15 @@ it('shows persisted per-job interruption counts without starting work from the v
|
||||
expect(vi.mocked(hostInvoke).mock.calls.every(([command]) => command === 'sync_status')).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
|
||||
it('keeps optional records off until an explicit unbound scope change', async () => {
|
||||
vi.mocked(hostInvoke).mockResolvedValue({ ...empty(), optional_scope: { persona: false, layout: false } })
|
||||
const wrapper = mount(SyncSettings); await flushPromises()
|
||||
const options = wrapper.findAll('.sync-scope input[type=checkbox]')
|
||||
expect(options).toHaveLength(2)
|
||||
expect((options[0]!.element as HTMLInputElement).checked).toBe(false)
|
||||
await options[0]!.setValue(true); await flushPromises()
|
||||
expect(hostInvoke).toHaveBeenCalledWith('sync_set_scope', { vaultId: empty().vault_id, scope: { persona: true, layout: false } })
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import { t } from '@/i18n'
|
||||
const { actionDialog, resolveAction, askConfirm } = useActionDialog()
|
||||
interface Binding { id: string; endpoint: string; account: string; remote_vault: string; cursor: number }
|
||||
interface Conflict { sequence: number; local_path: string; local_hash: string; current_hash?: string; current_path?: string; remote: { path: string; operation: string } }
|
||||
interface Status { vault_id: string; binding: Binding | null; paused: boolean; pending: number; conflicts: Conflict[]; credential_state: string; running: boolean; error: string | null; retry_in: number | null; failures: number; halted: boolean; attempts?: Array<{ operation_id: string; path: string; attempts: number; outcome: string; error: string | null }> }
|
||||
interface Status { optional_scope?: { persona: boolean; layout: boolean }; vault_id: string; binding: Binding | null; paused: boolean; pending: number; conflicts: Conflict[]; credential_state: string; running: boolean; error: string | null; retry_in: number | null; failures: number; halted: boolean; attempts?: Array<{ operation_id: string; path: string; attempts: number; outcome: string; error: string | null }> }
|
||||
interface RemoteVault { id: string; name: string; sequence: number; used: number; quota: number }
|
||||
const status = ref<Status | null>(null)
|
||||
const endpoint = ref('https://'), account = ref(''), password = ref(''), device = ref('OpenNexus Desktop'), testHttp = ref(false)
|
||||
@@ -42,6 +42,15 @@ async function act(action: () => Promise<void>) {
|
||||
catch (error) { message.value = error instanceof Error ? error.message : 'SYNC_FAILED' }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
function setScope(kind: 'persona' | 'layout', event: Event) {
|
||||
const current = status.value
|
||||
if (!current || current.binding) return
|
||||
const input = event.target as HTMLInputElement
|
||||
const scope = { persona: false, layout: false, ...current.optional_scope, [kind]: input.checked }
|
||||
input.checked = current.optional_scope?.[kind] ?? false
|
||||
preview.value = null
|
||||
return act(async () => { await hostInvoke('sync_set_scope', { vaultId: current.vault_id, scope }) })
|
||||
}
|
||||
async function listVaults() {
|
||||
const result = await hostInvoke<{ items: RemoteVault[] }>('sync_vaults', { endpoint: endpoint.value, account: account.value })
|
||||
remoteVaults.value = result.items
|
||||
@@ -118,6 +127,12 @@ onUnmounted(() => { mounted = false; clearInterval(timer); password.value = '' }
|
||||
<div class="inline-actions"><button :disabled="previewPage === 0" @click="previewPage--">{{ t('上一页', 'Previous') }}</button><button :disabled="(previewPage + 1) * 100 >= preview.items.length" @click="previewPage++">{{ t('下一页', 'Next') }}</button><button :disabled="busy" @click="merge">{{ t('确认合并并绑定', 'Confirm merge and bind') }}</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset v-if="status" :disabled="busy || !!status.binding" class="sync-scope">
|
||||
<legend>{{ t('可选同步内容', 'Optional sync content') }}</legend>
|
||||
<label><input type="checkbox" :checked="status.optional_scope?.persona ?? false" @change="setScope('persona', $event)" />{{ t('工作区人设', 'Workspace persona') }}</label>
|
||||
<label><input type="checkbox" :checked="status.optional_scope?.layout ?? false" @change="setScope('layout', $event)" />{{ t('侧栏布局', 'Sidebar layout') }}</label>
|
||||
<p>{{ t('默认仅保存在本机。修改已绑定范围时,请先解除绑定,再重新预览合并;关闭选项不会删除远端内容。', 'Kept locally by default. Unbind before changing scope, then preview a new merge. Disabling an option does not delete remote content.') }}</p>
|
||||
</fieldset>
|
||||
<div v-if="status?.binding" class="sync-bound">
|
||||
<p>{{ status.binding.endpoint }} · {{ status.binding.account }} · {{ status.binding.remote_vault }}</p>
|
||||
<p aria-live="polite">{{ status.paused ? t('已暂停', 'Paused') : status.running ? t('同步中', 'Syncing') : status.halted ? t('自动同步已停止,请处理错误后重试', 'Automatic sync stopped; resolve the error and retry') : t('等待下一轮同步', 'Waiting for next sync') }} · {{ t('待上传', 'Pending') }} {{ status.pending }} · cursor {{ status.binding.cursor }}</p>
|
||||
|
||||
Reference in New Issue
Block a user