fix(memhub): close model center settings gaps

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
kristinheandCursor committed 2026-11-04 00:22:25 +08:00
1 parent 17441d368c
commit 1429ba2d19
26 files changed
+1427 -142

No files matched your search

+7 -4
View File
@@ -35,6 +35,7 @@ import { OnboardingFlow } from "./OnboardingFlow";
import { R6Button, R6LocalStorage } from "./R6Controls";
import { R6Settings } from "./R6Settings";
import { prefetchDataRuntime } from "./runtime-state";
import { EmbeddingSwitchProvider } from "./use-embedding-switch";
import "./styles.css";
import "./core-visual.css";
import "./r6-shell.css";
@@ -48,10 +49,12 @@ const rendererStartedAt = Date.now();
export function App() {
return (
<DesktopFrame>
<AppContent />
<DebugLogPanel />
</DesktopFrame>
<EmbeddingSwitchProvider>
<DesktopFrame>
<AppContent />
<DebugLogPanel />
</DesktopFrame>
</EmbeddingSwitchProvider>
);
}
@@ -27,6 +27,7 @@ import {
EmbeddingSwitchConfirmDialog,
EmbeddingSwitchGlobalStatus,
} from "./EmbeddingSwitchExperience";
import { LANGUAGE_STORAGE_KEY, LanguageProvider } from "./gui-language";
const NOW = "2026-11-03T09:00:00Z";
const space = (id: string | null, model: string) => ({
@@ -101,9 +102,59 @@ const job: EmbeddingSwitchJob = {
afterEach(() => {
cleanup();
current = null;
localStorage.removeItem(LANGUAGE_STORAGE_KEY);
vi.clearAllMocks();
});
it("switches Chinese, Spanish, and English without leaking Chinese", async () => {
localStorage.setItem(LANGUAGE_STORAGE_KEY, "zh-Hans");
const view = render(
<LanguageProvider>
<EmbeddingSwitchConfirmDialog
plan={{ ...plan, users: 2 }}
busy={false}
errorCode={null}
onCancel={vi.fn()}
onStart={vi.fn()}
/>
</LanguageProvider>,
);
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "更新记忆搜索索引?" }),
).toBeTruthy(),
);
localStorage.setItem(LANGUAGE_STORAGE_KEY, "es");
window.dispatchEvent(
new StorageEvent("storage", { key: LANGUAGE_STORAGE_KEY }),
);
await waitFor(() =>
expect(
screen.getByRole("heading", {
name: "¿Actualizar el índice de búsqueda de memorias?",
}),
).toBeTruthy(),
);
expect(view.container.textContent).not.toMatch(/[一-鿿]/);
expect(view.container.textContent).toContain("8421");
localStorage.setItem(LANGUAGE_STORAGE_KEY, "en");
window.dispatchEvent(
new StorageEvent("storage", { key: LANGUAGE_STORAGE_KEY }),
);
await waitFor(() =>
expect(
screen.getByRole("heading", {
name: "Update the memory search index?",
}),
).toBeTruthy(),
);
expect(view.container.textContent).not.toMatch(/[一-鿿]/);
expect(view.container.textContent).toContain("8,421");
});
it("explains the index update in user terms before exposing technical details", async () => {
const onStart = vi.fn();
render(
+143 -104
View File
@@ -3,50 +3,68 @@ import type {
EmbeddingSwitchJob,
EmbeddingSwitchPlan,
} from "@one-memory/contracts";
import type { GuiLocale, MessageKey } from "@one-memory/i18n";
import { ChevronDown, CircleAlert, X } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { deleteRetiredEmbeddingSpace } from "./api";
import { useGuiLanguage } from "./gui-language";
import { R6Button } from "./R6Controls";
import { useEmbeddingSwitch } from "./use-embedding-switch";
import "./embedding-switch.css";
function count(value: number | null | undefined) {
return new Intl.NumberFormat().format(value ?? 0);
type Translator = (
key: MessageKey,
params?: Record<string, string | number>,
) => string;
function count(locale: GuiLocale, value: number | null | undefined) {
return new Intl.NumberFormat(locale).format(value ?? 0);
}
function duration(seconds: number | null) {
if (seconds === null) return "暂时无法估算";
if (seconds < 60) return `约 ${Math.max(1, Math.round(seconds))} 秒`;
return `约 ${Math.max(1, Math.round(seconds / 60))} 分钟`;
function duration(locale: GuiLocale, t: Translator, seconds: number | null) {
if (seconds === null) return t("embeddingSwitch.duration.unknown");
const value = Math.max(1, Math.round(seconds < 60 ? seconds : seconds / 60));
return t(
seconds < 60
? "embeddingSwitch.duration.seconds"
: "embeddingSwitch.duration.minutes",
{ count: count(locale, value) },
);
}
function size(bytes: number) {
if (bytes < 1024 * 1024) return `${Math.ceil(bytes / 1024)} KB`;
return `${Math.ceil(bytes / 1024 / 1024)} MB`;
function size(locale: GuiLocale, bytes: number) {
const megabytes = bytes >= 1024 * 1024;
const value = Math.ceil(bytes / 1024 / (megabytes ? 1024 : 1));
return `${count(locale, value)} ${megabytes ? "MB" : "KB"}`;
}
const blockerCopy: Record<string, string> = {
embedding_probe_required: "请先测试新的搜索模型连接。",
const blockerKeys: Record<string, MessageKey> = {
embedding_probe_required: "embeddingSwitch.blocker.embedding_probe_required",
embedding_dimensions_unknown:
"还无法确认新搜索模型的索引规格,请重新测试连接。",
"embeddingSwitch.blocker.embedding_dimensions_unknown",
embedding_switch_requires_migration:
"旧版本数据仍在升级,请等待升级完成后再更换。",
sdk_reembed_unavailable: "当前安装版本还不能安全更新记忆索引。",
insufficient_disk_space: "本机剩余空间不足,无法安全建立新索引。",
embedding_switch_running: "另一次索引更新正在进行。",
memory_lifecycle_busy: "记忆正在整合,请等待完成后再更换搜索模型。",
"embeddingSwitch.blocker.embedding_switch_requires_migration",
sdk_reembed_unavailable: "embeddingSwitch.blocker.sdk_reembed_unavailable",
insufficient_disk_space: "embeddingSwitch.blocker.insufficient_disk_space",
embedding_switch_running: "embeddingSwitch.blocker.embedding_switch_running",
memory_lifecycle_busy: "embeddingSwitch.blocker.memory_lifecycle_busy",
};
const failureCopy: Record<string, string> = {
writes_did_not_drain: "等待正在进行的记忆整理结束超时。",
embedding_service_failed: "搜索模型服务连续请求失败。",
embedding_dimensions_mismatch: "搜索服务返回的索引规格与测试结果不一致。",
embedding_verification_failed: "新的记忆索引没有通过完整性校验。",
target_profile_changed: "更新期间搜索模型配置发生了变化。",
embedding_switch_cancelled: "索引更新已取消。",
model_secret_unavailable: "无法读取搜索服务的 API Key。",
secure_storage_unavailable: "系统安全存储暂时不可用。",
embedding_switch_clients_busy: "记忆服务一直被占用,暂时无法完成切换。",
const failureKeys: Record<string, MessageKey> = {
writes_did_not_drain: "embeddingSwitch.failure.writes_did_not_drain",
embedding_service_failed: "embeddingSwitch.failure.embedding_service_failed",
embedding_dimensions_mismatch:
"embeddingSwitch.failure.embedding_dimensions_mismatch",
embedding_verification_failed:
"embeddingSwitch.failure.embedding_verification_failed",
target_profile_changed: "embeddingSwitch.failure.target_profile_changed",
embedding_switch_cancelled:
"embeddingSwitch.failure.embedding_switch_cancelled",
model_secret_unavailable: "embeddingSwitch.failure.model_secret_unavailable",
secure_storage_unavailable:
"embeddingSwitch.failure.secure_storage_unavailable",
embedding_switch_clients_busy:
"embeddingSwitch.failure.embedding_switch_clients_busy",
};
export function EmbeddingSwitchConfirmDialog({
@@ -62,9 +80,10 @@ export function EmbeddingSwitchConfirmDialog({
onCancel(): void;
onStart(): void;
}) {
const { locale, t } = useGuiLanguage();
const panel = useRef<HTMLElement>(null);
const blockers = plan.blockers.map(
(code) => blockerCopy[code] ?? "当前状态暂时不允许更换搜索模型。",
const blockers = plan.blockers.map((code) =>
t(blockerKeys[code] ?? "embeddingSwitch.blocker.default"),
);
useEffect(() => {
@@ -113,78 +132,88 @@ export function EmbeddingSwitchConfirmDialog({
>
<header>
<div>
<h2 id="embedding-switch-title">更新记忆搜索索引?</h2>
<p>
不同搜索模型建立的索引不能混用。为了继续找到已有记忆,需要重新建立索引。
</p>
<h2 id="embedding-switch-title">
{t("embeddingSwitch.confirm.title")}
</h2>
<p>{t("embeddingSwitch.confirm.description")}</p>
</div>
</header>
<dl className="embedding-switch-impact">
<div>
<dt>已有记忆</dt>
<dt>{t("embeddingSwitch.impact.memories")}</dt>
<dd>
{count(plan.total_vectors)} 条
{t("embeddingSwitch.impact.memoryCount", {
count: count(locale, plan.total_vectors),
})}
{plan.users && plan.users > 1
? ` · ${count(plan.users)} 位用户`
? t("embeddingSwitch.impact.userCount", {
count: count(locale, plan.users),
})
: ""}
</dd>
</div>
<div>
<dt>预计耗时</dt>
<dd>{duration(plan.estimate_s)}</dd>
<dt>{t("embeddingSwitch.impact.duration")}</dt>
<dd>{duration(locale, t, plan.estimate_s)}</dd>
</div>
<div>
<dt>远程请求</dt>
<dt>{t("embeddingSwitch.impact.requests")}</dt>
<dd>
{plan.target_local
? "不会产生"
? t("embeddingSwitch.requests.none")
: plan.requests === null
? "按服务商计费"
: `约 ${count(plan.requests)} 次`}
? t("embeddingSwitch.requests.billed")
: t("embeddingSwitch.requests.approx", {
count: count(locale, plan.requests),
})}
</dd>
</div>
</dl>
<div className="embedding-switch-safety">
<strong>更新期间仍可搜索记忆</strong>
<p>
MemHub
会继续使用原来的搜索模型。新对话仍会保存,更新完成后再整理成记忆。
</p>
<strong>失败不会影响已有数据</strong>
<p>
只有全部更新并校验通过后才会切换;失败或取消时仍使用原来的模型。
</p>
<strong>{t("embeddingSwitch.safety.searchTitle")}</strong>
<p>{t("embeddingSwitch.safety.searchBody")}</p>
<strong>{t("embeddingSwitch.safety.dataTitle")}</strong>
<p>{t("embeddingSwitch.safety.dataBody")}</p>
</div>
<details className="embedding-switch-details">
<summary>
<ChevronDown size={14} aria-hidden="true" />
查看技术详情
{t("embeddingSwitch.details.title")}
</summary>
<dl>
<div>
<dt>当前搜索模型</dt>
<dd>{plan.source.model ?? "旧版本索引"}</dd>
</div>
<div>
<dt>新的搜索模型</dt>
<dd>{plan.target.model ?? "待确认"}</dd>
</div>
<div>
<dt>索引维度</dt>
<dt>{t("embeddingSwitch.details.currentModel")}</dt>
<dd>
{plan.source.dims ?? "未知"} → {plan.target.dims ?? "未知"}
{plan.source.model ?? t("embeddingSwitch.details.legacyIndex")}
</dd>
</div>
<div>
<dt>额外磁盘空间</dt>
<dd>{size(plan.disk_bytes_needed)}</dd>
<dt>{t("embeddingSwitch.details.newModel")}</dt>
<dd>
{plan.target.model ?? t("embeddingSwitch.details.pending")}
</dd>
</div>
<div>
<dt>旧索引保留</dt>
<dd>{plan.retention_days} 天</dd>
<dt>{t("embeddingSwitch.details.dimensions")}</dt>
<dd>
{plan.source.dims ?? t("embeddingSwitch.details.unknown")} →{" "}
{plan.target.dims ?? t("embeddingSwitch.details.unknown")}
</dd>
</div>
<div>
<dt>{t("embeddingSwitch.details.disk")}</dt>
<dd>{size(locale, plan.disk_bytes_needed)}</dd>
</div>
<div>
<dt>{t("embeddingSwitch.details.retention")}</dt>
<dd>
{t("embeddingSwitch.details.days", {
count: count(locale, plan.retention_days),
})}
</dd>
</div>
</dl>
</details>
@@ -200,15 +229,15 @@ export function EmbeddingSwitchConfirmDialog({
<CircleAlert size={16} aria-hidden="true" />
<span>
{errorCode === "embedding_switch_plan_stale"
? "记忆状态发生了变化,请重新检查更换影响。"
: "索引更新暂时无法开始,请稍后重试。"}
? t("embeddingSwitch.error.planStale")
: t("embeddingSwitch.error.startFailed")}
</span>
</div>
) : null}
<footer>
<R6Button disabled={busy} onClick={onCancel}>
暂不更换
{t("embeddingSwitch.action.notNow")}
</R6Button>
<R6Button
variant="primary"
@@ -216,7 +245,7 @@ export function EmbeddingSwitchConfirmDialog({
disabled={blockers.length > 0}
onClick={onStart}
>
开始更新索引
{t("embeddingSwitch.action.start")}
</R6Button>
</footer>
</section>
@@ -224,14 +253,14 @@ export function EmbeddingSwitchConfirmDialog({
);
}
function jobTitle(job: EmbeddingSwitchJob) {
if (job.kind === "merge") return "正在整理旧版本留下的搜索索引";
if (job.state === "preparing") return "正在准备更新记忆索引";
if (job.state === "reembedding") return "正在更新记忆索引";
if (job.state === "verifying") return "正在校验新的记忆索引";
if (job.state === "committing") return "正在启用新的搜索模型";
if (job.state === "failed") return "更新没有完成,正在恢复原设置";
return "记忆索引更新";
function jobTitle(t: Translator, job: EmbeddingSwitchJob) {
if (job.kind === "merge") return t("embeddingSwitch.job.merge");
if (job.state === "preparing") return t("embeddingSwitch.job.preparing");
if (job.state === "reembedding") return t("embeddingSwitch.job.reembedding");
if (job.state === "verifying") return t("embeddingSwitch.job.verifying");
if (job.state === "committing") return t("embeddingSwitch.job.committing");
if (job.state === "failed") return t("embeddingSwitch.job.failed");
return t("embeddingSwitch.job.default");
}
function canCancel(job: EmbeddingSwitchJob) {
@@ -247,30 +276,36 @@ function ActiveSwitchBanner({
onCancel(): void;
cancelling: boolean;
}) {
const { locale, t } = useGuiLanguage();
const job = current.job;
if (!job && current.phase !== "scanning") return null;
const percent = job?.progress.percent ?? 0;
return (
<aside className="embedding-switch-strip" aria-live="polite">
<div className="embedding-switch-strip-copy">
<strong>{job ? jobTitle(job) : "正在检查本机的记忆搜索索引"}</strong>
<strong>
{job ? jobTitle(t, job) : t("embeddingSwitch.job.scanning")}
</strong>
<span>
{job?.state === "failed"
? "原来的搜索模型仍在使用,记忆没有丢失。"
? t("embeddingSwitch.job.failedSafety")
: job
? `${Math.round(percent)}%${
? `${new Intl.NumberFormat(locale, {
style: "percent",
maximumFractionDigits: 0,
}).format(percent / 100)}${
job.progress.eta_s === null
? ""
: ` · ${duration(job.progress.eta_s)}`
: ` · ${duration(locale, t, job.progress.eta_s)}`
}`
: "完成前暂不整理新记忆,搜索仍可使用。"}
: t("embeddingSwitch.job.scanPaused")}
</span>
</div>
{job ? (
<div
className="embedding-switch-progress"
role="progressbar"
aria-label="记忆索引更新进度"
aria-label={t("embeddingSwitch.progress.aria")}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(percent)}
@@ -280,7 +315,7 @@ function ActiveSwitchBanner({
) : null}
{job && canCancel(job) ? (
<R6Button variant="quiet" pending={cancelling} onClick={onCancel}>
取消更新
{t("embeddingSwitch.action.cancel")}
</R6Button>
) : null}
</aside>
@@ -294,6 +329,7 @@ export function EmbeddingSwitchGlobalStatus({
onConfigureSearch(): void;
onDiagnostics(): void;
}) {
const { t } = useGuiLanguage();
const controller = useEmbeddingSwitch();
const [recent, setRecent] = useState<EmbeddingSwitchJob | null>(null);
const [cleaning, setCleaning] = useState(false);
@@ -332,11 +368,11 @@ export function EmbeddingSwitchGlobalStatus({
{controller.refreshFailed && controller.supported === true ? (
<aside className="embedding-switch-outcome is-error" role="status">
<div>
<strong>索引状态暂时无法读取</strong>
<span>当前显示可能不是最新状态,可以继续使用已有记忆。</span>
<strong>{t("embeddingSwitch.refresh.title")}</strong>
<span>{t("embeddingSwitch.refresh.body")}</span>
</div>
<R6Button variant="quiet" onClick={() => void controller.refresh()}>
重新读取
{t("embeddingSwitch.action.refresh")}
</R6Button>
</aside>
) : null}
@@ -344,15 +380,15 @@ export function EmbeddingSwitchGlobalStatus({
["failed", "unavailable"].includes(controller.current.scan_state) ? (
<aside className="embedding-switch-outcome is-error" role="alert">
<div>
<strong>旧版本搜索索引检查未完成</strong>
<strong>{t("embeddingSwitch.scan.title")}</strong>
<span>
{controller.current.writes_paused
? "新的记忆整理暂时暂停,已有记忆仍可搜索。"
: "记忆整理已经恢复;可以继续使用,诊断中保留了失败原因。"}
? t("embeddingSwitch.scan.paused")
: t("embeddingSwitch.scan.recovered")}
</span>
</div>
<R6Button variant="quiet" onClick={onDiagnostics}>
查看诊断
{t("embeddingSwitch.action.diagnostics")}
</R6Button>
</aside>
) : null}
@@ -363,8 +399,8 @@ export function EmbeddingSwitchGlobalStatus({
aria-live="polite"
>
<div>
<strong>搜索模型已更换</strong>
<span>暂停期间保存的新对话正在整理成记忆。</span>
<strong>{t("embeddingSwitch.success.title")}</strong>
<span>{t("embeddingSwitch.success.body")}</span>
</div>
<div className="embedding-switch-outcome-actions">
{recent.source.space_id ? (
@@ -387,12 +423,12 @@ export function EmbeddingSwitchGlobalStatus({
});
}}
>
清理旧索引
{t("embeddingSwitch.action.cleanup")}
</R6Button>
) : null}
<R6Button
variant="quiet"
aria-label="关闭完成提示"
aria-label={t("embeddingSwitch.action.closeSuccess")}
onClick={() => setRecent(null)}
>
<X size={16} aria-hidden="true" />
@@ -402,7 +438,7 @@ export function EmbeddingSwitchGlobalStatus({
) : null}
{cleanupError ? (
<p className="embedding-switch-cleanup-error" role="alert">
旧索引暂时无法清理,MemHub 会在保留期结束后自动重试。
{t("embeddingSwitch.cleanup.failed")}
</p>
) : null}
{recent?.state === "rolled_back" ? (
@@ -412,23 +448,26 @@ export function EmbeddingSwitchGlobalStatus({
aria-live="assertive"
>
<div>
<strong>索引更新没有完成</strong>
<strong>{t("embeddingSwitch.rollback.title")}</strong>
<span>
{failureCopy[recent.error_code ?? ""]
? `${failureCopy[recent.error_code ?? ""]} 仍在使用原来的搜索模型,记忆没有丢失。`
: "仍在使用原来的搜索模型,记忆没有丢失。"}
{failureKeys[recent.error_code ?? ""]
? t("embeddingSwitch.rollback.reason", {
reason: t(failureKeys[recent.error_code ?? ""]),
safety: t("embeddingSwitch.rollback.safety"),
})
: t("embeddingSwitch.rollback.safety")}
</span>
</div>
<div className="embedding-switch-outcome-actions">
<R6Button variant="quiet" onClick={onDiagnostics}>
查看诊断
{t("embeddingSwitch.action.diagnostics")}
</R6Button>
<R6Button variant="quiet" onClick={onConfigureSearch}>
检查连接
{t("embeddingSwitch.action.checkConnection")}
</R6Button>
<R6Button
variant="quiet"
aria-label="关闭失败提示"
aria-label={t("embeddingSwitch.action.closeFailure")}
onClick={() => setRecent(null)}
>
<X size={16} aria-hidden="true" />
@@ -331,3 +331,46 @@ it("keeps lite available while model status is unknown", async () => {
screen.getByRole("radio", { name: /深度记忆(ultra)/ }),
).toHaveProperty("disabled", true);
});
it("blocks mode and Evolve controls during embedding maintenance", async () => {
prepare("ultra");
vi.mocked(getMemoryProcessing).mockResolvedValue({
embedding_switch: { phase: "running" },
} as never);
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
expect(
await screen.findByText(
"正在检查或切换向量模型,完成前不能切换模式或手动整合。",
),
).toBeTruthy();
expect(
screen.getByRole("radio", { name: /基础记忆(lite)/ }),
).toHaveProperty("disabled", true);
expect(screen.getByRole("button", { name: "立即整合" })).toHaveProperty(
"disabled",
true,
);
});
it("blocks lifecycle controls while the knowledge graph is being built", async () => {
prepare("ultra");
vi.mocked(getMemoryMode).mockResolvedValue({
...state("ultra"),
graph_build: {
state: "running",
started_at: "2026-11-03T09:00:00Z",
finished_at: null,
users: {},
},
});
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
expect(
await screen.findByText("正在把已有高级记忆加入知识图谱…"),
).toBeTruthy();
expect(screen.getByRole("button", { name: "立即整合" })).toHaveProperty(
"disabled",
true,
);
});
+9 -8
View File
@@ -19,6 +19,7 @@ import {
} from "./api";
import { useOnboardingTranslation } from "./onboarding-language";
import { R6Button, R6Switch } from "./R6Controls";
import { R6StateRecovery } from "./R6StateRecovery";
const MODE_COPY: Record<
MemoryPipelineMode,
@@ -137,7 +138,7 @@ export function MemoryModeSettings({
onConfigureModel(message?: string): void;
onModeChange?(mode: MemoryPipelineMode): void;
}) {
const { t, text, locale } = useOnboardingTranslation();
const { text, locale } = useOnboardingTranslation();
const id = useId();
const [state, setState] = useState<MemoryModeState | null>(null);
const [schedule, setScheduleState] = useState<EvolveSchedule | null>(null);
@@ -424,16 +425,16 @@ export function MemoryModeSettings({
</p>
) : null}
{error && !pendingMode ? (
<div className="r6-state-recovery">
readFailed ? (
<R6StateRecovery
message={text(error)}
onRetry={() => void refresh()}
/>
) : (
<p className="r6-settings-error" role="alert">
{text(error)}
</p>
{readFailed ? (
<R6Button onClick={() => void refresh()}>
{t("startupRetry")}
</R6Button>
) : null}
</div>
)
) : null}
{state?.mode === "ultra" && schedule ? (
+7 -11
View File
@@ -20,6 +20,7 @@ import { EmbeddingSwitchConfirmDialog } from "./EmbeddingSwitchExperience";
import { OptionMenu } from "./OptionMenu";
import { useOnboardingTranslation } from "./onboarding-language";
import { PROVIDER_PRESETS } from "./provider-presets";
import { R6StateRecovery } from "./R6StateRecovery";
import { useEmbeddingSwitch } from "./use-embedding-switch";
import { useModelProfiles } from "./use-model-profiles";
import "./model-setup-next.css";
@@ -899,17 +900,12 @@ export function ModelSettings({
</p>
)}
{models.refreshError && (
<div className="model-refresh-notice" role="status">
<p>{text(models.refreshError)}</p>
<button
type="button"
className="secondary"
disabled={locked}
onClick={() => void models.refreshProfiles()}
>
{text("刷新配置列表")}
</button>
</div>
<R6StateRecovery
message={text(models.refreshError)}
pending={models.refreshing}
role="status"
onRetry={() => void models.refreshProfiles()}
/>
)}
{/* The paste-one-key path goes first when it exists. Behind it sits the
@@ -44,6 +44,22 @@ const models = {
activate: vi.fn(),
};
vi.mock("./use-model-profiles", () => ({ useModelProfiles: () => models }));
vi.mock("./use-embedding-switch", () => ({
useEmbeddingSwitch: () => ({
current: null,
plan: null,
busy: null,
errorCode: null,
refreshFailed: false,
supported: false,
planActivation: vi.fn(),
startPlannedSwitch: vi.fn(),
cancelCurrentSwitch: vi.fn(),
shouldUseLegacyActivation: () => true,
clearPlan: vi.fn(),
clearError: vi.fn(),
}),
}));
const source: SourceResponse = {
source_id: "cursor",
display_name: "Cursor",
@@ -41,6 +41,21 @@ const { models } = vi.hoisted(() => ({
vi.mock("./use-model-profiles", () => ({
useModelProfiles: () => models,
}));
vi.mock("./use-embedding-switch", () => ({
useEmbeddingSwitch: () => ({
current: null,
plan: null,
busy: null,
errorCode: null,
refreshFailed: false,
supported: false,
planActivation: vi.fn(),
startPlannedSwitch: vi.fn(),
shouldUseLegacyActivation: () => true,
clearPlan: vi.fn(),
clearError: vi.fn(),
}),
}));
vi.mock("./OnboardingModelStep", () => ({
OnboardingModelStep: (props: {
probe?: { message: string } | null;
+9 -12
View File
@@ -20,6 +20,7 @@ import {
} from "./ModelSettings";
import { useOnboardingTranslation } from "./onboarding-language";
import { R6Button, R6Switch } from "./R6Controls";
import { R6StateRecovery } from "./R6StateRecovery";
import {
refreshLoginPreference,
setLoginPreference,
@@ -446,18 +447,14 @@ export function R6Settings({
</div>
</div>
{modelError && (
<div className="r6-state-recovery">
<p role="alert" className="r6-settings-error">
{text(
profiles === null
? "模型状态未能读取,请重试。"
: "模型状态刷新未成功,仍显示上次确认的模型状态;请重试。",
)}
</p>
<R6Button onClick={() => setModelVersion((value) => value + 1)}>
{t("startupRetry")}
</R6Button>
</div>
<R6StateRecovery
message={text(
profiles === null
? "模型状态未能读取,请重试。"
: "模型状态刷新未成功,仍显示上次确认的模型状态;请重试。",
)}
onRetry={() => setModelVersion((value) => value + 1)}
/>
)}
{configurationRefusal && (
<p role="alert" className="r6-settings-error">
+26
View File
@@ -0,0 +1,26 @@
import { useOnboardingTranslation } from "./onboarding-language";
import { R6Button } from "./R6Controls";
export function R6StateRecovery({
message,
onRetry,
pending = false,
role = "alert",
}: {
message: string;
onRetry(): void;
pending?: boolean;
role?: "alert" | "status";
}) {
const { t } = useOnboardingTranslation();
return (
<div className="r6-state-recovery">
<p className="r6-settings-error" role={role}>
{message}
</p>
<R6Button pending={pending} onClick={onRetry}>
{t("startupRetry")}
</R6Button>
</div>
);
}
@@ -19,7 +19,7 @@ const api = vi.hoisted(() => ({
vi.mock("./api", () => api);
import { pollNow, resetPollScheduler } from "./poll-scheduler";
import { useEmbeddingSwitch } from "./use-embedding-switch";
import { useEmbeddingSwitchController as useEmbeddingSwitch } from "./use-embedding-switch";
const NOW = "2026-11-03T09:00:00Z";
const summary = (space_id: string | null) => ({
+34 -2
View File
@@ -3,7 +3,16 @@ import type {
EmbeddingSwitchJob,
EmbeddingSwitchPlan,
} from "@one-memory/contracts";
import { useCallback, useEffect, useRef, useState } from "react";
import {
createContext,
createElement,
type ReactNode,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import {
cancelEmbeddingSwitch,
embeddingSwitchRefusal,
@@ -47,7 +56,7 @@ async function readCurrentSequenced(): Promise<SequencedCurrent> {
* Reusable switch controller. It owns only transport-backed switch state;
* profile activation and product UI remain outside this module.
*/
export function useEmbeddingSwitch() {
export function useEmbeddingSwitchController() {
const [current, setCurrent] = useState<EmbeddingSwitchCurrent | null>(null);
const [plan, setPlan] = useState<EmbeddingSwitchPlan | null>(null);
const [busy, setBusy] = useState<EmbeddingSwitchBusy>(null);
@@ -265,3 +274,26 @@ export function useEmbeddingSwitch() {
clearError: () => setErrorCode(null),
};
}
export type EmbeddingSwitchController = ReturnType<
typeof useEmbeddingSwitchController
>;
const EmbeddingSwitchContext = createContext<EmbeddingSwitchController | null>(
null,
);
export function EmbeddingSwitchProvider({ children }: { children: ReactNode }) {
const controller = useEmbeddingSwitchController();
return createElement(
EmbeddingSwitchContext.Provider,
{ value: controller },
children,
);
}
export function useEmbeddingSwitch(): EmbeddingSwitchController {
const controller = useContext(EmbeddingSwitchContext);
if (!controller) throw new Error("embedding_switch_provider_missing");
return controller;
}
@@ -605,6 +605,7 @@ it("prevents an older refresh from overwriting discard's profile snapshot", asyn
refreshing = hook.result.current.refreshProfiles();
});
await waitFor(() => expect(api.getModelProfiles).toHaveBeenCalledTimes(2));
expect(hook.result.current.refreshing).toBe(true);
await act(async () => {
expect(await hook.result.current.discardMissingCreation()).toBeUndefined();
});
@@ -614,6 +615,7 @@ it("prevents an older refresh from overwriting discard's profile snapshot", asyn
await refreshing;
});
expect(hook.result.current.profiles).toEqual([profile]);
expect(hook.result.current.refreshing).toBe(false);
expect(readPendingModelCreation()).toBe(ID);
});
+10
View File
@@ -82,6 +82,7 @@ export function useModelProfiles() {
const [busy, setBusy] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [refreshError, setRefreshError] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
const [pendingCreation, setPendingCreation] = useState<string | null>(null);
const [creationMissing, setCreationMissingState] = useState<string | null>(
null,
@@ -107,6 +108,7 @@ export function useModelProfiles() {
const alive = useRef(false);
const session = useRef(0);
const readVersion = useRef(0);
const refreshingReads = useRef(0);
const flight = useRef<{ key: string; promise: Promise<unknown> } | null>(
null,
);
@@ -119,6 +121,8 @@ export function useModelProfiles() {
async (afterOperation = false) => {
const version = ++readVersion.current;
const owner = session.current;
refreshingReads.current++;
setRefreshing(true);
try {
const next = await getModelProfiles();
if (
@@ -141,6 +145,10 @@ export function useModelProfiles() {
? "操作已完成,但配置列表暂未刷新。已保留操作回执,请刷新核对,不要重复保存。"
: "暂时无法刷新模型配置,已确认的配置仍保留。请重试刷新。",
);
} finally {
refreshingReads.current = Math.max(0, refreshingReads.current - 1);
if (alive.current && refreshingReads.current === 0)
setRefreshing(false);
}
},
[setProfiles],
@@ -165,6 +173,7 @@ export function useModelProfiles() {
alive.current = false;
session.current++;
readVersion.current++;
refreshingReads.current = 0;
flight.current = null;
creationMissingRef.current = null;
};
@@ -336,6 +345,7 @@ export function useModelProfiles() {
busy,
error,
refreshError,
refreshing,
pendingCreation,
creationMissing,
pendingProbe,
@@ -0,0 +1,985 @@
export const embeddingSwitchChineseMessages = {
"embeddingSwitch.confirm.title": "更新记忆搜索索引?",
"embeddingSwitch.confirm.description":
"不同搜索模型建立的索引不能混用。为了继续找到已有记忆,需要重新建立索引。",
"embeddingSwitch.impact.memories": "已有记忆",
"embeddingSwitch.impact.memoryCount": "{count} 条",
"embeddingSwitch.impact.userCount": " · {count} 位用户",
"embeddingSwitch.impact.duration": "预计耗时",
"embeddingSwitch.duration.unknown": "暂时无法估算",
"embeddingSwitch.duration.seconds": "约 {count} 秒",
"embeddingSwitch.duration.minutes": "约 {count} 分钟",
"embeddingSwitch.impact.requests": "远程请求",
"embeddingSwitch.requests.none": "不会产生",
"embeddingSwitch.requests.billed": "按服务商计费",
"embeddingSwitch.requests.approx": "约 {count} 次",
"embeddingSwitch.safety.searchTitle": "更新期间仍可搜索记忆",
"embeddingSwitch.safety.searchBody":
"MemHub 会继续使用原来的搜索模型。新对话仍会保存,更新完成后再整理成记忆。",
"embeddingSwitch.safety.dataTitle": "失败不会影响已有数据",
"embeddingSwitch.safety.dataBody":
"只有全部更新并校验通过后才会切换;失败或取消时仍使用原来的模型。",
"embeddingSwitch.details.title": "查看技术详情",
"embeddingSwitch.details.currentModel": "当前搜索模型",
"embeddingSwitch.details.legacyIndex": "旧版本索引",
"embeddingSwitch.details.newModel": "新的搜索模型",
"embeddingSwitch.details.pending": "待确认",
"embeddingSwitch.details.dimensions": "索引维度",
"embeddingSwitch.details.unknown": "未知",
"embeddingSwitch.details.disk": "额外磁盘空间",
"embeddingSwitch.details.retention": "旧索引保留",
"embeddingSwitch.details.days": "{count} 天",
"embeddingSwitch.blocker.embedding_probe_required":
"请先测试新的搜索模型连接。",
"embeddingSwitch.blocker.embedding_dimensions_unknown":
"还无法确认新搜索模型的索引规格,请重新测试连接。",
"embeddingSwitch.blocker.embedding_switch_requires_migration":
"旧版本数据仍在升级,请等待升级完成后再更换。",
"embeddingSwitch.blocker.sdk_reembed_unavailable":
"当前安装版本还不能安全更新记忆索引。",
"embeddingSwitch.blocker.insufficient_disk_space":
"本机剩余空间不足,无法安全建立新索引。",
"embeddingSwitch.blocker.embedding_switch_running":
"另一次索引更新正在进行。",
"embeddingSwitch.blocker.memory_lifecycle_busy":
"记忆正在整合,请等待完成后再更换搜索模型。",
"embeddingSwitch.blocker.default": "当前状态暂时不允许更换搜索模型。",
"embeddingSwitch.error.planStale": "记忆状态发生了变化,请重新检查更换影响。",
"embeddingSwitch.error.startFailed": "索引更新暂时无法开始,请稍后重试。",
"embeddingSwitch.action.notNow": "暂不更换",
"embeddingSwitch.action.start": "开始更新索引",
"embeddingSwitch.job.merge": "正在整理旧版本留下的搜索索引",
"embeddingSwitch.job.preparing": "正在准备更新记忆索引",
"embeddingSwitch.job.reembedding": "正在更新记忆索引",
"embeddingSwitch.job.verifying": "正在校验新的记忆索引",
"embeddingSwitch.job.committing": "正在启用新的搜索模型",
"embeddingSwitch.job.failed": "更新没有完成,正在恢复原设置",
"embeddingSwitch.job.default": "记忆索引更新",
"embeddingSwitch.job.scanning": "正在检查本机的记忆搜索索引",
"embeddingSwitch.job.failedSafety": "原来的搜索模型仍在使用,记忆没有丢失。",
"embeddingSwitch.job.scanPaused": "完成前暂不整理新记忆,搜索仍可使用。",
"embeddingSwitch.progress.aria": "记忆索引更新进度",
"embeddingSwitch.action.cancel": "取消更新",
"embeddingSwitch.refresh.title": "索引状态暂时无法读取",
"embeddingSwitch.refresh.body":
"当前显示可能不是最新状态,可以继续使用已有记忆。",
"embeddingSwitch.action.refresh": "重新读取",
"embeddingSwitch.scan.title": "旧版本搜索索引检查未完成",
"embeddingSwitch.scan.paused": "新的记忆整理暂时暂停,已有记忆仍可搜索。",
"embeddingSwitch.scan.recovered":
"记忆整理已经恢复;可以继续使用,诊断中保留了失败原因。",
"embeddingSwitch.action.diagnostics": "查看诊断",
"embeddingSwitch.success.title": "搜索模型已更换",
"embeddingSwitch.success.body": "暂停期间保存的新对话正在整理成记忆。",
"embeddingSwitch.action.cleanup": "清理旧索引",
"embeddingSwitch.action.closeSuccess": "关闭完成提示",
"embeddingSwitch.cleanup.failed":
"旧索引暂时无法清理,MemHub 会在保留期结束后自动重试。",
"embeddingSwitch.rollback.title": "索引更新没有完成",
"embeddingSwitch.rollback.safety": "仍在使用原来的搜索模型,记忆没有丢失。",
"embeddingSwitch.failure.writes_did_not_drain":
"等待正在进行的记忆整理结束超时。",
"embeddingSwitch.failure.embedding_service_failed":
"搜索模型服务连续请求失败。",
"embeddingSwitch.failure.embedding_dimensions_mismatch":
"搜索服务返回的索引规格与测试结果不一致。",
"embeddingSwitch.failure.embedding_verification_failed":
"新的记忆索引没有通过完整性校验。",
"embeddingSwitch.failure.target_profile_changed":
"更新期间搜索模型配置发生了变化。",
"embeddingSwitch.failure.embedding_switch_cancelled": "索引更新已取消。",
"embeddingSwitch.failure.model_secret_unavailable":
"无法读取搜索服务的 API Key。",
"embeddingSwitch.failure.secure_storage_unavailable":
"系统安全存储暂时不可用。",
"embeddingSwitch.failure.embedding_switch_clients_busy":
"记忆服务一直被占用,暂时无法完成切换。",
"embeddingSwitch.rollback.reason": "{reason} {safety}",
"embeddingSwitch.action.checkConnection": "检查连接",
"embeddingSwitch.action.closeFailure": "关闭失败提示",
} as const;
type EmbeddingSwitchMessages = Record<
keyof typeof embeddingSwitchChineseMessages,
string
>;
export const embeddingSwitchEnglishMessages: EmbeddingSwitchMessages = {
"embeddingSwitch.confirm.title": "Update the memory search index?",
"embeddingSwitch.confirm.description":
"Indexes created by different search models cannot be mixed. Rebuild the index to keep finding existing memories.",
"embeddingSwitch.impact.memories": "Existing memories",
"embeddingSwitch.impact.memoryCount": "{count}",
"embeddingSwitch.impact.userCount": " · Users: {count}",
"embeddingSwitch.impact.duration": "Estimated time",
"embeddingSwitch.duration.unknown": "Cannot estimate yet",
"embeddingSwitch.duration.seconds": "About {count} sec",
"embeddingSwitch.duration.minutes": "About {count} min",
"embeddingSwitch.impact.requests": "Remote requests",
"embeddingSwitch.requests.none": "None",
"embeddingSwitch.requests.billed": "Billed by provider",
"embeddingSwitch.requests.approx": "About {count}",
"embeddingSwitch.safety.searchTitle":
"Memories stay searchable during the update",
"embeddingSwitch.safety.searchBody":
"MemHub keeps using the current search model. New conversations are saved and turned into memories after the update.",
"embeddingSwitch.safety.dataTitle": "A failure will not affect existing data",
"embeddingSwitch.safety.dataBody":
"The model changes only after the update and verification finish. The current model stays active if the update fails or is cancelled.",
"embeddingSwitch.details.title": "View technical details",
"embeddingSwitch.details.currentModel": "Current search model",
"embeddingSwitch.details.legacyIndex": "Legacy index",
"embeddingSwitch.details.newModel": "New search model",
"embeddingSwitch.details.pending": "Pending",
"embeddingSwitch.details.dimensions": "Index dimensions",
"embeddingSwitch.details.unknown": "Unknown",
"embeddingSwitch.details.disk": "Additional disk space",
"embeddingSwitch.details.retention": "Keep old index",
"embeddingSwitch.details.days": "{count} días",
"embeddingSwitch.blocker.embedding_probe_required":
"Test the new search model connection first.",
"embeddingSwitch.blocker.embedding_dimensions_unknown":
"The new search model's index dimensions are not confirmed. Test the connection again.",
"embeddingSwitch.blocker.embedding_switch_requires_migration":
"Legacy data is still being upgraded. Wait for the upgrade to finish.",
"embeddingSwitch.blocker.sdk_reembed_unavailable":
"This installation cannot safely update the memory index.",
"embeddingSwitch.blocker.insufficient_disk_space":
"There is not enough free disk space to build the new index safely.",
"embeddingSwitch.blocker.embedding_switch_running":
"Another index update is already running.",
"embeddingSwitch.blocker.memory_lifecycle_busy":
"Memories are being consolidated. Wait for it to finish before changing the search model.",
"embeddingSwitch.blocker.default":
"The search model cannot be changed in the current state.",
"embeddingSwitch.error.planStale":
"The memory state changed. Review the impact again.",
"embeddingSwitch.error.startFailed":
"The index update cannot start right now. Try again later.",
"embeddingSwitch.action.notNow": "Not now",
"embeddingSwitch.action.start": "Start index update",
"embeddingSwitch.job.merge": "Consolidating a legacy search index",
"embeddingSwitch.job.preparing": "Preparing the memory index update",
"embeddingSwitch.job.reembedding": "Updating the memory index",
"embeddingSwitch.job.verifying": "Verifying the new memory index",
"embeddingSwitch.job.committing": "Enabling the new search model",
"embeddingSwitch.job.failed":
"Restoring previous settings after an incomplete update",
"embeddingSwitch.job.default": "Memory index update",
"embeddingSwitch.job.scanning": "Checking local memory search indexes",
"embeddingSwitch.job.failedSafety":
"The previous search model is still active. No memories were lost.",
"embeddingSwitch.job.scanPaused":
"New memories will not be consolidated until this finishes. Search is still available.",
"embeddingSwitch.progress.aria": "Memory index update progress",
"embeddingSwitch.action.cancel": "Cancel update",
"embeddingSwitch.refresh.title": "Index status is temporarily unavailable",
"embeddingSwitch.refresh.body":
"This status may be out of date. Existing memories remain available.",
"embeddingSwitch.action.refresh": "Reload",
"embeddingSwitch.scan.title": "Legacy search index check did not finish",
"embeddingSwitch.scan.paused":
"New memory consolidation is paused. Existing memories remain searchable.",
"embeddingSwitch.scan.recovered":
"Memory consolidation has resumed. You can keep using MemHub; diagnostics retain the failure reason.",
"embeddingSwitch.action.diagnostics": "View diagnostics",
"embeddingSwitch.success.title": "Search model changed",
"embeddingSwitch.success.body":
"Conversations saved during the pause are being turned into memories.",
"embeddingSwitch.action.cleanup": "Clean up old index",
"embeddingSwitch.action.closeSuccess": "Dismiss completion message",
"embeddingSwitch.cleanup.failed":
"The old index cannot be removed right now. MemHub will retry after the retention period.",
"embeddingSwitch.rollback.title": "Index update did not finish",
"embeddingSwitch.rollback.safety":
"The previous search model is still active. No memories were lost.",
"embeddingSwitch.failure.writes_did_not_drain":
"Timed out while waiting for memory consolidation to finish.",
"embeddingSwitch.failure.embedding_service_failed":
"The search model service failed repeatedly.",
"embeddingSwitch.failure.embedding_dimensions_mismatch":
"The search service returned index dimensions that differ from the test result.",
"embeddingSwitch.failure.embedding_verification_failed":
"The new memory index failed integrity verification.",
"embeddingSwitch.failure.target_profile_changed":
"The search model configuration changed during the update.",
"embeddingSwitch.failure.embedding_switch_cancelled":
"The index update was cancelled.",
"embeddingSwitch.failure.model_secret_unavailable":
"The search service API key could not be read.",
"embeddingSwitch.failure.secure_storage_unavailable":
"Secure system storage is temporarily unavailable.",
"embeddingSwitch.failure.embedding_switch_clients_busy":
"The memory service remained busy, so the change could not finish.",
"embeddingSwitch.rollback.reason": "{reason} {safety}",
"embeddingSwitch.action.checkConnection": "Check connection",
"embeddingSwitch.action.closeFailure": "Dismiss failure message",
};
export const embeddingSwitchSpanishMessages: EmbeddingSwitchMessages = {
"embeddingSwitch.confirm.title":
"¿Actualizar el índice de búsqueda de memorias?",
"embeddingSwitch.confirm.description":
"Los índices creados por modelos de búsqueda distintos no se pueden combinar. Hay que reconstruir el índice para seguir encontrando las memorias existentes.",
"embeddingSwitch.impact.memories": "Memorias existentes",
"embeddingSwitch.impact.memoryCount": "{count}",
"embeddingSwitch.impact.userCount": " · Usuarios: {count}",
"embeddingSwitch.impact.duration": "Tiempo estimado",
"embeddingSwitch.duration.unknown": "Aún no se puede estimar",
"embeddingSwitch.duration.seconds": "Aprox. {count} s",
"embeddingSwitch.duration.minutes": "Aprox. {count} min",
"embeddingSwitch.impact.requests": "Solicitudes remotas",
"embeddingSwitch.requests.none": "Ninguna",
"embeddingSwitch.requests.billed": "Facturadas por el proveedor",
"embeddingSwitch.requests.approx": "Aprox. {count}",
"embeddingSwitch.safety.searchTitle":
"Las memorias seguirán disponibles durante la actualización",
"embeddingSwitch.safety.searchBody":
"MemHub seguirá usando el modelo de búsqueda actual. Las conversaciones nuevas se guardarán y se convertirán en memorias después de la actualización.",
"embeddingSwitch.safety.dataTitle":
"Un fallo no afectará a los datos existentes",
"embeddingSwitch.safety.dataBody":
"El modelo solo cambiará cuando terminen la actualización y la verificación. Si falla o se cancela, se seguirá usando el modelo actual.",
"embeddingSwitch.details.title": "Ver detalles técnicos",
"embeddingSwitch.details.currentModel": "Modelo de búsqueda actual",
"embeddingSwitch.details.legacyIndex": "Índice anterior",
"embeddingSwitch.details.newModel": "Nuevo modelo de búsqueda",
"embeddingSwitch.details.pending": "Pendiente",
"embeddingSwitch.details.dimensions": "Dimensiones del índice",
"embeddingSwitch.details.unknown": "Desconocido",
"embeddingSwitch.details.disk": "Espacio adicional en disco",
"embeddingSwitch.details.retention": "Conservar índice anterior",
"embeddingSwitch.details.days": "{count} d",
"embeddingSwitch.blocker.embedding_probe_required":
"Prueba primero la conexión del nuevo modelo de búsqueda.",
"embeddingSwitch.blocker.embedding_dimensions_unknown":
"No se han confirmado las dimensiones del índice del nuevo modelo. Prueba otra vez la conexión.",
"embeddingSwitch.blocker.embedding_switch_requires_migration":
"Los datos anteriores aún se están actualizando. Espera a que termine.",
"embeddingSwitch.blocker.sdk_reembed_unavailable":
"Esta instalación no puede actualizar el índice de memorias de forma segura.",
"embeddingSwitch.blocker.insufficient_disk_space":
"No hay suficiente espacio libre para crear el índice nuevo de forma segura.",
"embeddingSwitch.blocker.embedding_switch_running":
"Ya hay otra actualización del índice en curso.",
"embeddingSwitch.blocker.memory_lifecycle_busy":
"Las memorias se están consolidando. Espera a que termine antes de cambiar el modelo.",
"embeddingSwitch.blocker.default":
"No se puede cambiar el modelo de búsqueda en el estado actual.",
"embeddingSwitch.error.planStale":
"El estado de las memorias ha cambiado. Revisa de nuevo el impacto.",
"embeddingSwitch.error.startFailed":
"No se puede iniciar ahora la actualización del índice. Inténtalo más tarde.",
"embeddingSwitch.action.notNow": "Ahora no",
"embeddingSwitch.action.start": "Iniciar actualización",
"embeddingSwitch.job.merge": "Consolidando un índice de búsqueda anterior",
"embeddingSwitch.job.preparing": "Preparando la actualización del índice",
"embeddingSwitch.job.reembedding": "Actualizando el índice de memorias",
"embeddingSwitch.job.verifying": "Verificando el nuevo índice",
"embeddingSwitch.job.committing": "Activando el nuevo modelo de búsqueda",
"embeddingSwitch.job.failed": "Restaurando la configuración anterior",
"embeddingSwitch.job.default": "Actualización del índice de memorias",
"embeddingSwitch.job.scanning": "Comprobando los índices locales de memorias",
"embeddingSwitch.job.failedSafety":
"El modelo anterior sigue activo. No se ha perdido ninguna memoria.",
"embeddingSwitch.job.scanPaused":
"No se consolidarán memorias nuevas hasta que termine. La búsqueda sigue disponible.",
"embeddingSwitch.progress.aria": "Progreso de actualización del índice",
"embeddingSwitch.action.cancel": "Cancelar actualización",
"embeddingSwitch.refresh.title": "El estado del índice no está disponible",
"embeddingSwitch.refresh.body":
"Este estado puede no estar actualizado. Las memorias existentes siguen disponibles.",
"embeddingSwitch.action.refresh": "Volver a cargar",
"embeddingSwitch.scan.title":
"No terminó la comprobación del índice anterior",
"embeddingSwitch.scan.paused":
"La consolidación de memorias nuevas está pausada. Las existentes siguen disponibles.",
"embeddingSwitch.scan.recovered":
"La consolidación se ha reanudado. Puedes seguir usando MemHub; el motivo del fallo se conserva en el diagnóstico.",
"embeddingSwitch.action.diagnostics": "Ver diagnóstico",
"embeddingSwitch.success.title": "Modelo de búsqueda cambiado",
"embeddingSwitch.success.body":
"Las conversaciones guardadas durante la pausa se están convirtiendo en memorias.",
"embeddingSwitch.action.cleanup": "Eliminar índice anterior",
"embeddingSwitch.action.closeSuccess": "Cerrar aviso de finalización",
"embeddingSwitch.cleanup.failed":
"No se puede eliminar ahora el índice anterior. MemHub lo reintentará al terminar el periodo de conservación.",
"embeddingSwitch.rollback.title": "La actualización no terminó",
"embeddingSwitch.rollback.safety":
"El modelo anterior sigue activo. No se ha perdido ninguna memoria.",
"embeddingSwitch.failure.writes_did_not_drain":
"Se agotó el tiempo de espera para terminar la consolidación.",
"embeddingSwitch.failure.embedding_service_failed":
"El servicio del modelo de búsqueda falló repetidamente.",
"embeddingSwitch.failure.embedding_dimensions_mismatch":
"El servicio devolvió dimensiones distintas del resultado de la prueba.",
"embeddingSwitch.failure.embedding_verification_failed":
"El nuevo índice no superó la verificación de integridad.",
"embeddingSwitch.failure.target_profile_changed":
"La configuración del modelo cambió durante la actualización.",
"embeddingSwitch.failure.embedding_switch_cancelled":
"Se canceló la actualización del índice.",
"embeddingSwitch.failure.model_secret_unavailable":
"No se pudo leer la clave API del servicio de búsqueda.",
"embeddingSwitch.failure.secure_storage_unavailable":
"El almacenamiento seguro del sistema no está disponible.",
"embeddingSwitch.failure.embedding_switch_clients_busy":
"El servicio de memorias siguió ocupado y no se pudo completar el cambio.",
"embeddingSwitch.rollback.reason": "{reason} {safety}",
"embeddingSwitch.action.checkConnection": "Comprobar conexión",
"embeddingSwitch.action.closeFailure": "Cerrar aviso de error",
};
const embeddingSwitchKeys = Object.keys(
embeddingSwitchEnglishMessages,
) as (keyof EmbeddingSwitchMessages)[];
const localized = (values: readonly string[]): EmbeddingSwitchMessages => {
if (values.length !== embeddingSwitchKeys.length)
throw new Error(
`Expected ${embeddingSwitchKeys.length} embedding switch messages, got ${values.length}`,
);
return Object.fromEntries(
embeddingSwitchKeys.map((key, index) => [key, values[index]]),
) as EmbeddingSwitchMessages;
};
export const embeddingSwitchTraditionalChineseMessages = localized([
"更新記憶搜尋索引?",
"不同搜尋模型建立的索引不能混用。若要繼續找到現有記憶,必須重新建立索引。",
"現有記憶",
"{count} 條",
" · {count} 位使用者",
"預計耗時",
"暫時無法估算",
"約 {count} 秒",
"約 {count} 分鐘",
"遠端請求",
"不會產生",
"由服務供應商計費",
"約 {count} 次",
"更新期間仍可搜尋記憶",
"MemHub 會繼續使用目前的搜尋模型。新對話仍會儲存,並在更新後轉換為記憶。",
"更新失敗不會影響現有資料",
"只有完成更新及驗證後才會切換模型。若更新失敗或取消,仍會使用目前的模型。",
"檢視技術詳細資料",
"目前的搜尋模型",
"舊版索引",
"新的搜尋模型",
"等待處理",
"索引維度",
"未知",
"額外磁碟空間",
"保留舊索引",
"{count} 天",
"請先測試新搜尋模型的連線。",
"尚未確認新搜尋模型的索引維度,請重新測試連線。",
"舊版資料仍在升級,請等待升級完成。",
"目前的安裝版本無法安全更新記憶索引。",
"可用磁碟空間不足,無法安全建立新索引。",
"另一項索引更新已在進行中。",
"系統正在整合記憶,請等候完成後再更換搜尋模型。",
"目前狀態不允許更換搜尋模型。",
"記憶狀態已變更,請重新檢查影響。",
"目前無法開始更新索引,請稍後再試。",
"暫時不要",
"開始更新索引",
"正在整合舊版搜尋索引",
"正在準備更新記憶索引",
"正在更新記憶索引",
"正在驗證新的記憶索引",
"正在啟用新的搜尋模型",
"更新未完成,正在還原先前設定",
"記憶索引更新",
"正在檢查本機記憶搜尋索引",
"先前的搜尋模型仍在使用,沒有遺失任何記憶。",
"完成前不會整合新記憶,但搜尋功能仍可使用。",
"記憶索引更新進度",
"取消更新",
"暫時無法取得索引狀態",
"此狀態可能不是最新資訊,現有記憶仍可使用。",
"重新載入",
"舊版搜尋索引檢查未完成",
"新記憶的整合作業已暫停,現有記憶仍可搜尋。",
"記憶整合作業已恢復。你可以繼續使用 MemHub;診斷資訊會保留失敗原因。",
"檢視診斷資訊",
"搜尋模型已更換",
"暫停期間儲存的對話正在轉換為記憶。",
"清理舊索引",
"關閉完成訊息",
"目前無法移除舊索引。MemHub 會在保留期限結束後重試。",
"索引更新未完成",
"先前的搜尋模型仍在使用,沒有遺失任何記憶。",
"等待記憶整合作業完成時逾時。",
"搜尋模型服務連續發生錯誤。",
"搜尋服務傳回的索引維度與測試結果不符。",
"新的記憶索引未通過完整性驗證。",
"更新期間搜尋模型設定已變更。",
"索引更新已取消。",
"無法讀取搜尋服務的 API Key。",
"系統的安全儲存空間暫時無法使用。",
"記憶服務持續忙碌,因此無法完成更換。",
"{reason} {safety}",
"檢查連線",
"關閉失敗訊息",
]);
export const embeddingSwitchJapaneseMessages = localized([
"メモリ検索インデックスを更新しますか?",
"異なる検索モデルで作成したインデックスは併用できません。既存のメモリを引き続き検索できるよう、インデックスを再構築します。",
"既存のメモリ",
"{count}",
" · ユーザー数: {count}",
"推定所要時間",
"まだ推定できません",
"約 {count} 秒",
"約 {count} 分",
"リモートリクエスト",
"発生しません",
"プロバイダーによる課金",
"約 {count} 回",
"更新中もメモリを検索できます",
"MemHub は現在の検索モデルを引き続き使用します。新しい会話は保存され、更新後にメモリへ変換されます。",
"失敗しても既存のデータには影響しません",
"更新と検証が完了してからモデルを切り替えます。失敗またはキャンセルした場合は、現在のモデルを使い続けます。",
"技術情報を表示",
"現在の検索モデル",
"旧形式のインデックス",
"新しい検索モデル",
"保留中",
"インデックスの次元数",
"不明",
"追加のディスク容量",
"旧インデックスの保持期間",
"{count} 日",
"先に新しい検索モデルへの接続をテストしてください。",
"新しい検索モデルのインデックス次元数を確認できません。もう一度接続をテストしてください。",
"旧形式のデータをアップグレード中です。完了するまでお待ちください。",
"このインストール環境では、メモリインデックスを安全に更新できません。",
"新しいインデックスを安全に作成するための空き容量が不足しています。",
"別のインデックス更新がすでに実行中です。",
"メモリを統合中です。完了してから検索モデルを変更してください。",
"現在の状態では検索モデルを変更できません。",
"メモリの状態が変わりました。影響をもう一度確認してください。",
"現在はインデックス更新を開始できません。しばらくしてから再試行してください。",
"今はしない",
"インデックス更新を開始",
"旧形式の検索インデックスを統合中",
"メモリインデックスの更新を準備中",
"メモリインデックスを更新中",
"新しいメモリインデックスを検証中",
"新しい検索モデルを有効化中",
"未完了の更新から以前の設定を復元中",
"メモリインデックスの更新",
"ローカルのメモリ検索インデックスを確認中",
"以前の検索モデルが引き続き有効です。メモリは失われていません。",
"完了するまで新しいメモリは統合されません。検索は引き続き利用できます。",
"メモリインデックスの更新状況",
"更新をキャンセル",
"インデックスの状態を一時的に取得できません",
"表示されている状態は最新でない可能性があります。既存のメモリは引き続き利用できます。",
"再読み込み",
"旧形式の検索インデックスの確認が完了しませんでした",
"新しいメモリの統合は一時停止中です。既存のメモリは引き続き検索できます。",
"メモリの統合を再開しました。MemHub はそのまま利用できます。失敗理由は診断情報に保持されます。",
"診断情報を表示",
"検索モデルを変更しました",
"一時停止中に保存された会話をメモリへ変換しています。",
"旧インデックスを削除",
"完了メッセージを閉じる",
"現在は旧インデックスを削除できません。保持期間の終了後に MemHub が再試行します。",
"インデックスの更新が完了しませんでした",
"以前の検索モデルが引き続き有効です。メモリは失われていません。",
"メモリの統合完了を待機中にタイムアウトしました。",
"検索モデルのサービスでエラーが繰り返し発生しました。",
"検索サービスから返されたインデックス次元数がテスト結果と一致しません。",
"新しいメモリインデックスは整合性検証に合格しませんでした。",
"更新中に検索モデルの設定が変更されました。",
"インデックスの更新をキャンセルしました。",
"検索サービスの API Key を読み取れませんでした。",
"システムの安全なストレージを一時的に利用できません。",
"メモリサービスが使用中のままだったため、変更を完了できませんでした。",
"{reason} {safety}",
"接続を確認",
"失敗メッセージを閉じる",
]);
export const embeddingSwitchGermanMessages = localized([
"Speichersuchindex aktualisieren?",
"Indizes verschiedener Suchmodelle lassen sich nicht kombinieren. Der Index wird neu erstellt, damit vorhandene Erinnerungen weiterhin auffindbar sind.",
"Vorhandene Erinnerungen",
"{count}",
" · Benutzer: {count}",
"Geschätzte Dauer",
"Noch nicht abschätzbar",
"Etwa {count} Sek.",
"Etwa {count} Min.",
"Remote-Anfragen",
"Keine",
"Abrechnung durch den Anbieter",
"Etwa {count}",
"Erinnerungen bleiben während der Aktualisierung durchsuchbar",
"MemHub verwendet weiterhin das aktuelle Suchmodell. Neue Unterhaltungen werden gespeichert und nach der Aktualisierung in Erinnerungen umgewandelt.",
"Ein Fehler beeinträchtigt vorhandene Daten nicht",
"Das Modell wird erst nach erfolgreicher Aktualisierung und Prüfung gewechselt. Bei einem Fehler oder Abbruch bleibt das aktuelle Modell aktiv.",
"Technische Details anzeigen",
"Aktuelles Suchmodell",
"Alter Index",
"Neues Suchmodell",
"Ausstehend",
"Indexdimensionen",
"Unbekannt",
"Zusätzlicher Speicherplatz",
"Alten Index aufbewahren",
"{count} Tg.",
"Testen Sie zuerst die Verbindung zum neuen Suchmodell.",
"Die Indexdimensionen des neuen Suchmodells sind nicht bestätigt. Testen Sie die Verbindung erneut.",
"Altdaten werden noch aktualisiert. Warten Sie, bis der Vorgang abgeschlossen ist.",
"Diese Installation kann den Speicherindex nicht sicher aktualisieren.",
"Zum sicheren Erstellen des neuen Index ist nicht genügend Speicherplatz verfügbar.",
"Eine weitere Indexaktualisierung wird bereits ausgeführt.",
"Erinnerungen werden gerade zusammengeführt. Warten Sie vor dem Modellwechsel, bis der Vorgang beendet ist.",
"Im aktuellen Zustand kann das Suchmodell nicht geändert werden.",
"Der Speicherstatus hat sich geändert. Prüfen Sie die Auswirkungen erneut.",
"Die Indexaktualisierung kann derzeit nicht gestartet werden. Versuchen Sie es später erneut.",
"Nicht jetzt",
"Index aktualisieren",
"Alter Suchindex wird zusammengeführt",
"Aktualisierung des Speicherindex wird vorbereitet",
"Speicherindex wird aktualisiert",
"Neuer Speicherindex wird geprüft",
"Neues Suchmodell wird aktiviert",
"Vorherige Einstellungen werden nach unvollständiger Aktualisierung wiederhergestellt",
"Speicherindex-Aktualisierung",
"Lokale Speichersuchindizes werden geprüft",
"Das vorherige Suchmodell ist weiterhin aktiv. Es gingen keine Erinnerungen verloren.",
"Neue Erinnerungen werden erst danach zusammengeführt. Die Suche bleibt verfügbar.",
"Fortschritt der Speicherindex-Aktualisierung",
"Aktualisierung abbrechen",
"Indexstatus ist vorübergehend nicht verfügbar",
"Dieser Status ist möglicherweise veraltet. Vorhandene Erinnerungen bleiben verfügbar.",
"Neu laden",
"Prüfung des alten Suchindex wurde nicht abgeschlossen",
"Das Zusammenführen neuer Erinnerungen ist pausiert. Vorhandene Erinnerungen bleiben durchsuchbar.",
"Das Zusammenführen wurde fortgesetzt. Sie können MemHub weiterverwenden; der Fehlergrund bleibt in der Diagnose erhalten.",
"Diagnose anzeigen",
"Suchmodell wurde geändert",
"Während der Pause gespeicherte Unterhaltungen werden in Erinnerungen umgewandelt.",
"Alten Index bereinigen",
"Erfolgsmeldung schließen",
"Der alte Index kann derzeit nicht entfernt werden. MemHub versucht es nach Ablauf der Aufbewahrungsfrist erneut.",
"Indexaktualisierung wurde nicht abgeschlossen",
"Das vorherige Suchmodell ist weiterhin aktiv. Es gingen keine Erinnerungen verloren.",
"Zeitüberschreitung beim Warten auf den Abschluss der Speicherzusammenführung.",
"Der Suchmodelldienst ist wiederholt fehlgeschlagen.",
"Der Suchdienst lieferte andere Indexdimensionen als beim Test.",
"Der neue Speicherindex hat die Integritätsprüfung nicht bestanden.",
"Die Konfiguration des Suchmodells wurde während der Aktualisierung geändert.",
"Die Indexaktualisierung wurde abgebrochen.",
"Der API Key des Suchdienstes konnte nicht gelesen werden.",
"Der sichere Systemspeicher ist vorübergehend nicht verfügbar.",
"Der Speicherdienst blieb belegt, daher konnte die Änderung nicht abgeschlossen werden.",
"{reason} {safety}",
"Verbindung prüfen",
"Fehlermeldung schließen",
]);
export const embeddingSwitchPortugueseMessages = localized([
"Atualizar o índice de pesquisa de memórias?",
"Os índices criados por modelos de pesquisa diferentes não podem ser combinados. O índice será reconstruído para que as memórias existentes continuem acessíveis.",
"Memórias existentes",
"{count}",
" · Utilizadores: {count}",
"Tempo estimado",
"Ainda não é possível estimar",
"Cerca de {count} s",
"Cerca de {count} min",
"Pedidos remotos",
"Nenhum",
"Faturados pelo fornecedor",
"Cerca de {count}",
"As memórias continuam pesquisáveis durante a atualização",
"O MemHub continuará a usar o modelo de pesquisa atual. As novas conversas serão guardadas e convertidas em memórias após a atualização.",
"Uma falha não afeta os dados existentes",
"O modelo só muda depois de concluídas a atualização e a verificação. Se o processo falhar ou for cancelado, o modelo atual permanece ativo.",
"Ver detalhes técnicos",
"Modelo de pesquisa atual",
"Índice antigo",
"Novo modelo de pesquisa",
"Pendente",
"Dimensões do índice",
"Desconhecidas",
"Espaço adicional em disco",
"Manter o índice antigo",
"{count} dias",
"Teste primeiro a ligação ao novo modelo de pesquisa.",
"As dimensões do índice do novo modelo ainda não foram confirmadas. Teste novamente a ligação.",
"Os dados antigos ainda estão a ser atualizados. Aguarde até o processo terminar.",
"Esta instalação não consegue atualizar o índice de memórias em segurança.",
"Não há espaço livre suficiente para criar o novo índice em segurança.",
"Já está em curso outra atualização do índice.",
"As memórias estão a ser consolidadas. Aguarde até terminar antes de mudar o modelo de pesquisa.",
"Não é possível mudar o modelo de pesquisa no estado atual.",
"O estado das memórias mudou. Reveja novamente o impacto.",
"Não é possível iniciar agora a atualização do índice. Tente novamente mais tarde.",
"Agora não",
"Iniciar atualização do índice",
"A consolidar um índice de pesquisa antigo",
"A preparar a atualização do índice de memórias",
"A atualizar o índice de memórias",
"A verificar o novo índice de memórias",
"A ativar o novo modelo de pesquisa",
"A restaurar as definições anteriores após uma atualização incompleta",
"Atualização do índice de memórias",
"A verificar os índices locais de pesquisa de memórias",
"O modelo de pesquisa anterior continua ativo. Nenhuma memória foi perdida.",
"As novas memórias só serão consolidadas após a conclusão. A pesquisa continua disponível.",
"Progresso da atualização do índice de memórias",
"Cancelar atualização",
"O estado do índice está temporariamente indisponível",
"Este estado pode estar desatualizado. As memórias existentes continuam disponíveis.",
"Recarregar",
"A verificação do índice de pesquisa antigo não terminou",
"A consolidação de novas memórias está em pausa. As memórias existentes continuam pesquisáveis.",
"A consolidação de memórias foi retomada. Pode continuar a usar o MemHub; o motivo da falha fica guardado no diagnóstico.",
"Ver diagnóstico",
"Modelo de pesquisa alterado",
"As conversas guardadas durante a pausa estão a ser convertidas em memórias.",
"Limpar índice antigo",
"Fechar mensagem de conclusão",
"Não é possível remover agora o índice antigo. O MemHub tentará novamente após o período de retenção.",
"A atualização do índice não terminou",
"O modelo de pesquisa anterior continua ativo. Nenhuma memória foi perdida.",
"O tempo de espera terminou enquanto se aguardava a conclusão da consolidação de memórias.",
"O serviço do modelo de pesquisa falhou repetidamente.",
"O serviço de pesquisa devolveu dimensões de índice diferentes do resultado do teste.",
"O novo índice de memórias não passou na verificação de integridade.",
"A configuração do modelo de pesquisa mudou durante a atualização.",
"A atualização do índice foi cancelada.",
"Não foi possível ler a API Key do serviço de pesquisa.",
"O armazenamento seguro do sistema está temporariamente indisponível.",
"O serviço de memórias permaneceu ocupado, por isso não foi possível concluir a alteração.",
"{reason} {safety}",
"Verificar ligação",
"Fechar mensagem de falha",
]);
export const embeddingSwitchFrenchMessages = localized([
"Mettre à jour l’index de recherche des mémoires ?",
"Les index créés par des modèles de recherche différents ne peuvent pas être combinés. L’index sera reconstruit pour continuer à retrouver les mémoires existantes.",
"Mémoires existantes",
"{count}",
" · Utilisateurs : {count}",
"Durée estimée",
"Estimation pas encore disponible",
"Environ {count} s",
"Environ {count} min",
"Requêtes distantes",
"Aucune",
"Facturées par le fournisseur",
"Environ {count}",
"Les mémoires restent accessibles pendant la mise à jour",
"MemHub continue d’utiliser le modèle de recherche actuel. Les nouvelles conversations sont enregistrées, puis transformées en mémoires après la mise à jour.",
"Un échec n’affectera pas les données existantes",
"Le modèle change uniquement après la mise à jour et sa vérification. En cas d’échec ou d’annulation, le modèle actuel reste actif.",
"Afficher les détails techniques",
"Modèle de recherche actuel",
"Ancien index",
"Nouveau modèle de recherche",
"En attente",
"Dimensions de l’index",
"Inconnues",
"Espace disque supplémentaire",
"Conserver l’ancien index",
"{count} j",
"Testez d’abord la connexion au nouveau modèle de recherche.",
"Les dimensions de l’index du nouveau modèle ne sont pas confirmées. Testez à nouveau la connexion.",
"Les anciennes données sont encore en cours de mise à niveau. Attendez la fin de l’opération.",
"Cette installation ne peut pas mettre à jour l’index des mémoires en toute sécurité.",
"L’espace disque disponible est insuffisant pour créer le nouvel index en toute sécurité.",
"Une autre mise à jour de l’index est déjà en cours.",
"Les mémoires sont en cours de consolidation. Attendez la fin avant de changer de modèle de recherche.",
"Le modèle de recherche ne peut pas être modifié dans l’état actuel.",
"L’état des mémoires a changé. Vérifiez à nouveau les conséquences.",
"Impossible de lancer la mise à jour de l’index pour le moment. Réessayez plus tard.",
"Pas maintenant",
"Lancer la mise à jour de l’index",
"Consolidation d’un ancien index de recherche",
"Préparation de la mise à jour de l’index des mémoires",
"Mise à jour de l’index des mémoires",
"Vérification du nouvel index des mémoires",
"Activation du nouveau modèle de recherche",
"Restauration des réglages précédents après une mise à jour inachevée",
"Mise à jour de l’index des mémoires",
"Vérification des index locaux de recherche des mémoires",
"Le modèle de recherche précédent est toujours actif. Aucune mémoire n’a été perdue.",
"Les nouvelles mémoires ne seront pas consolidées avant la fin. La recherche reste disponible.",
"Progression de la mise à jour de l’index des mémoires",
"Annuler la mise à jour",
"L’état de l’index est temporairement indisponible",
"Cet état n’est peut-être pas à jour. Les mémoires existantes restent disponibles.",
"Recharger",
"La vérification de l’ancien index de recherche ne s’est pas terminée",
"La consolidation des nouvelles mémoires est en pause. Les mémoires existantes restent accessibles.",
"La consolidation des mémoires a repris. Vous pouvez continuer à utiliser MemHub ; le motif de l’échec reste disponible dans le diagnostic.",
"Afficher le diagnostic",
"Modèle de recherche modifié",
"Les conversations enregistrées pendant la pause sont transformées en mémoires.",
"Nettoyer l’ancien index",
"Fermer le message de réussite",
"Impossible de supprimer l’ancien index pour le moment. MemHub réessaiera après la période de conservation.",
"La mise à jour de l’index ne s’est pas terminée",
"Le modèle de recherche précédent est toujours actif. Aucune mémoire n’a été perdue.",
"Le délai d’attente de la consolidation des mémoires a expiré.",
"Le service du modèle de recherche a échoué à plusieurs reprises.",
"Le service de recherche a renvoyé des dimensions d’index différentes du résultat du test.",
"Le nouvel index des mémoires n’a pas réussi la vérification d’intégrité.",
"La configuration du modèle de recherche a changé pendant la mise à jour.",
"La mise à jour de l’index a été annulée.",
"Impossible de lire l’API Key du service de recherche.",
"Le stockage sécurisé du système est temporairement indisponible.",
"Le service de mémoires est resté occupé, empêchant la modification de se terminer.",
"{reason} {safety}",
"Vérifier la connexion",
"Fermer le message d’échec",
]);
export const embeddingSwitchKoreanMessages = localized([
"메모리 검색 인덱스를 업데이트할까요?",
"서로 다른 검색 모델로 만든 인덱스는 함께 사용할 수 없습니다. 기존 메모리를 계속 찾을 수 있도록 인덱스를 다시 만듭니다.",
"기존 메모리",
"{count}개",
" · 사용자 {count}명",
"예상 소요 시간",
"아직 예상할 수 없음",
"약 {count}초",
"약 {count}분",
"원격 요청",
"없음",
"제공업체에서 요금 부과",
"약 {count}회",
"업데이트 중에도 메모리를 검색할 수 있습니다",
"MemHub는 현재 검색 모델을 계속 사용합니다. 새 대화는 저장되며 업데이트 후 메모리로 변환됩니다.",
"실패해도 기존 데이터에는 영향이 없습니다",
"업데이트와 검증이 모두 끝난 후에만 모델이 바뀝니다. 실패하거나 취소하면 현재 모델이 계속 활성화됩니다.",
"기술 세부 정보 보기",
"현재 검색 모델",
"이전 인덱스",
"새 검색 모델",
"대기 중",
"인덱스 차원",
"알 수 없음",
"추가 디스크 공간",
"이전 인덱스 보관",
"{count}일",
"먼저 새 검색 모델의 연결을 테스트하세요.",
"새 검색 모델의 인덱스 차원이 확인되지 않았습니다. 연결을 다시 테스트하세요.",
"이전 데이터가 아직 업그레이드 중입니다. 완료될 때까지 기다려 주세요.",
"현재 설치 버전에서는 메모리 인덱스를 안전하게 업데이트할 수 없습니다.",
"새 인덱스를 안전하게 만들 디스크 여유 공간이 부족합니다.",
"다른 인덱스 업데이트가 이미 실행 중입니다.",
"메모리를 통합하고 있습니다. 완료된 후 검색 모델을 변경하세요.",
"현재 상태에서는 검색 모델을 변경할 수 없습니다.",
"메모리 상태가 변경되었습니다. 영향을 다시 확인하세요.",
"지금은 인덱스 업데이트를 시작할 수 없습니다. 나중에 다시 시도하세요.",
"나중에",
"인덱스 업데이트 시작",
"이전 검색 인덱스 통합 중",
"메모리 인덱스 업데이트 준비 중",
"메모리 인덱스 업데이트 중",
"새 메모리 인덱스 검증 중",
"새 검색 모델 활성화 중",
"완료되지 않은 업데이트에서 이전 설정 복원 중",
"메모리 인덱스 업데이트",
"로컬 메모리 검색 인덱스 확인 중",
"이전 검색 모델이 계속 활성화되어 있습니다. 손실된 메모리는 없습니다.",
"완료될 때까지 새 메모리를 통합하지 않습니다. 검색은 계속 사용할 수 있습니다.",
"메모리 인덱스 업데이트 진행률",
"업데이트 취소",
"인덱스 상태를 일시적으로 확인할 수 없음",
"표시된 상태가 최신이 아닐 수 있습니다. 기존 메모리는 계속 사용할 수 있습니다.",
"새로고침",
"이전 검색 인덱스 확인이 완료되지 않음",
"새 메모리 통합이 일시 중지되었습니다. 기존 메모리는 계속 검색할 수 있습니다.",
"메모리 통합이 재개되었습니다. MemHub를 계속 사용할 수 있으며 실패 원인은 진단 정보에 보관됩니다.",
"진단 정보 보기",
"검색 모델 변경 완료",
"일시 중지 중 저장된 대화를 메모리로 변환하고 있습니다.",
"이전 인덱스 정리",
"완료 메시지 닫기",
"지금은 이전 인덱스를 삭제할 수 없습니다. 보관 기간이 끝나면 MemHub가 다시 시도합니다.",
"인덱스 업데이트가 완료되지 않음",
"이전 검색 모델이 계속 활성화되어 있습니다. 손실된 메모리는 없습니다.",
"메모리 통합이 끝나기를 기다리는 동안 시간이 초과되었습니다.",
"검색 모델 서비스에서 오류가 반복적으로 발생했습니다.",
"검색 서비스에서 테스트 결과와 다른 인덱스 차원을 반환했습니다.",
"새 메모리 인덱스가 무결성 검증을 통과하지 못했습니다.",
"업데이트 중 검색 모델 설정이 변경되었습니다.",
"인덱스 업데이트가 취소되었습니다.",
"검색 서비스의 API Key를 읽을 수 없습니다.",
"시스템 보안 저장소를 일시적으로 사용할 수 없습니다.",
"메모리 서비스가 계속 사용 중이어서 변경을 완료할 수 없었습니다.",
"{reason} {safety}",
"연결 확인",
"실패 메시지 닫기",
]);
export const embeddingSwitchRussianMessages = localized([
"Обновить индекс поиска воспоминаний?",
"Индексы, созданные разными моделями поиска, нельзя использовать вместе. Индекс будет перестроен, чтобы существующие воспоминания оставались доступными.",
"Существующие воспоминания",
"{count}",
" · Пользователей: {count}",
"Расчётное время",
"Пока невозможно оценить",
"Около {count} с",
"Около {count} мин",
"Удалённые запросы",
"Не требуются",
"Оплачиваются по тарифу провайдера",
"Около {count}",
"Во время обновления воспоминания остаются доступными для поиска",
"MemHub продолжит использовать текущую модель поиска. Новые диалоги будут сохранены и преобразованы в воспоминания после обновления.",
"Сбой не затронет существующие данные",
"Модель изменится только после завершения обновления и проверки. При сбое или отмене текущая модель останется активной.",
"Показать технические сведения",
"Текущая модель поиска",
"Старый индекс",
"Новая модель поиска",
"Ожидает",
"Размерность индекса",
"Неизвестно",
"Дополнительное место на диске",
"Хранить старый индекс",
"{count} дн.",
"Сначала проверьте подключение к новой модели поиска.",
"Размерность индекса новой модели поиска не подтверждена. Проверьте подключение ещё раз.",
"Старые данные всё ещё обновляются. Дождитесь завершения.",
"Эта установка не может безопасно обновить индекс воспоминаний.",
"Недостаточно свободного места для безопасного создания нового индекса.",
"Другое обновление индекса уже выполняется.",
"Воспоминания объединяются. Дождитесь завершения, прежде чем менять модель поиска.",
"В текущем состоянии изменить модель поиска нельзя.",
"Состояние воспоминаний изменилось. Проверьте последствия ещё раз.",
"Сейчас невозможно запустить обновление индекса. Повторите попытку позже.",
"Не сейчас",
"Начать обновление индекса",
"Объединение старого поискового индекса",
"Подготовка обновления индекса воспоминаний",
"Обновление индекса воспоминаний",
"Проверка нового индекса воспоминаний",
"Включение новой модели поиска",
"Восстановление прежних настроек после незавершённого обновления",
"Обновление индекса воспоминаний",
"Проверка локальных поисковых индексов воспоминаний",
"Предыдущая модель поиска по-прежнему активна. Воспоминания не потеряны.",
"Новые воспоминания не будут объединяться до завершения. Поиск остаётся доступным.",
"Ход обновления индекса воспоминаний",
"Отменить обновление",
"Состояние индекса временно недоступно",
"Эти сведения могут быть устаревшими. Существующие воспоминания остаются доступными.",
"Обновить",
"Проверка старого поискового индекса не завершилась",
"Объединение новых воспоминаний приостановлено. Существующие воспоминания остаются доступными для поиска.",
"Объединение воспоминаний возобновлено. Можно продолжать пользоваться MemHub; причина сбоя сохранена в диагностике.",
"Показать диагностику",
"Модель поиска изменена",
"Диалоги, сохранённые во время паузы, преобразуются в воспоминания.",
"Удалить старый индекс",
"Закрыть сообщение об успехе",
"Сейчас удалить старый индекс невозможно. MemHub повторит попытку после окончания срока хранения.",
"Обновление индекса не завершилось",
"Предыдущая модель поиска по-прежнему активна. Воспоминания не потеряны.",
"Истекло время ожидания завершения объединения воспоминаний.",
"Сервис модели поиска несколько раз завершился с ошибкой.",
"Сервис поиска вернул размерность индекса, отличающуюся от результата проверки.",
"Новый индекс воспоминаний не прошёл проверку целостности.",
"Во время обновления изменилась конфигурация модели поиска.",
"Обновление индекса отменено.",
"Не удалось прочитать API Key сервиса поиска.",
"Защищённое системное хранилище временно недоступно.",
"Сервис воспоминаний оставался занят, поэтому изменение не удалось завершить.",
"{reason} {safety}",
"Проверить подключение",
"Закрыть сообщение об ошибке",
]);
export const embeddingSwitchIndonesianMessages = localized([
"Perbarui indeks pencarian memori?",
"Indeks yang dibuat oleh model pencarian berbeda tidak dapat digunakan bersama. Indeks akan dibuat ulang agar memori yang ada tetap dapat ditemukan.",
"Memori yang ada",
"{count}",
" · Pengguna: {count}",
"Perkiraan waktu",
"Belum dapat diperkirakan",
"Sekitar {count} detik",
"Sekitar {count} menit",
"Permintaan jarak jauh",
"Tidak ada",
"Ditagihkan oleh penyedia",
"Sekitar {count}",
"Memori tetap dapat dicari selama pembaruan",
"MemHub tetap menggunakan model pencarian saat ini. Percakapan baru akan disimpan dan diubah menjadi memori setelah pembaruan.",
"Kegagalan tidak akan memengaruhi data yang ada",
"Model hanya diganti setelah pembaruan dan verifikasi selesai. Jika gagal atau dibatalkan, model saat ini tetap aktif.",
"Lihat detail teknis",
"Model pencarian saat ini",
"Indeks lama",
"Model pencarian baru",
"Tertunda",
"Dimensi indeks",
"Tidak diketahui",
"Ruang disk tambahan",
"Simpan indeks lama",
"{count} hari",
"Uji koneksi model pencarian baru terlebih dahulu.",
"Dimensi indeks model pencarian baru belum dikonfirmasi. Uji kembali koneksinya.",
"Data lama masih ditingkatkan. Tunggu hingga proses selesai.",
"Instalasi ini tidak dapat memperbarui indeks memori dengan aman.",
"Ruang disk kosong tidak cukup untuk membuat indeks baru dengan aman.",
"Pembaruan indeks lain sedang berjalan.",
"Memori sedang digabungkan. Tunggu hingga selesai sebelum mengganti model pencarian.",
"Model pencarian tidak dapat diganti dalam keadaan saat ini.",
"Status memori telah berubah. Tinjau kembali dampaknya.",
"Pembaruan indeks tidak dapat dimulai sekarang. Coba lagi nanti.",
"Jangan sekarang",
"Mulai pembaruan indeks",
"Menggabungkan indeks pencarian lama",
"Menyiapkan pembaruan indeks memori",
"Memperbarui indeks memori",
"Memverifikasi indeks memori baru",
"Mengaktifkan model pencarian baru",
"Memulihkan pengaturan sebelumnya setelah pembaruan tidak selesai",
"Pembaruan indeks memori",
"Memeriksa indeks pencarian memori lokal",
"Model pencarian sebelumnya masih aktif. Tidak ada memori yang hilang.",
"Memori baru tidak akan digabungkan hingga proses selesai. Pencarian tetap tersedia.",
"Progres pembaruan indeks memori",
"Batalkan pembaruan",
"Status indeks untuk sementara tidak tersedia",
"Status ini mungkin sudah tidak terbaru. Memori yang ada tetap tersedia.",
"Muat ulang",
"Pemeriksaan indeks pencarian lama tidak selesai",
"Penggabungan memori baru dijeda. Memori yang ada tetap dapat dicari.",
"Penggabungan memori telah dilanjutkan. Anda dapat terus menggunakan MemHub; alasan kegagalan tetap tersimpan dalam diagnostik.",
"Lihat diagnostik",
"Model pencarian telah diganti",
"Percakapan yang disimpan selama jeda sedang diubah menjadi memori.",
"Bersihkan indeks lama",
"Tutup pesan penyelesaian",
"Indeks lama tidak dapat dihapus sekarang. MemHub akan mencoba lagi setelah masa penyimpanan berakhir.",
"Pembaruan indeks tidak selesai",
"Model pencarian sebelumnya masih aktif. Tidak ada memori yang hilang.",
"Waktu tunggu habis saat menunggu penggabungan memori selesai.",
"Layanan model pencarian gagal berulang kali.",
"Layanan pencarian mengembalikan dimensi indeks yang berbeda dari hasil pengujian.",
"Indeks memori baru tidak lolos verifikasi integritas.",
"Konfigurasi model pencarian berubah selama pembaruan.",
"Pembaruan indeks dibatalkan.",
"API Key layanan pencarian tidak dapat dibaca.",
"Penyimpanan aman sistem untuk sementara tidak tersedia.",
"Layanan memori tetap sibuk sehingga perubahan tidak dapat diselesaikan.",
"{reason} {safety}",
"Periksa koneksi",
"Tutup pesan kegagalan",
]);
@@ -1,4 +1,5 @@
import { currentEnglishMessages } from "./gui-i18n-current.js";
import { embeddingSwitchEnglishMessages } from "./gui-i18n-embedding-switch.js";
import { modeEnglishMessages } from "./gui-i18n-mode.js";
// English translations integrated from PR #114.
@@ -2020,5 +2021,6 @@ const baseEnglishMessages = {
export const englishMessages = {
...baseEnglishMessages,
...currentEnglishMessages,
...embeddingSwitchEnglishMessages,
...modeEnglishMessages,
} as const;
@@ -16,6 +16,12 @@ const INTENTIONALLY_EMPTY: Partial<Record<string, readonly string[]>> = {
es: ["apps.unit"],
};
/** Locale-independent interpolation-only values that cannot be translated. */
const ENGLISH_EQUIVALENT_ALLOWED = new Set<MessageKey>([
"embeddingSwitch.impact.memoryCount",
"embeddingSwitch.rollback.reason",
]);
const keys = Object.keys(chineseMessages) as MessageKey[];
describe.each(SUPPORTED_LOCALES)("%s catalog", (locale) => {
@@ -61,6 +67,41 @@ describe.each(SUPPORTED_LOCALES)("%s catalog", (locale) => {
});
});
describe("embedding switch catalog", () => {
const embeddingKeys = keys.filter((key) =>
key.startsWith("embeddingSwitch."),
);
it("is complete in all 11 locales", () => {
expect(embeddingKeys.length).toBeGreaterThan(0);
for (const locale of SUPPORTED_LOCALES) {
for (const key of embeddingKeys)
expect(
guiMessages[locale][key]?.trim(),
`${locale} ${key}`,
).toBeTruthy();
}
});
it.each(["en", "es"] as const)("%s has no Chinese leakage", (locale) => {
for (const key of embeddingKeys)
expect(guiMessages[locale][key], key).not.toMatch(/[一-鿿]/);
});
it("does not silently retain English fallback copy", () => {
const untranslated: string[] = [];
for (const locale of SUPPORTED_LOCALES) {
if (locale === "en") continue;
for (const key of embeddingKeys) {
if (ENGLISH_EQUIVALENT_ALLOWED.has(key)) continue;
if (guiMessages[locale][key] === guiMessages.en[key])
untranslated.push(`${locale} ${key}`);
}
}
expect(untranslated).toEqual([]);
});
});
describe("fallback", () => {
it("falls back to English when a locale lacks a key", () => {
const key = "languageTitle";
@@ -10,6 +10,10 @@ import {
currentChineseMessages,
currentSpanishMessages,
} from "./gui-i18n-current.js";
import {
embeddingSwitchChineseMessages,
embeddingSwitchSpanishMessages,
} from "./gui-i18n-embedding-switch.js";
import {
guideChineseMessages,
guideSpanishMessages,
@@ -95,6 +99,11 @@ export const guiCatalogs: readonly Catalog[] = [
chinese: currentChineseMessages,
spanish: currentSpanishMessages,
},
{
name: "embedding-switch",
chinese: embeddingSwitchChineseMessages,
spanish: embeddingSwitchSpanishMessages,
},
{
name: "workbench",
chinese: workbenchChineseMessages,
@@ -187,6 +196,7 @@ const merged = mergeCatalogs(guiCatalogs);
/** The complete Chinese GUI catalog defines the supported message keys. */
export const chineseMessages = merged.chinese as typeof guideChineseMessages &
typeof embeddingSwitchChineseMessages &
typeof healthChineseMessages &
typeof recoveryChineseMessages &
typeof settingsChineseMessages &
@@ -1,9 +1,11 @@
import type { MessageKey } from "../gui-i18n.js";
import { embeddingSwitchGermanMessages } from "../gui-i18n-embedding-switch.js";
import { modeGermanMessages } from "../gui-i18n-mode.js";
/** German (de, formal Sie) GUI copy. Machine-assisted; native-speaker review recommended. */
export const germanMessages: Record<MessageKey, string> = {
...modeGermanMessages,
...embeddingSwitchGermanMessages,
"connectionFallback.title": "Andere Tools verbinden",
"connectionFallback.desktopOnly":
"Lokale Kopplung ist nur in der Desktop-App verfügbar.",
@@ -1,9 +1,11 @@
import type { MessageKey } from "../gui-i18n.js";
import { embeddingSwitchFrenchMessages } from "../gui-i18n-embedding-switch.js";
import { modeFrenchMessages } from "../gui-i18n-mode.js";
/** French (fr, vous) GUI copy. Machine-assisted; native-speaker review recommended. */
export const frenchMessages: Record<MessageKey, string> = {
...modeFrenchMessages,
...embeddingSwitchFrenchMessages,
"connectionFallback.title": "Connecter d’autres outils",
"connectionFallback.desktopOnly":
"L’association locale n’est disponible que dans l’application de bureau.",
@@ -1,9 +1,11 @@
import type { MessageKey } from "../gui-i18n.js";
import { embeddingSwitchIndonesianMessages } from "../gui-i18n-embedding-switch.js";
import { modeIndonesianMessages } from "../gui-i18n-mode.js";
/** Indonesian (id, Anda) GUI copy. Machine-assisted; native-speaker review recommended. */
export const indonesianMessages: Record<MessageKey, string> = {
...modeIndonesianMessages,
...embeddingSwitchIndonesianMessages,
"connectionFallback.title": "Hubungkan alat lain",
"connectionFallback.desktopOnly":
"Pemasangan lokal hanya tersedia di aplikasi desktop.",
@@ -1,9 +1,11 @@
import type { MessageKey } from "../gui-i18n.js";
import { embeddingSwitchJapaneseMessages } from "../gui-i18n-embedding-switch.js";
import { modeJapaneseMessages } from "../gui-i18n-mode.js";
/** Japanese (ja) GUI copy. Machine-assisted; native-speaker review recommended. */
export const japaneseMessages: Record<MessageKey, string> = {
...modeJapaneseMessages,
...embeddingSwitchJapaneseMessages,
"connectionFallback.title": "ほかのツールを接続",
"connectionFallback.desktopOnly":
"ローカルのペアリングはデスクトップアプリでのみ利用できます。",
@@ -1,9 +1,11 @@
import type { MessageKey } from "../gui-i18n.js";
import { embeddingSwitchKoreanMessages } from "../gui-i18n-embedding-switch.js";
import { modeKoreanMessages } from "../gui-i18n-mode.js";
/** Korean (ko) GUI copy. Machine-assisted; native-speaker review recommended. */
export const koreanMessages: Record<MessageKey, string> = {
...modeKoreanMessages,
...embeddingSwitchKoreanMessages,
"connectionFallback.title": "다른 도구 연결",
"connectionFallback.desktopOnly":
"로컬 페어링은 데스크톱 앱에서만 사용할 수 있습니다.",
@@ -1,9 +1,11 @@
import type { MessageKey } from "../gui-i18n.js";
import { embeddingSwitchPortugueseMessages } from "../gui-i18n-embedding-switch.js";
import { modePortugueseMessages } from "../gui-i18n-mode.js";
/** Portuguese (pt, Brazilian usage) GUI copy. Machine-assisted; native-speaker review recommended. */
export const portugueseMessages: Record<MessageKey, string> = {
...modePortugueseMessages,
...embeddingSwitchPortugueseMessages,
"connectionFallback.title": "Conectar outras ferramentas",
"connectionFallback.desktopOnly":
"O pareamento local só está disponível no app para desktop.",
@@ -1,9 +1,11 @@
import type { MessageKey } from "../gui-i18n.js";
import { embeddingSwitchRussianMessages } from "../gui-i18n-embedding-switch.js";
import { modeRussianMessages } from "../gui-i18n-mode.js";
/** Russian (ru) GUI copy. Machine-assisted; native-speaker review recommended. */
export const russianMessages: Record<MessageKey, string> = {
...modeRussianMessages,
...embeddingSwitchRussianMessages,
"connectionFallback.title": "Подключить другие инструменты",
"connectionFallback.desktopOnly":
"Локальное сопряжение доступно только в настольном приложении.",
@@ -1,9 +1,11 @@
import type { MessageKey } from "../gui-i18n.js";
import { embeddingSwitchTraditionalChineseMessages } from "../gui-i18n-embedding-switch.js";
import { modeTraditionalChineseMessages } from "../gui-i18n-mode.js";
/** Traditional Chinese (zh-Hant, Taiwan usage) GUI copy. Machine-assisted; native-speaker review recommended. */
export const traditionalChineseMessages: Record<MessageKey, string> = {
...modeTraditionalChineseMessages,
...embeddingSwitchTraditionalChineseMessages,
"connectionFallback.title": "連線其他工具",
"connectionFallback.desktopOnly": "本機配對僅在桌面 App 中可用。",
"connectionFallback.description":