Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac958571be | ||
|
|
c73bbb9eac |
No files matched your search
+1
-1
@@ -105,7 +105,7 @@ Hy Memory and MemHub are versioned and released independently.
|
||||
|
||||
## MemHub 1.0.6 — 2026-11-04
|
||||
|
||||
Build 45, on `hy-memory==2.0.14`. Linux x64/arm64 Runtime and macOS arm64
|
||||
Build 44, on `hy-memory==2.0.14`. Linux x64/arm64 Runtime and macOS arm64
|
||||
App and Runtime.
|
||||
|
||||
- MemHub App now exposes the complete memory-mode lifecycle. New installs
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ If you run into a problem, please open an Issue with the version, platform,repro
|
||||
## 发行与平台合同
|
||||
|
||||
源码当前固定使用公开 PyPI 正式版 `hy-memory==2.0.14`。MemHub 源码版本为 `1.0.6`
|
||||
(build 45),GitHub 仓库为
|
||||
(build 44),GitHub 仓库为
|
||||
[`Tencent-Hunyuan/Hy-Memory`](https://github.com/Tencent-Hunyuan/Hy-Memory),npm 包名为
|
||||
`memhub-cli`。当前代码架构见
|
||||
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)。
|
||||
|
||||
@@ -34,7 +34,6 @@ it("shows every mode feature and reveals Ultra scheduling after selection", asyn
|
||||
expect(screen.getByText("不调用生成模型")).toBeTruthy();
|
||||
expect(screen.getByText("沉淀高级记忆")).toBeTruthy();
|
||||
expect(screen.getByText("增强关联召回")).toBeTruthy();
|
||||
expect(screen.getByText("· Pro").className).toContain("memory-mode-tier");
|
||||
expect(screen.queryByText("每天自动整合")).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole("radio", { name: /深度记忆/ }));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { MemoryPipelineMode } from "@one-memory/contracts";
|
||||
import { Check, Clock3 } from "lucide-react";
|
||||
import { type ReactNode, useId } from "react";
|
||||
import { MODE_COPY, MODE_TIER, modeDisplayName } from "./memory-mode-ui";
|
||||
import { MODE_COPY } from "./memory-mode-ui";
|
||||
import { useOnboardingTranslation } from "./onboarding-language";
|
||||
import "./memory-mode-cards.css";
|
||||
|
||||
@@ -68,8 +68,11 @@ export function MemoryModeCards({
|
||||
{MODES.map((mode) => {
|
||||
const selected = value === mode.id;
|
||||
const modeDisabled = unavailable(mode.id);
|
||||
const title = modeDisplayName(text, mode.id);
|
||||
const tier = MODE_TIER[mode.id];
|
||||
const localizedTitle = text(MODE_COPY[mode.id].title);
|
||||
const title = localizedTitle.replace(
|
||||
/\s*[((](?:lite|pro|ultra)[))]\s*$/i,
|
||||
"",
|
||||
);
|
||||
const features = text(MODE_COPY[mode.id].description).split(
|
||||
/\s*·\s*/,
|
||||
);
|
||||
@@ -87,7 +90,7 @@ export function MemoryModeCards({
|
||||
type="radio"
|
||||
name={name}
|
||||
value={mode.id}
|
||||
aria-label={`${title} · ${tier}`}
|
||||
aria-label={localizedTitle}
|
||||
aria-describedby={describedBy?.[mode.id]}
|
||||
checked={selected}
|
||||
disabled={modeDisabled}
|
||||
@@ -96,7 +99,9 @@ export function MemoryModeCards({
|
||||
<span className="memory-mode-title">
|
||||
<span className="memory-mode-index">{mode.index}</span>
|
||||
<strong>{title}</strong>
|
||||
<span className="memory-mode-tier">· {tier}</span>
|
||||
<span className="memory-mode-tier">
|
||||
{mode.id.toUpperCase()}
|
||||
</span>
|
||||
{current === mode.id && currentBadge ? (
|
||||
<span className="memory-mode-current">{currentBadge}</span>
|
||||
) : null}
|
||||
|
||||
@@ -75,13 +75,9 @@ async function chooseMode(
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
name: RegExp,
|
||||
) {
|
||||
const control = await screen.findByRole("combobox", {
|
||||
name: "记忆模式",
|
||||
});
|
||||
await user.click(control);
|
||||
const option = await screen.findByRole("option", { name });
|
||||
const option = await screen.findByRole("radio", { name });
|
||||
await user.click(option);
|
||||
return control;
|
||||
return option;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
@@ -92,43 +88,17 @@ afterEach(() => {
|
||||
vi.mocked(memoryModePlanUnsupported).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("shows the current mode in a compact dropdown", async () => {
|
||||
it("renders all settings modes as one grouped shared choice list", async () => {
|
||||
prepare("pro");
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
|
||||
const control = await screen.findByRole("combobox", {
|
||||
name: "记忆模式",
|
||||
const group = await screen.findByRole("radiogroup", {
|
||||
name: "当前模式",
|
||||
});
|
||||
expect(control.textContent).toContain("智能记忆 · Pro");
|
||||
expect(control.textContent).toContain("沉淀高级记忆");
|
||||
expect(within(group).getAllByRole("radio")).toHaveLength(3);
|
||||
expect(
|
||||
control.querySelector(".om-option-menu-suffix")?.textContent,
|
||||
).toContain("· Pro");
|
||||
expect(control.textContent).not.toContain("(");
|
||||
expect(screen.queryByText("当前模式")).toBeNull();
|
||||
expect(screen.queryByRole("radiogroup")).toBeNull();
|
||||
|
||||
await user.click(control);
|
||||
expect(screen.getAllByRole("option")).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("shows the effective Ultra mode and disables selection when forced by the environment", async () => {
|
||||
prepare("ultra");
|
||||
vi.mocked(getMemoryMode).mockResolvedValue({
|
||||
...state("ultra"),
|
||||
configured_mode: "pro",
|
||||
forced_by_env: true,
|
||||
});
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
|
||||
const control = await screen.findByRole("combobox", {
|
||||
name: "记忆模式",
|
||||
});
|
||||
expect(control.textContent).toContain("深度记忆 · Ultra");
|
||||
expect(control).toHaveProperty("disabled", true);
|
||||
expect(screen.getByRole("switch", { name: "每天自动整合" })).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "立即整合" })).toBeTruthy();
|
||||
within(group).getByText("当前模式").closest("label")?.textContent,
|
||||
).toContain("智能记忆PRO");
|
||||
});
|
||||
|
||||
it("confirms the data effects before switching to lite", async () => {
|
||||
@@ -141,7 +111,7 @@ it("confirms the data effects before switching to lite", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
|
||||
await chooseMode(user, /基础记忆 · Lite/);
|
||||
await chooseMode(user, /基础记忆(lite)/);
|
||||
expect(screen.getByText(/已有记忆继续保留/)).toBeTruthy();
|
||||
expect(screen.getByText("模型调用与费用")).toBeTruthy();
|
||||
expect(screen.getByText("搜索范围")).toBeTruthy();
|
||||
@@ -151,18 +121,18 @@ it("confirms the data effects before switching to lite", async () => {
|
||||
await waitFor(() => expect(setMemoryMode).toHaveBeenCalledWith("lite"));
|
||||
await waitFor(() =>
|
||||
expect(document.activeElement).toBe(
|
||||
screen.getByRole("combobox", { name: "记忆模式" }),
|
||||
screen.getByRole("radio", { name: /基础记忆(lite)/ }),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("traps modal focus, closes on Escape and restores the mode dropdown", async () => {
|
||||
it("traps modal focus, closes on Escape and restores the mode-card focus", async () => {
|
||||
prepare("pro");
|
||||
const outside = document.createElement("button");
|
||||
document.body.append(outside);
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
const lite = await chooseMode(user, /基础记忆 · Lite/);
|
||||
const lite = await chooseMode(user, /基础记忆(lite)/);
|
||||
|
||||
const cancel = screen.getByRole("button", { name: "取消" });
|
||||
await waitFor(() => expect(document.activeElement).toBe(cancel));
|
||||
@@ -184,7 +154,7 @@ it("keeps focus inside the modal while the mode change is pending", async () =>
|
||||
vi.mocked(setMemoryMode).mockReturnValue(new Promise(() => {}));
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
await chooseMode(user, /基础记忆 · Lite/);
|
||||
await chooseMode(user, /基础记忆(lite)/);
|
||||
await user.click(screen.getByRole("button", { name: "确认选择" }));
|
||||
|
||||
const modal = screen.getByRole("dialog");
|
||||
@@ -215,7 +185,7 @@ it("routes to model setup instead of switching pro without a model", async () =>
|
||||
<MemoryModeSettings hasActiveModel={false} onConfigureModel={configure} />,
|
||||
);
|
||||
|
||||
await chooseMode(user, /智能记忆 · Pro/);
|
||||
await chooseMode(user, /智能记忆(pro)/);
|
||||
expect(configure).toHaveBeenCalledExactlyOnceWith(
|
||||
"使用 pro 或 ultra 前需要先配置并启用生成模型。",
|
||||
);
|
||||
@@ -229,29 +199,11 @@ it("uses the isolated legacy plan only for a route-level old-Core 404", async ()
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
|
||||
await chooseMode(user, /基础记忆 · Lite/);
|
||||
await chooseMode(user, /基础记忆(lite)/);
|
||||
expect(planMemoryMode).toHaveBeenCalledWith("lite");
|
||||
expect(await screen.findByText(/已有记忆继续保留/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("restores focus to the mode dropdown when planning fails", async () => {
|
||||
prepare("pro");
|
||||
vi.mocked(planMemoryMode).mockRejectedValue(new Error("offline"));
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
|
||||
await chooseMode(user, /基础记忆 · Lite/);
|
||||
|
||||
expect(
|
||||
await screen.findByText("模式选择影响暂时无法读取,请重试。"),
|
||||
).toBeTruthy();
|
||||
await waitFor(() =>
|
||||
expect(document.activeElement).toBe(
|
||||
screen.getByRole("combobox", { name: "记忆模式" }),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps downgrade effects when an old Core falls back from ultra to pro", async () => {
|
||||
prepare("ultra");
|
||||
vi.mocked(planMemoryMode).mockRejectedValue(new Error("404"));
|
||||
@@ -259,7 +211,7 @@ it("keeps downgrade effects when an old Core falls back from ultra to pro", asyn
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
|
||||
await chooseMode(user, /智能记忆 · Pro/);
|
||||
await chooseMode(user, /智能记忆(pro)/);
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
expect(within(dialog).getAllByText("已有数据")).toHaveLength(1);
|
||||
@@ -271,7 +223,7 @@ it("explains model cost, search scope and the next step before switching to ultr
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
|
||||
await chooseMode(user, /深度记忆 · Ultra/);
|
||||
await chooseMode(user, /深度记忆(ultra)/);
|
||||
|
||||
expect(screen.getByText("模型调用与费用")).toBeTruthy();
|
||||
expect(screen.getByText("搜索范围")).toBeTruthy();
|
||||
@@ -287,7 +239,7 @@ it("distinguishes regular memories from higher-level Ultra data when switching t
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
|
||||
await chooseMode(user, /智能记忆 · Pro/);
|
||||
await chooseMode(user, /智能记忆(pro)/);
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
expect(within(dialog).getAllByText("已有数据")).toHaveLength(1);
|
||||
@@ -304,7 +256,7 @@ it("does not show automatic consolidation when switching from lite to pro", asyn
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
|
||||
await chooseMode(user, /智能记忆 · Pro/);
|
||||
await chooseMode(user, /智能记忆(pro)/);
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
expect(within(dialog).getAllByText("已有数据")).toHaveLength(1);
|
||||
@@ -321,10 +273,10 @@ it("focuses the Evolve settings after switching to ultra", async () => {
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
await screen.findByRole("combobox", { name: "记忆模式" });
|
||||
await screen.findByRole("radio", { name: /智能记忆(pro)/ });
|
||||
vi.mocked(getMemoryMode).mockResolvedValue(state("ultra"));
|
||||
|
||||
await chooseMode(user, /深度记忆 · Ultra/);
|
||||
await chooseMode(user, /深度记忆(ultra)/);
|
||||
await user.click(screen.getByRole("button", { name: "确认选择" }));
|
||||
|
||||
await waitFor(() =>
|
||||
@@ -335,29 +287,6 @@ it("focuses the Evolve settings after switching to ultra", async () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("focuses the recoverable Evolve section when Ultra schedule loading fails", async () => {
|
||||
prepare("pro");
|
||||
vi.mocked(getEvolveSchedule).mockRejectedValue(new Error("offline"));
|
||||
vi.mocked(setMemoryMode).mockResolvedValue({
|
||||
...state("ultra"),
|
||||
previous_mode: "pro",
|
||||
effects: ["existing_memories_added_to_graph"],
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
await screen.findByRole("combobox", { name: "记忆模式" });
|
||||
vi.mocked(getMemoryMode).mockResolvedValue(state("ultra"));
|
||||
|
||||
await chooseMode(user, /深度记忆 · Ultra/);
|
||||
await user.click(screen.getByRole("button", { name: "确认选择" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(document.activeElement?.getAttribute("id")).toContain("-evolve"),
|
||||
);
|
||||
expect(screen.getByText("自动整合设置暂时无法读取,请重试。")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "立即整合" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows the default nightly evolve controls in ultra", async () => {
|
||||
prepare("ultra");
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
@@ -376,26 +305,6 @@ it("shows the default nightly evolve controls in ultra", async () => {
|
||||
expect(screen.getByRole("button", { name: "立即整合" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps the immediate action visible and recovers when the schedule read fails", async () => {
|
||||
prepare("ultra");
|
||||
vi.mocked(getEvolveSchedule).mockRejectedValueOnce(new Error("offline"));
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
|
||||
expect(
|
||||
await screen.findByText("自动整合设置暂时无法读取,请重试。"),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByRole("switch", { name: "每天自动整合" })).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "立即整合" })).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "重新读取" }));
|
||||
|
||||
const automatic = await screen.findByRole("switch", {
|
||||
name: "每天自动整合",
|
||||
});
|
||||
await waitFor(() => expect(document.activeElement).toBe(automatic));
|
||||
});
|
||||
|
||||
it("opens the time picker from the whole control and saves a complete time", async () => {
|
||||
prepare("ultra");
|
||||
vi.mocked(setEvolveSchedule).mockResolvedValue({
|
||||
@@ -534,9 +443,9 @@ it("hides the time field when automatic evolve is off and keeps the saved time v
|
||||
it("updates mode state automatically without showing a manual refresh control", async () => {
|
||||
prepare("pro");
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
const mode = await screen.findByRole("combobox", { name: "记忆模式" });
|
||||
const pro = await screen.findByRole("radio", { name: /智能记忆(pro)/ });
|
||||
|
||||
expect(mode.textContent).toContain("智能记忆 · Pro");
|
||||
expect((pro as HTMLInputElement).checked).toBe(true);
|
||||
expect(screen.queryByRole("button", { name: "刷新记忆模式" })).toBeNull();
|
||||
expect(getMemoryMode).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => {
|
||||
@@ -550,9 +459,9 @@ it("keeps the mode usable when an evolve status read fails", async () => {
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
|
||||
expect(
|
||||
await screen.findByRole("combobox", {
|
||||
name: "记忆模式",
|
||||
}),
|
||||
(await screen.findByRole("radio", {
|
||||
name: /智能记忆(pro)/,
|
||||
})) as HTMLInputElement,
|
||||
).toHaveProperty("disabled", false);
|
||||
});
|
||||
|
||||
@@ -565,17 +474,15 @@ it("offers a text recovery action when the mode read fails", async () => {
|
||||
expect(
|
||||
await screen.findByText("记忆模式状态暂时无法读取,请重试。"),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole("combobox", { name: "记忆模式" })).toHaveProperty(
|
||||
"disabled",
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("radio", { name: /基础记忆(lite)/ }),
|
||||
).toHaveProperty("disabled", true);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "重新读取" }));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole("combobox", { name: "记忆模式" })).toHaveProperty(
|
||||
"disabled",
|
||||
false,
|
||||
),
|
||||
expect(
|
||||
screen.getByRole("radio", { name: /智能记忆(pro)/ }),
|
||||
).toHaveProperty("disabled", false),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -586,7 +493,7 @@ it("keeps a failed switch explanation inside the open confirmation dialog", asyn
|
||||
const user = userEvent.setup();
|
||||
render(<MemoryModeSettings hasActiveModel onConfigureModel={vi.fn()} />);
|
||||
|
||||
await chooseMode(user, /深度记忆 · Ultra/);
|
||||
await chooseMode(user, /深度记忆(ultra)/);
|
||||
await user.click(screen.getByRole("button", { name: "确认选择" }));
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
@@ -607,17 +514,15 @@ it("keeps lite available while model status is unknown", async () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("combobox", { name: "记忆模式" }));
|
||||
expect(
|
||||
await screen.findByRole("option", { name: /基础记忆 · Lite/ }),
|
||||
await screen.findByRole("radio", { name: /基础记忆(lite)/ }),
|
||||
).toHaveProperty("disabled", false);
|
||||
expect(screen.getByRole("option", { name: /智能记忆 · Pro/ })).toHaveProperty(
|
||||
expect(screen.getByRole("radio", { name: /智能记忆(pro)/ })).toHaveProperty(
|
||||
"disabled",
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("option", { name: /深度记忆 · Ultra/ }),
|
||||
screen.getByRole("radio", { name: /深度记忆(ultra)/ }),
|
||||
).toHaveProperty("disabled", true);
|
||||
});
|
||||
|
||||
@@ -633,10 +538,9 @@ it("blocks mode and Evolve controls during embedding maintenance", async () => {
|
||||
"正在检查或切换向量模型,完成前不能选择模式或手动整合。",
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole("combobox", { name: "记忆模式" })).toHaveProperty(
|
||||
"disabled",
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("radio", { name: /基础记忆(lite)/ }),
|
||||
).toHaveProperty("disabled", true);
|
||||
expect(screen.getByRole("button", { name: "立即整合" })).toHaveProperty(
|
||||
"disabled",
|
||||
true,
|
||||
|
||||
@@ -23,14 +23,8 @@ import {
|
||||
issueConfirmedModeTicket,
|
||||
} from "./confirmed-memory-mode";
|
||||
import { commitEvolveRun, useEvolveState } from "./evolve-state";
|
||||
import {
|
||||
BLOCKER_COPY,
|
||||
MemoryModeConfirmDialog,
|
||||
MODE_COPY,
|
||||
MODE_TIER,
|
||||
modeDisplayName,
|
||||
} from "./memory-mode-ui";
|
||||
import { OptionMenu } from "./OptionMenu";
|
||||
import { MemoryModeCards } from "./MemoryModeCards";
|
||||
import { BLOCKER_COPY, MemoryModeConfirmDialog } from "./memory-mode-ui";
|
||||
import { useOnboardingTranslation } from "./onboarding-language";
|
||||
import { R6Button } from "./R6Controls";
|
||||
import { R6StateRecovery } from "./R6StateRecovery";
|
||||
@@ -64,39 +58,28 @@ export function MemoryModeSettings({
|
||||
const [maintenance, setMaintenance] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [readFailed, setReadFailed] = useState(false);
|
||||
const [scheduleReadFailed, setScheduleReadFailed] = useState(false);
|
||||
const refreshVersion = useRef(0);
|
||||
const dialogOpener = useRef<HTMLElement | null>(null);
|
||||
const focusEvolveWhenReady = useRef(false);
|
||||
const focusModeWhenReady = useRef<MemoryPipelineMode | null>(null);
|
||||
const focusScheduleWhenReady = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusEvolveWhenReady.current || pendingMode || state?.mode !== "ultra")
|
||||
return;
|
||||
const evolve = document.getElementById(`${id}-evolve`);
|
||||
if (evolve) {
|
||||
focusEvolveWhenReady.current = false;
|
||||
evolve.focus();
|
||||
}
|
||||
focusEvolveWhenReady.current = false;
|
||||
document.getElementById(`${id}-evolve`)?.focus();
|
||||
}, [id, pendingMode, state?.mode]);
|
||||
|
||||
useEffect(() => {
|
||||
const target = focusModeWhenReady.current;
|
||||
if (!target || busy || pendingMode || state?.mode !== target) return;
|
||||
focusModeWhenReady.current = null;
|
||||
document.getElementById(`${id}-select`)?.focus();
|
||||
}, [busy, id, pendingMode, state?.mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusScheduleWhenReady.current || scheduleReadFailed || !schedule)
|
||||
return;
|
||||
focusScheduleWhenReady.current = false;
|
||||
document
|
||||
.getElementById(`${id}-evolve`)
|
||||
?.querySelector<HTMLElement>('[role="switch"]')
|
||||
.querySelector<HTMLInputElement>(
|
||||
`.r6-memory-mode-settings input[type="radio"][value="${target}"]`,
|
||||
)
|
||||
?.focus();
|
||||
}, [id, schedule, scheduleReadFailed]);
|
||||
}, [busy, pendingMode, state?.mode]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const version = ++refreshVersion.current;
|
||||
@@ -118,12 +101,8 @@ export function MemoryModeSettings({
|
||||
setError("记忆模式状态暂时无法读取,请重试。");
|
||||
setReadFailed(true);
|
||||
}
|
||||
if (scheduleResult.status === "fulfilled") {
|
||||
if (scheduleResult.status === "fulfilled")
|
||||
setScheduleState(scheduleResult.value);
|
||||
setScheduleReadFailed(false);
|
||||
} else {
|
||||
setScheduleReadFailed(true);
|
||||
}
|
||||
if (processingResult.status === "fulfilled")
|
||||
setMaintenance(Boolean(processingResult.value.embedding_switch));
|
||||
}, [onModeChange]);
|
||||
@@ -144,7 +123,7 @@ export function MemoryModeSettings({
|
||||
run?.state === "running" ||
|
||||
state?.graph_build.state === "running";
|
||||
|
||||
async function choose(mode: MemoryPipelineMode) {
|
||||
async function choose(mode: MemoryPipelineMode, opener: HTMLElement) {
|
||||
if (!state || mode === state.mode || busy || lifecycleBusy) return;
|
||||
setError("");
|
||||
setBusy(true);
|
||||
@@ -154,7 +133,6 @@ export function MemoryModeSettings({
|
||||
} catch (reason) {
|
||||
if (!memoryModePlanUnsupported(reason)) {
|
||||
setError("模式切换影响暂时无法读取,请重试。");
|
||||
focusModeWhenReady.current = state.mode;
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
@@ -165,7 +143,7 @@ export function MemoryModeSettings({
|
||||
onConfigureModel(BLOCKER_COPY.active_model_required);
|
||||
return;
|
||||
}
|
||||
dialogOpener.current = document.getElementById(`${id}-select`);
|
||||
dialogOpener.current = opener;
|
||||
setPendingPlan(plan);
|
||||
setPendingMode(mode);
|
||||
}
|
||||
@@ -217,7 +195,7 @@ export function MemoryModeSettings({
|
||||
}
|
||||
|
||||
async function evolveNow() {
|
||||
if (!state) return;
|
||||
if (!state || !schedule) return;
|
||||
refreshVersion.current++;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
@@ -225,15 +203,12 @@ export function MemoryModeSettings({
|
||||
const receipt = await startEvolve();
|
||||
commitEvolveRun(
|
||||
receipt,
|
||||
overview ??
|
||||
(schedule
|
||||
? {
|
||||
mode: state.mode,
|
||||
schedule,
|
||||
users: {},
|
||||
running: [],
|
||||
}
|
||||
: undefined),
|
||||
overview ?? {
|
||||
mode: state.mode,
|
||||
schedule,
|
||||
users: {},
|
||||
running: [],
|
||||
},
|
||||
);
|
||||
} catch (reason) {
|
||||
const code = apiErrorCode(reason);
|
||||
@@ -259,69 +234,28 @@ export function MemoryModeSettings({
|
||||
<p>{text("模式决定新对话如何保存,以及记忆页面展示什么。")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="r6-mode-control"
|
||||
data-expanded={state?.mode === "ultra" || undefined}
|
||||
>
|
||||
<div className="r6-mode-setting-row" aria-busy={state === null}>
|
||||
<OptionMenu
|
||||
id={`${id}-select`}
|
||||
ariaLabel={text("记忆模式")}
|
||||
value={state?.mode ?? ""}
|
||||
disabled={
|
||||
!state || busy || lifecycleBusy || Boolean(state?.forced_by_env)
|
||||
}
|
||||
options={(["lite", "pro", "ultra"] as const).map((mode) => ({
|
||||
value: mode,
|
||||
label: modeDisplayName(text, mode),
|
||||
suffix: MODE_TIER[mode],
|
||||
description: text(MODE_COPY[mode].description),
|
||||
disabled: mode !== "lite" && !modelStateKnown,
|
||||
}))}
|
||||
className="r6-language-option-menu r6-mode-option-menu"
|
||||
panelClassName="r6-language-option-menu-panel--settings r6-mode-option-menu-panel"
|
||||
onChange={(value) => {
|
||||
if (value === "lite" || value === "pro" || value === "ultra")
|
||||
void choose(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{state?.forced_by_env ? (
|
||||
<p className="r6-settings-note" role="status">
|
||||
{text("开发环境当前强制使用 ultra,保存的选择暂不会生效。")}
|
||||
</p>
|
||||
) : null}
|
||||
{maintenance ? (
|
||||
<p className="r6-settings-note" role="status">
|
||||
{text("正在检查或切换向量模型,完成前不能切换模式或手动整合。")}
|
||||
</p>
|
||||
) : null}
|
||||
{state?.graph_build.state === "running" ? (
|
||||
<p className="r6-settings-note" role="status">
|
||||
{text("正在把已有高级记忆加入知识图谱…")}
|
||||
</p>
|
||||
) : null}
|
||||
{error && !pendingMode ? (
|
||||
readFailed ? (
|
||||
<R6StateRecovery
|
||||
message={text(error)}
|
||||
onRetry={() => void refresh()}
|
||||
/>
|
||||
) : (
|
||||
<p className="r6-settings-error" role="alert">
|
||||
{text(error)}
|
||||
</p>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{state?.mode === "ultra" ? (
|
||||
<section
|
||||
id={`${id}-evolve`}
|
||||
className="r6-evolve-settings"
|
||||
aria-labelledby={`${id}-evolve-title`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{schedule ? (
|
||||
<MemoryModeCards
|
||||
aria-label={text("当前模式")}
|
||||
aria-busy={state === null}
|
||||
current={state?.mode}
|
||||
value={state?.mode ?? null}
|
||||
currentBadge={text("当前模式")}
|
||||
disabled={(mode) =>
|
||||
!state ||
|
||||
busy ||
|
||||
lifecycleBusy ||
|
||||
Boolean(state.forced_by_env && mode !== state.mode) ||
|
||||
(mode !== "lite" && !modelStateKnown)
|
||||
}
|
||||
onChange={(mode, control) => void choose(mode, control)}
|
||||
ultraDetail={
|
||||
state?.mode === "ultra" && schedule ? (
|
||||
<section
|
||||
id={`${id}-evolve`}
|
||||
className="r6-evolve-settings r6-evolve-settings--in-card"
|
||||
aria-labelledby={`${id}-evolve-title`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<UltraEvolveScheduleFields
|
||||
schedule={schedule}
|
||||
headingId={`${id}-evolve-title`}
|
||||
@@ -329,56 +263,63 @@ export function MemoryModeSettings({
|
||||
onEnabledChange={(enabled) => void changeSchedule({ enabled })}
|
||||
onTimeChange={(time) => changeSchedule({ time })}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="r6-evolve-heading">
|
||||
<h3 id={`${id}-evolve-title`}>
|
||||
{text("记忆整合(Evolve)")}
|
||||
</h3>
|
||||
<div className="r6-evolve-setting r6-evolve-setting--manual">
|
||||
<div className="r6-evolve-setting-copy">
|
||||
<h3>{text("立即整合一次")}</h3>
|
||||
<p>
|
||||
{text("定期重新整理已有记忆,补充更稳定的关联和结论。")}
|
||||
{text("只运行这一次,不改变每天的计划。")}
|
||||
{" · "}
|
||||
{text(
|
||||
run?.state === "running"
|
||||
? "正在整合记忆…"
|
||||
: last?.last_run_at
|
||||
? text("上次整合:{time}").replace(
|
||||
"{time}",
|
||||
new Date(last.last_run_at).toLocaleString(locale),
|
||||
)
|
||||
: "尚未整合",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{scheduleReadFailed ? (
|
||||
<R6StateRecovery
|
||||
message={text("自动整合设置暂时无法读取,请重试。")}
|
||||
onRetry={() => {
|
||||
focusScheduleWhenReady.current = true;
|
||||
void refresh();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
<div className="r6-evolve-setting r6-evolve-setting--manual">
|
||||
<div className="r6-evolve-setting-copy">
|
||||
<h3>{text("立即整合一次")}</h3>
|
||||
<p>
|
||||
{text("只运行这一次,不改变每天的计划。")}
|
||||
{" · "}
|
||||
{text(
|
||||
run?.state === "running"
|
||||
? "正在整合记忆…"
|
||||
: last?.last_run_at
|
||||
? text("上次整合:{time}").replace(
|
||||
"{time}",
|
||||
new Date(last.last_run_at).toLocaleString(locale),
|
||||
)
|
||||
: "尚未整合",
|
||||
)}
|
||||
</p>
|
||||
<R6Button
|
||||
pending={run?.state === "running"}
|
||||
disabled={busy || lifecycleBusy}
|
||||
onClick={() => void evolveNow()}
|
||||
>
|
||||
{text("立即整合")}
|
||||
</R6Button>
|
||||
</div>
|
||||
<R6Button
|
||||
pending={run?.state === "running"}
|
||||
disabled={busy || lifecycleBusy}
|
||||
onClick={() => void evolveNow()}
|
||||
>
|
||||
{text("立即整合")}
|
||||
</R6Button>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
{state?.forced_by_env ? (
|
||||
<p className="r6-settings-note" role="status">
|
||||
{text("开发环境当前强制使用 ultra,保存的选择暂不会生效。")}
|
||||
</p>
|
||||
) : null}
|
||||
{maintenance ? (
|
||||
<p className="r6-settings-note" role="status">
|
||||
{text("正在检查或切换向量模型,完成前不能切换模式或手动整合。")}
|
||||
</p>
|
||||
) : null}
|
||||
{state?.graph_build.state === "running" ? (
|
||||
<p className="r6-settings-note" role="status">
|
||||
{text("正在把已有高级记忆加入知识图谱…")}
|
||||
</p>
|
||||
) : null}
|
||||
{error && !pendingMode ? (
|
||||
readFailed ? (
|
||||
<R6StateRecovery
|
||||
message={text(error)}
|
||||
onRetry={() => void refresh()}
|
||||
/>
|
||||
) : (
|
||||
<p className="r6-settings-error" role="alert">
|
||||
{text(error)}
|
||||
</p>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{pendingMode && pendingPlan ? (
|
||||
<MemoryModeConfirmDialog
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { LanguageProvider } from "./gui-language";
|
||||
import { OptionMenu } from "./OptionMenu";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
it("shows a suffix without requiring description copy", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<OptionMenu
|
||||
ariaLabel="记忆模式"
|
||||
value="pro"
|
||||
options={[
|
||||
{ value: "lite", label: "基础记忆", suffix: "Lite" },
|
||||
{ value: "pro", label: "智能记忆", suffix: "Pro" },
|
||||
]}
|
||||
onChange={vi.fn()}
|
||||
/>
|
||||
</LanguageProvider>,
|
||||
);
|
||||
|
||||
const control = screen.getByRole("combobox", { name: "记忆模式" });
|
||||
expect(control.textContent).toContain("智能记忆 · Pro");
|
||||
await user.click(control);
|
||||
expect(screen.getByRole("option", { name: "基础记忆 · Lite" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps option descriptions available to assistive technology", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<OptionMenu
|
||||
ariaLabel="记忆模式"
|
||||
value="pro"
|
||||
options={[
|
||||
{
|
||||
value: "lite",
|
||||
label: "基础记忆",
|
||||
description: "保存原始会话",
|
||||
},
|
||||
{
|
||||
value: "pro",
|
||||
label: "智能记忆",
|
||||
suffix: "Pro",
|
||||
description: "调用模型整理记忆",
|
||||
},
|
||||
]}
|
||||
onChange={vi.fn()}
|
||||
/>
|
||||
</LanguageProvider>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("combobox", { name: "记忆模式" }));
|
||||
const option = screen.getByRole("option", { name: "智能记忆 · Pro" });
|
||||
const descriptionId = option.getAttribute("aria-describedby");
|
||||
expect(descriptionId).toBeTruthy();
|
||||
expect(document.getElementById(descriptionId ?? "")?.textContent).toBe(
|
||||
"调用模型整理记忆",
|
||||
);
|
||||
expect(
|
||||
screen
|
||||
.getByRole("option", { name: /基础记忆.*保存原始会话/ })
|
||||
.getAttribute("aria-describedby"),
|
||||
).toBeNull();
|
||||
});
|
||||
@@ -13,7 +13,6 @@ import { useGuiLanguage } from "./gui-language";
|
||||
export interface OptionMenuItem {
|
||||
value: string;
|
||||
label: string;
|
||||
suffix?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
lang?: string;
|
||||
@@ -23,17 +22,6 @@ const VIEWPORT_MARGIN = 12;
|
||||
const PANEL_GAP = 4;
|
||||
const MAX_PANEL_HEIGHT = 280;
|
||||
|
||||
function optionLabel(label: string, suffix?: string) {
|
||||
return suffix ? (
|
||||
<>
|
||||
{label}
|
||||
<span className="om-option-menu-suffix"> · {suffix}</span>
|
||||
</>
|
||||
) : (
|
||||
label
|
||||
);
|
||||
}
|
||||
|
||||
export function optionMenuPosition(
|
||||
anchor: Pick<DOMRect, "top" | "right" | "bottom" | "left" | "width">,
|
||||
panelHeight: number,
|
||||
@@ -280,11 +268,11 @@ export function OptionMenu(props: {
|
||||
>
|
||||
{selected?.description ? (
|
||||
<span className="om-option-menu-copy">
|
||||
<span>{optionLabel(visibleLabel, selected.suffix)}</span>
|
||||
<span>{visibleLabel}</span>
|
||||
<small>{selected.description}</small>
|
||||
</span>
|
||||
) : (
|
||||
optionLabel(visibleLabel, selected?.suffix)
|
||||
visibleLabel
|
||||
)}
|
||||
</button>
|
||||
{open &&
|
||||
@@ -311,14 +299,6 @@ export function OptionMenu(props: {
|
||||
lang={item.lang}
|
||||
data-active={activeIndex === index || undefined}
|
||||
aria-selected={item.value === props.value}
|
||||
aria-label={
|
||||
item.suffix ? `${item.label} · ${item.suffix}` : undefined
|
||||
}
|
||||
aria-describedby={
|
||||
item.description && item.suffix
|
||||
? `${panelId}-option-${index}-description`
|
||||
: undefined
|
||||
}
|
||||
disabled={item.disabled}
|
||||
tabIndex={-1}
|
||||
onMouseEnter={() => {
|
||||
@@ -328,13 +308,11 @@ export function OptionMenu(props: {
|
||||
>
|
||||
{item.description ? (
|
||||
<span className="om-option-menu-copy">
|
||||
<span>{optionLabel(item.label, item.suffix)}</span>
|
||||
<small id={`${panelId}-option-${index}-description`}>
|
||||
{item.description}
|
||||
</small>
|
||||
<span>{item.label}</span>
|
||||
<small>{item.description}</small>
|
||||
</span>
|
||||
) : (
|
||||
optionLabel(item.label, item.suffix)
|
||||
item.label
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -204,13 +204,7 @@ it("uses one sidebar hierarchy and explains when a saved model is inactive in li
|
||||
await user.click(screen.getByRole("button", { name: "界面语言" }));
|
||||
expect(screen.getByRole("combobox", { name: "语言" })).toBeTruthy();
|
||||
await user.click(screen.getByRole("button", { name: "模型配置" }));
|
||||
expect(
|
||||
await screen.findByText(
|
||||
(_, element) =>
|
||||
element?.tagName === "STRONG" &&
|
||||
element.textContent === "基础记忆 · Lite",
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(await screen.findByText("当前是基础记忆(lite)")).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(/是否开始整理取决于.*“用模型沉淀记忆”.*开关/),
|
||||
).toBeTruthy();
|
||||
@@ -249,9 +243,8 @@ it("carries a missing-model explanation into model configuration and moves focus
|
||||
const user = userEvent.setup();
|
||||
render(<R6Settings onDiagnostics={vi.fn()} />);
|
||||
|
||||
await user.click(await screen.findByRole("combobox", { name: "记忆模式" }));
|
||||
await user.click(
|
||||
await screen.findByRole("option", { name: /智能记忆 · Pro/ }),
|
||||
await screen.findByRole("radio", { name: /智能记忆(pro)/ }),
|
||||
);
|
||||
|
||||
expect(
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
type ModelSettingsActions,
|
||||
type ModelSettingsFocusSection,
|
||||
} from "./ModelSettings";
|
||||
import { MODE_TIER, modeDisplayName } from "./memory-mode-ui";
|
||||
import { useOnboardingTranslation } from "./onboarding-language";
|
||||
import { R6Button, R6Switch } from "./R6Controls";
|
||||
import { R6StateRecovery } from "./R6StateRecovery";
|
||||
@@ -395,16 +394,14 @@ export function R6Settings({
|
||||
{currentModel && (
|
||||
<div className="r6-model-mode-status" role="status">
|
||||
<strong>
|
||||
{memoryMode ? (
|
||||
<>
|
||||
{modeDisplayName(text, memoryMode)}
|
||||
<span className="r6-model-mode-tier">
|
||||
{" "}
|
||||
· {MODE_TIER[memoryMode]}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
text("正在读取记忆模式…")
|
||||
{text(
|
||||
memoryMode === "lite"
|
||||
? "当前是基础记忆(lite)"
|
||||
: memoryMode === "ultra"
|
||||
? "当前是深度记忆(ultra)"
|
||||
: memoryMode === "pro"
|
||||
? "当前是智能记忆(pro)"
|
||||
: "正在读取记忆模式…",
|
||||
)}
|
||||
</strong>
|
||||
<p>
|
||||
|
||||
@@ -8,19 +8,14 @@ function declaration(file: string, selector: string): string {
|
||||
}
|
||||
|
||||
it("fills settings controls without widening shared fallback controls", () => {
|
||||
const modeMenu = declaration(
|
||||
"r6-settings.css",
|
||||
".r6-settings .r6-mode-option-menu",
|
||||
);
|
||||
expect(modeMenu).toMatch(/(?:^|[\s;])width:\s*100%\s*;/);
|
||||
expect(modeMenu).toMatch(/(?:^|[\s;])max-width:\s*none\s*;/);
|
||||
|
||||
const fallback = declaration(
|
||||
"r6-settings.css",
|
||||
for (const selector of [
|
||||
".r6-memory-mode-settings .memory-mode-cards",
|
||||
".r6-settings .connection-fallback .custom-harness-name",
|
||||
);
|
||||
expect(fallback).toMatch(/(?:^|[\s;])width:\s*100%\s*;/);
|
||||
expect(fallback).toMatch(/(?:^|[\s;])max-width:\s*none\s*;/);
|
||||
]) {
|
||||
const rule = declaration("r6-settings.css", selector);
|
||||
expect(rule, selector).toMatch(/(?:^|[\s;])width:\s*100%\s*;/);
|
||||
expect(rule, selector).toMatch(/(?:^|[\s;])max-width:\s*none\s*;/);
|
||||
}
|
||||
expect(declaration("core-visual.css", ".custom-harness-name")).toMatch(
|
||||
/(?:^|[\s;])max-width:\s*420px\s*;/,
|
||||
);
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.memory-mode-current {
|
||||
|
||||
@@ -21,22 +21,6 @@ export const MODE_COPY: Record<
|
||||
},
|
||||
};
|
||||
|
||||
export const MODE_TIER: Record<MemoryPipelineMode, string> = {
|
||||
lite: "Lite",
|
||||
pro: "Pro",
|
||||
ultra: "Ultra",
|
||||
};
|
||||
|
||||
export function modeDisplayName(
|
||||
text: (value: string) => string,
|
||||
mode: MemoryPipelineMode,
|
||||
) {
|
||||
return text(MODE_COPY[mode].title)
|
||||
.replace(/\s*[((](?:lite|pro|ultra)[))]\s*/i, " ")
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export const BLOCKER_COPY: Record<MemoryModePlan["blockers"][number], string> =
|
||||
{
|
||||
active_model_required: "使用 pro 或 ultra 前需要先配置并启用生成模型。",
|
||||
@@ -200,7 +184,7 @@ export function MemoryModeConfirmDialog({
|
||||
<h2 id="memory-mode-confirm-title">
|
||||
{text("切换到 {mode}?").replace(
|
||||
"{mode}",
|
||||
`${modeDisplayName(text, mode)} · ${MODE_TIER[mode]}`,
|
||||
text(MODE_COPY[mode].title),
|
||||
)}
|
||||
</h2>
|
||||
<dl className="r6-mode-confirm-effects">
|
||||
|
||||
@@ -227,140 +227,26 @@
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.r6-settings .r6-mode-setting-row {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.r6-mode-control[data-expanded="true"] {
|
||||
border: 1px solid #cbd6de;
|
||||
border-radius: 10px;
|
||||
background: #fbfcfd;
|
||||
box-shadow:
|
||||
inset 0 1px 0 #fff,
|
||||
0 4px 14px rgb(48 71 87 / 5%);
|
||||
}
|
||||
.r6-mode-control[data-expanded="true"] .r6-mode-setting-row {
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid #dbe4ea;
|
||||
}
|
||||
.r6-settings
|
||||
.r6-mode-control[data-expanded="true"]
|
||||
.r6-mode-option-menu
|
||||
.om-option-menu-button {
|
||||
border: 0;
|
||||
border-radius: 9px 9px 0 0;
|
||||
box-shadow: inset 0 1px 0 #fff;
|
||||
}
|
||||
.r6-settings .r6-mode-option-menu {
|
||||
.r6-memory-mode-settings .memory-mode-cards {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
.r6-settings
|
||||
.r6-mode-option-menu.r6-language-option-menu
|
||||
.om-option-menu-button {
|
||||
height: auto;
|
||||
min-height: 72px;
|
||||
padding: 12px 64px 12px 18px;
|
||||
border-color: #cbd6de;
|
||||
border-radius: 10px;
|
||||
background: #f8fafb;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgb(255 255 255 / 90%),
|
||||
0 1px 2px rgb(48 71 87 / 4%);
|
||||
white-space: normal;
|
||||
transition:
|
||||
border-color 140ms ease-out,
|
||||
background-color 140ms ease-out,
|
||||
box-shadow 140ms ease-out;
|
||||
.r6-memory-mode-settings
|
||||
.memory-mode-card[data-mode="ultra"][data-selected="true"] {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.r6-settings
|
||||
.r6-mode-option-menu.r6-language-option-menu
|
||||
.om-option-menu-button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 52px;
|
||||
bottom: 12px;
|
||||
width: 1px;
|
||||
background: #dbe4ea;
|
||||
.r6-memory-mode-settings
|
||||
.memory-mode-card[data-mode="ultra"][data-selected="true"]
|
||||
.memory-mode-card-choice {
|
||||
min-height: 180px;
|
||||
}
|
||||
.r6-settings
|
||||
.r6-mode-option-menu.r6-language-option-menu
|
||||
.om-option-menu-button::after {
|
||||
right: 22px;
|
||||
.r6-evolve-settings--in-card {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
.r6-mode-option-menu.r6-language-option-menu
|
||||
.om-option-menu-button:hover:not(:disabled) {
|
||||
border-color: #a8bac5;
|
||||
background: #fbfcfd;
|
||||
box-shadow:
|
||||
inset 0 1px 0 #fff,
|
||||
0 4px 12px rgb(48 71 87 / 7%);
|
||||
}
|
||||
.r6-settings
|
||||
.r6-mode-option-menu.r6-language-option-menu
|
||||
.om-option-menu-button[aria-expanded="true"] {
|
||||
border-color: #7892a6;
|
||||
background: #fff;
|
||||
box-shadow:
|
||||
inset 0 1px 0 #fff,
|
||||
0 0 0 3px rgb(85 124 147 / 10%);
|
||||
}
|
||||
.r6-mode-option-menu .om-option-menu-copy,
|
||||
.r6-mode-option-menu-panel .om-option-menu-copy {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
.r6-mode-option-menu .om-option-menu-copy > span,
|
||||
.r6-mode-option-menu-panel .om-option-menu-copy > span {
|
||||
color: #304757;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.r6-mode-option-menu .om-option-menu-suffix,
|
||||
.r6-mode-option-menu-panel .om-option-menu-suffix {
|
||||
color: var(--ok, #2f6b57);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.r6-mode-option-menu .om-option-menu-copy > small,
|
||||
.r6-mode-option-menu-panel .om-option-menu-copy > small {
|
||||
color: #526674;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.01em;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.r6-language-option-menu-panel--settings.r6-mode-option-menu-panel {
|
||||
padding: 8px;
|
||||
border-color: #cbd6de;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 14px 32px rgb(48 71 87 / 12%);
|
||||
}
|
||||
.r6-language-option-menu-panel--settings.r6-mode-option-menu-panel
|
||||
[role="option"] {
|
||||
min-height: 64px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
.r6-language-option-menu-panel--settings.r6-mode-option-menu-panel
|
||||
[role="option"][aria-selected="true"] {
|
||||
background: #e8eff3;
|
||||
}
|
||||
.r6-language-option-menu-panel--settings.r6-mode-option-menu-panel
|
||||
[role="option"][data-active="true"] {
|
||||
background: #f0f4f6;
|
||||
box-shadow: inset 2px 0 #607b8e;
|
||||
}
|
||||
.r6-language-option-menu-panel--settings.r6-mode-option-menu-panel
|
||||
[role="option"][aria-selected="true"][data-active="true"] {
|
||||
background: #e8eff3;
|
||||
.r6-evolve-settings--in-card .r6-evolve-setting:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.r6-settings-note {
|
||||
margin: 14px 0 0;
|
||||
@@ -375,18 +261,11 @@
|
||||
margin-top: 28px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #d8e2ea;
|
||||
animation: r6-evolve-settings-in 180ms ease-out both;
|
||||
}
|
||||
.r6-evolve-settings:focus-visible {
|
||||
outline: 2px solid var(--r6-focus);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
@keyframes r6-evolve-settings-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
.r6-evolve-heading {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -813,20 +692,6 @@
|
||||
line-height: 22px;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
.r6-mode-control[data-expanded="true"] > .r6-settings-note,
|
||||
.r6-mode-control[data-expanded="true"] > .r6-settings-error {
|
||||
margin-right: 18px;
|
||||
margin-left: 18px;
|
||||
}
|
||||
.r6-mode-control[data-expanded="true"] > .r6-evolve-settings {
|
||||
margin: 18px 18px 0;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.r6-mode-control[data-expanded="true"]
|
||||
> .r6-evolve-settings
|
||||
.r6-evolve-setting--manual {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.r6-settings .r6-settings-read-status {
|
||||
color: #63707b;
|
||||
font-size: 13px;
|
||||
@@ -862,11 +727,6 @@
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
.r6-settings .r6-model-mode-tier {
|
||||
color: var(--ok, #2f6b57);
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.r6-settings .r6-model-mode-status p {
|
||||
margin: 2px 0 0;
|
||||
color: var(--r6-muted);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MemHub 安装与发行
|
||||
|
||||
MemHub `v1.0.6`(build 45)的发行目标如下:
|
||||
MemHub `v1.0.6`(build 44)的发行目标如下:
|
||||
|
||||
| 平台 | 发行物 | 构建与验收 |
|
||||
| --- | --- | --- |
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "one-memory",
|
||||
"version": "1.0.6",
|
||||
"release": {
|
||||
"build_number": 45,
|
||||
"build_number": 44,
|
||||
"macos_min_version": "13.0",
|
||||
"app_update": "manual",
|
||||
"runtime_manifest_schema": 1,
|
||||
|
||||
@@ -62,7 +62,6 @@ const modeChineseMessages = {
|
||||
"模式切换未能确认,仍显示上次确认的模式。":
|
||||
"模式选择未能确认,仍显示上次确认的模式。",
|
||||
"自动整合设置未能保存,请重试。": "自动整合设置未能保存,请重试。",
|
||||
"自动整合设置暂时无法读取,请重试。": "自动整合设置暂时无法读取,请重试。",
|
||||
"正在检查或切换向量模型,完成后才能整合记忆。":
|
||||
"正在检查或切换向量模型,完成后才能整合记忆。",
|
||||
"未能开始整合记忆,请重试。": "未能开始整合记忆,请重试。",
|
||||
@@ -261,8 +260,6 @@ export const modeEnglishMessages: ModeMessages = {
|
||||
"The mode change could not be confirmed. The last confirmed mode is still shown.",
|
||||
"自动整合设置未能保存,请重试。":
|
||||
"Could not save automatic consolidation settings. Please retry.",
|
||||
"自动整合设置暂时无法读取,请重试。":
|
||||
"Could not load automatic consolidation settings. Please retry.",
|
||||
"正在检查或切换向量模型,完成后才能整合记忆。":
|
||||
"The embedding model is being checked or changed. Wait for it to finish before consolidating memories.",
|
||||
"未能开始整合记忆,请重试。":
|
||||
@@ -469,8 +466,6 @@ export const modeSpanishMessages: ModeMessages = {
|
||||
"No se pudo confirmar el cambio. Se sigue mostrando el último modo confirmado.",
|
||||
"自动整合设置未能保存,请重试。":
|
||||
"No se pudo guardar la consolidación automática. Reinténtalo.",
|
||||
"自动整合设置暂时无法读取,请重试。":
|
||||
"No se pudo cargar la configuración de consolidación automática. Reinténtalo.",
|
||||
"正在检查或切换向量模型,完成后才能整合记忆。":
|
||||
"Se está comprobando o cambiando el modelo de embeddings. Espera para consolidar memorias.",
|
||||
"未能开始整合记忆,请重试。":
|
||||
@@ -671,7 +666,6 @@ export const modeTraditionalChineseMessages: ModeMessages = {
|
||||
"模式切换未能确认,仍显示上次确认的模式。":
|
||||
"無法確認模式切換,仍顯示上次確認的模式。",
|
||||
"自动整合设置未能保存,请重试。": "自動整合設定無法儲存,請重試。",
|
||||
"自动整合设置暂时无法读取,请重试。": "暫時無法讀取自動整合設定,請重試。",
|
||||
"正在检查或切换向量模型,完成后才能整合记忆。":
|
||||
"正在檢查或切換向量模型,完成後才能整合記憶。",
|
||||
"未能开始整合记忆,请重试。": "無法開始整合記憶,請重試。",
|
||||
@@ -954,8 +948,6 @@ export const modeJapaneseMessages: ModeMessages = {
|
||||
"モード変更を確認できませんでした。最後に確認されたモードを表示しています。",
|
||||
"自动整合设置未能保存,请重试。":
|
||||
"自動統合の設定を保存できませんでした。もう一度お試しください。",
|
||||
"自动整合设置暂时无法读取,请重试。":
|
||||
"自動統合の設定を読み込めませんでした。もう一度お試しください。",
|
||||
"正在检查或切换向量模型,完成后才能整合记忆。":
|
||||
"埋め込みモデルを確認または切り替えています。完了後に記憶を統合できます。",
|
||||
"未能开始整合记忆,请重试。":
|
||||
@@ -1161,8 +1153,6 @@ export const modeGermanMessages: ModeMessages = {
|
||||
"Der Moduswechsel konnte nicht bestätigt werden. Der zuletzt bestätigte Modus wird weiterhin angezeigt.",
|
||||
"自动整合设置未能保存,请重试。":
|
||||
"Die Einstellungen zur automatischen Konsolidierung konnten nicht gespeichert werden. Bitte erneut versuchen.",
|
||||
"自动整合设置暂时无法读取,请重试。":
|
||||
"Die Einstellungen zur automatischen Konsolidierung konnten nicht geladen werden. Bitte erneut versuchen.",
|
||||
"正在检查或切换向量模型,完成后才能整合记忆。":
|
||||
"Das Embedding-Modell wird geprüft oder gewechselt. Danach können Erinnerungen konsolidiert werden.",
|
||||
"未能开始整合记忆,请重试。":
|
||||
@@ -1369,8 +1359,6 @@ export const modePortugueseMessages: ModeMessages = {
|
||||
"Não foi possível confirmar a mudança. Continua a ser mostrado o último modo confirmado.",
|
||||
"自动整合设置未能保存,请重试。":
|
||||
"Não foi possível guardar a consolidação automática. Tente novamente.",
|
||||
"自动整合设置暂时无法读取,请重试。":
|
||||
"Não foi possível carregar as configurações de consolidação automática. Tente novamente.",
|
||||
"正在检查或切换向量模型,完成后才能整合记忆。":
|
||||
"O modelo de embeddings está a ser verificado ou alterado. Aguarde para consolidar memórias.",
|
||||
"未能开始整合记忆,请重试。":
|
||||
@@ -1579,8 +1567,6 @@ export const modeFrenchMessages: ModeMessages = {
|
||||
"Le changement n’a pas pu être confirmé. Le dernier mode confirmé reste affiché.",
|
||||
"自动整合设置未能保存,请重试。":
|
||||
"Impossible d’enregistrer la consolidation automatique. Réessayez.",
|
||||
"自动整合设置暂时无法读取,请重试。":
|
||||
"Impossible de charger les paramètres de consolidation automatique. Réessayez.",
|
||||
"正在检查或切换向量模型,完成后才能整合记忆。":
|
||||
"Le modèle d’embeddings est en cours de vérification ou de changement. Patientez avant de consolider les mémoires.",
|
||||
"未能开始整合记忆,请重试。":
|
||||
@@ -1782,8 +1768,6 @@ export const modeKoreanMessages: ModeMessages = {
|
||||
"모드 전환을 확인하지 못했습니다. 마지막으로 확인된 모드가 계속 표시됩니다.",
|
||||
"自动整合设置未能保存,请重试。":
|
||||
"자동 통합 설정을 저장하지 못했습니다. 다시 시도하세요.",
|
||||
"自动整合设置暂时无法读取,请重试。":
|
||||
"자동 통합 설정을 불러오지 못했습니다. 다시 시도하세요.",
|
||||
"正在检查或切换向量模型,完成后才能整合记忆。":
|
||||
"임베딩 모델을 확인하거나 전환 중입니다. 완료된 후 메모리를 통합할 수 있습니다.",
|
||||
"未能开始整合记忆,请重试。":
|
||||
@@ -1987,8 +1971,6 @@ export const modeRussianMessages: ModeMessages = {
|
||||
"Не удалось подтвердить смену режима. По-прежнему показан последний подтверждённый режим.",
|
||||
"自动整合设置未能保存,请重试。":
|
||||
"Не удалось сохранить настройки автообъединения. Повторите попытку.",
|
||||
"自动整合设置暂时无法读取,请重试。":
|
||||
"Не удалось загрузить настройки автообъединения. Повторите попытку.",
|
||||
"正在检查或切换向量模型,完成后才能整合记忆。":
|
||||
"Модель эмбеддингов проверяется или меняется. После завершения можно будет объединить воспоминания.",
|
||||
"未能开始整合记忆,请重试。":
|
||||
@@ -2194,8 +2176,6 @@ export const modeIndonesianMessages: ModeMessages = {
|
||||
"Perubahan mode tidak dapat dikonfirmasi. Mode terakhir yang dikonfirmasi tetap ditampilkan.",
|
||||
"自动整合设置未能保存,请重试。":
|
||||
"Pengaturan konsolidasi otomatis tidak dapat disimpan. Coba lagi.",
|
||||
"自动整合设置暂时无法读取,请重试。":
|
||||
"Pengaturan konsolidasi otomatis tidak dapat dimuat. Coba lagi.",
|
||||
"正在检查或切换向量模型,完成后才能整合记忆。":
|
||||
"Model embedding sedang diperiksa atau diganti. Tunggu hingga selesai sebelum mengonsolidasikan memori.",
|
||||
"未能开始整合记忆,请重试。":
|
||||
|
||||
Reference in new issue
Block a user