Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38ea7bb041 | ||
|
|
865c66de52 |
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 44, on `hy-memory==2.0.14`. Linux x64/arm64 Runtime and macOS arm64
|
||||
Build 43, 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 44),GitHub 仓库为
|
||||
(build 43),GitHub 仓库为
|
||||
[`Tencent-Hunyuan/Hy-Memory`](https://github.com/Tencent-Hunyuan/Hy-Memory),npm 包名为
|
||||
`memhub-cli`。当前代码架构见
|
||||
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)。
|
||||
|
||||
@@ -1786,7 +1786,6 @@ def create_app(
|
||||
else None
|
||||
)
|
||||
app.state.live_sync_watch_lock = asyncio.Lock()
|
||||
app.state.source_activation_tasks = {}
|
||||
app.state.live_sync_supervisor.set_watched_sources(
|
||||
watch_roots if watching and runtime_settings.live_sync_enabled else ()
|
||||
)
|
||||
@@ -2122,11 +2121,6 @@ def create_app(
|
||||
app.state.memory_governance_store.wait_content_candidate_backfill,
|
||||
5.0,
|
||||
)
|
||||
source_activation_tasks = tuple(app.state.source_activation_tasks.values())
|
||||
for task in source_activation_tasks:
|
||||
task.cancel()
|
||||
if source_activation_tasks:
|
||||
await asyncio.gather(*source_activation_tasks, return_exceptions=True)
|
||||
if app.state.live_sync_watch is not None:
|
||||
await app.state.live_sync_watch.shutdown()
|
||||
await app.state.live_sync_supervisor.shutdown()
|
||||
@@ -3269,69 +3263,6 @@ def create_app(
|
||||
"""`POST /api/v1/import-jobs` on one user's lane (Hermes only for another user)."""
|
||||
return await create_job_on(request.source_id, request.operation, request.user_id)
|
||||
|
||||
async def _activate_connected_sources(
|
||||
request_id: str,
|
||||
source_ids: tuple[str, ...],
|
||||
*,
|
||||
enable_sync: bool,
|
||||
) -> None:
|
||||
"""Apply one committed connection intent without holding its HTTP response."""
|
||||
try:
|
||||
for source_id in source_ids:
|
||||
if enable_sync:
|
||||
await _live_sync(app).initialize_source(
|
||||
source_id,
|
||||
baseline_missing_files=True,
|
||||
)
|
||||
else:
|
||||
await _live_sync(app).stop_source(source_id)
|
||||
if enable_sync:
|
||||
for source_id in source_ids:
|
||||
await _activate_source_resume(source_id)
|
||||
await _refresh_live_watch(app)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as error:
|
||||
diagnostic_event(
|
||||
level="error",
|
||||
component="source.connection",
|
||||
event="source.connection.activation_failed",
|
||||
outcome="failed",
|
||||
**diagnostic_exception(error),
|
||||
details={"request_id": request_id, "source_ids": list(source_ids)},
|
||||
)
|
||||
raise
|
||||
|
||||
def _schedule_source_activation(
|
||||
request_id: str,
|
||||
source_ids: tuple[str, ...],
|
||||
*,
|
||||
enable_sync: bool,
|
||||
) -> None:
|
||||
tasks = app.state.source_activation_tasks
|
||||
existing = tasks.get(request_id)
|
||||
if existing is not None and not existing.done():
|
||||
return
|
||||
task = watched_task(
|
||||
asyncio.get_running_loop(),
|
||||
lambda: _activate_connected_sources(
|
||||
request_id,
|
||||
source_ids,
|
||||
enable_sync=enable_sync,
|
||||
),
|
||||
component="source.connection",
|
||||
name=f"source-connection-{request_id}",
|
||||
)
|
||||
if task is None:
|
||||
return
|
||||
tasks[request_id] = task
|
||||
|
||||
def forget(completed: asyncio.Future[None]) -> None:
|
||||
if tasks.get(request_id) is completed:
|
||||
tasks.pop(request_id, None)
|
||||
|
||||
task.add_done_callback(forget)
|
||||
|
||||
@app.post(
|
||||
"/api/v1/sources/connect",
|
||||
response_model=ConnectSourcesResponse,
|
||||
@@ -3421,21 +3352,44 @@ def create_app(
|
||||
if grant is not None:
|
||||
_supervisor(app).register_adapter(registration.create_adapter(grant))
|
||||
_detection(app).pop(job.source_id, None)
|
||||
if request.enable_sync:
|
||||
await _prepare_source_resume(job.source_id)
|
||||
else:
|
||||
await _live_sync(app).stop_source(job.source_id)
|
||||
configured.append(job.source_id)
|
||||
if configured:
|
||||
await asyncio.to_thread(
|
||||
_config_store(app).set_sync_many,
|
||||
{source_id: request.enable_sync for source_id in configured},
|
||||
)
|
||||
if request.enable_sync:
|
||||
for job in result.jobs:
|
||||
if job.source_id in configured and job.status is ImportStatus.PENDING:
|
||||
await _supervisor(app).enqueue_off_loop(job.job_id, job=job)
|
||||
_schedule_source_activation(
|
||||
request.request_id,
|
||||
tuple(configured),
|
||||
enable_sync=request.enable_sync,
|
||||
)
|
||||
previous = {
|
||||
source_id: _config_store(app).sync_enabled(source_id) for source_id in configured
|
||||
}
|
||||
try:
|
||||
await asyncio.to_thread(
|
||||
_config_store(app).set_sync_many,
|
||||
{source_id: request.enable_sync for source_id in configured},
|
||||
)
|
||||
except BaseException:
|
||||
for source_id in configured:
|
||||
if request.enable_sync:
|
||||
await _live_sync(app).stop_source(source_id)
|
||||
elif previous[source_id]:
|
||||
await _prepare_source_resume(source_id)
|
||||
await _activate_source_resume(source_id)
|
||||
await _refresh_live_watch(app)
|
||||
raise
|
||||
try:
|
||||
if request.enable_sync:
|
||||
for source_id in configured:
|
||||
await _activate_source_resume(source_id)
|
||||
await _refresh_live_watch(app)
|
||||
except BaseException:
|
||||
await asyncio.to_thread(_config_store(app).set_sync_many, previous)
|
||||
for source_id in configured:
|
||||
if request.enable_sync:
|
||||
await _live_sync(app).stop_source(source_id)
|
||||
elif previous[source_id]:
|
||||
await _prepare_source_resume(source_id)
|
||||
await _activate_source_resume(source_id)
|
||||
await _refresh_live_watch(app)
|
||||
raise
|
||||
return ConnectSourcesResponse(**result.model_dump())
|
||||
|
||||
async def _refuse_overlapping_root(source_id: str, root: Path, user_id: str) -> None:
|
||||
|
||||
@@ -376,9 +376,8 @@ def apply_sdk_language(config: MemoryConfig, language: str | None) -> MemoryConf
|
||||
"""Set the SDK prompt language (`MemoryConfig.language`), when there is one.
|
||||
|
||||
None (no language set) sets nothing: the SDK detects zh/en as it always
|
||||
did. Every display language, zh-Hans included, is set explicitly. Like a
|
||||
feature, the language never touches `vector_store`, so the collection
|
||||
identity never depends on it.
|
||||
did. Every display language, zh-Hans included, is set explicitly. Like a feature, the language never touches `vector_store`, so
|
||||
the collection identity never depends on it.
|
||||
"""
|
||||
if language is not None:
|
||||
config.language = language
|
||||
|
||||
@@ -197,82 +197,6 @@ def test_connection_retry_reuses_completed_job_and_checks_root_snapshot(tmp_path
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_connection_returns_committed_receipt_before_live_activation_finishes(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr("one_memory.model_presets._preset_data", lambda: None)
|
||||
|
||||
async def scenario():
|
||||
home = tmp_path / "home"
|
||||
root = home / ".claude/projects"
|
||||
root.mkdir(parents=True)
|
||||
app = create_app(
|
||||
Settings(
|
||||
data_dir=tmp_path / "data",
|
||||
source_home=home,
|
||||
api_token=SecretStr("test"),
|
||||
automatic_memory_processing=False,
|
||||
live_sync_enabled=False,
|
||||
),
|
||||
source_catalog=build_source_catalog(home=home, environment={}),
|
||||
)
|
||||
async with (
|
||||
app.router.lifespan_context(app),
|
||||
AsyncClient(
|
||||
transport=ASGITransport(app=app),
|
||||
base_url="http://test",
|
||||
headers={"Authorization": "Bearer test"},
|
||||
) as client,
|
||||
):
|
||||
found = await client.post("/api/v1/sources/claude-code/location")
|
||||
started = asyncio.Event()
|
||||
release = asyncio.Event()
|
||||
original = app.state.live_sync_supervisor.initialize_source
|
||||
|
||||
async def blocked_initialize(source_id, *, baseline_missing_files):
|
||||
started.set()
|
||||
await release.wait()
|
||||
return await original(
|
||||
source_id,
|
||||
baseline_missing_files=baseline_missing_files,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
app.state.live_sync_supervisor,
|
||||
"initialize_source",
|
||||
blocked_initialize,
|
||||
)
|
||||
try:
|
||||
response = await asyncio.wait_for(
|
||||
client.post(
|
||||
"/api/v1/sources/connect",
|
||||
json={
|
||||
"source_ids": ["claude-code"],
|
||||
"request_id": "fast-receipt",
|
||||
"source_roots": {
|
||||
"claude-code": found.json()["root_id"],
|
||||
},
|
||||
"onboarding": True,
|
||||
"full_rescan": True,
|
||||
"enable_sync": True,
|
||||
},
|
||||
),
|
||||
timeout=1,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["request_id"] == "fast-receipt"
|
||||
assert app.state.config_store.sync_enabled("claude-code")
|
||||
await asyncio.wait_for(started.wait(), timeout=1)
|
||||
assert not release.is_set()
|
||||
finally:
|
||||
release.set()
|
||||
tasks = tuple(app.state.source_activation_tasks.values())
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("turn_count", "age_days", "limited"),
|
||||
[
|
||||
|
||||
@@ -46,7 +46,6 @@ export function defaultHostSourceIds(
|
||||
export type ImportConnectionDraft = {
|
||||
hostSourceIds: readonly string[];
|
||||
};
|
||||
export type ImportSubmissionState = "confirmed" | "checking" | "refused";
|
||||
export interface FirstImportReviewProps {
|
||||
sourceIds: readonly string[];
|
||||
sources: SourceResponse[];
|
||||
@@ -55,7 +54,6 @@ export interface FirstImportReviewProps {
|
||||
acceptedJobIds?: readonly string[];
|
||||
failedSources?: Record<string, string>;
|
||||
sourceSnapshot?: { confirmedAt: string | null; stale: boolean };
|
||||
importSubmissionState?: ImportSubmissionState;
|
||||
busy?: boolean;
|
||||
error?: string | null;
|
||||
connectionNote?: string;
|
||||
@@ -80,7 +78,6 @@ export function FirstImportReview({
|
||||
acceptedJobIds,
|
||||
failedSources = {},
|
||||
sourceSnapshot,
|
||||
importSubmissionState = "confirmed",
|
||||
busy = false,
|
||||
error,
|
||||
connectionNote,
|
||||
@@ -332,24 +329,13 @@ export function FirstImportReview({
|
||||
>
|
||||
<div className="first-import-r6-intro">
|
||||
<h2>{text("正在导入你的会话")}</h2>
|
||||
{importSubmissionState === "checking" ? (
|
||||
<p className="first-import-r6-submission-status" role="status">
|
||||
{t("onboardingImportChecking")}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{text(
|
||||
allCompleted
|
||||
? "本次导入已结束。已保存的对话无需重复导入。"
|
||||
: "后台将持续为你导入,可以放心进入下一步",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{ids.length > 3 && (
|
||||
<p className="first-import-r6-scroll-hint">
|
||||
{t("onboardingMoreImportApps", { count: ids.length - 3 })}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
{text(
|
||||
allCompleted
|
||||
? "本次导入已结束。已保存的对话无需重复导入。"
|
||||
: "后台将持续为你导入,可以放心进入下一步",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{(error || actionMessage) && (
|
||||
<div className="first-import-r6-feedback">
|
||||
|
||||
@@ -467,10 +467,9 @@ describe("OnboardingFlow S1.3", () => {
|
||||
name: "对话正在汇入,记忆从这里开始",
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByRole("alert")).toBeNull();
|
||||
expect(
|
||||
screen.getByText("正在核对导入结果,已保存的选择不会丢失"),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole("alert").textContent).toContain(
|
||||
"开始导入的结果尚未确认",
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole("heading", { name: "把熟悉的背景,带到这里" }),
|
||||
).toBeNull();
|
||||
@@ -553,11 +552,11 @@ describe("OnboardingFlow S1.3", () => {
|
||||
modelBoundary,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "返回导入" }));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("正在核对导入结果,已保存的选择不会丢失"),
|
||||
).toBeTruthy();
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole("alert").textContent).toContain(
|
||||
"开始导入的结果尚未确认",
|
||||
),
|
||||
);
|
||||
expect(screen.getByRole("heading", { name: "选择记忆模式" })).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -2691,7 +2690,7 @@ describe("OnboardingFlow S1.3", () => {
|
||||
expect(controller.connectForOnboarding).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps an unknown connection visible without reporting an error or duplicating the POST", async () => {
|
||||
it("keeps an unknown connection visible and reconciles its receipt without a duplicate POST", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onFinish = vi.fn();
|
||||
vi.stubGlobal("crypto", webcrypto);
|
||||
@@ -2707,7 +2706,6 @@ describe("OnboardingFlow S1.3", () => {
|
||||
vi.mocked(api.getSourceConnection).mockRejectedValueOnce(
|
||||
new Error("receipt offline"),
|
||||
);
|
||||
window.oneMemory = {} as NonNullable<typeof window.oneMemory>;
|
||||
function Harness() {
|
||||
const controller = useImportSources({ automaticSync: false });
|
||||
return controller.loading ? (
|
||||
@@ -2725,9 +2723,9 @@ describe("OnboardingFlow S1.3", () => {
|
||||
await choose(user);
|
||||
await approve(user);
|
||||
await user.click(screen.getByRole("button", { name: START }));
|
||||
await screen.findByText("正在核对导入结果,已保存的选择不会丢失");
|
||||
await screen.findByRole("alert");
|
||||
expect(onFinish).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole("alert")).toBeNull();
|
||||
expect(screen.getByRole("alert").textContent).toContain("尚未确认");
|
||||
expect(completeOnboarding).not.toHaveBeenCalled();
|
||||
expect(api.connectSources).toHaveBeenCalledOnce();
|
||||
const requestId = vi.mocked(api.connectSources).mock.calls[0][2]?.requestId;
|
||||
@@ -2740,9 +2738,26 @@ describe("OnboardingFlow S1.3", () => {
|
||||
requestId,
|
||||
}),
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: START }));
|
||||
await waitFor(() =>
|
||||
expect(api.getSourceConnection).toHaveBeenCalledTimes(2),
|
||||
);
|
||||
expect(api.getSourceConnection).toHaveBeenLastCalledWith(requestId);
|
||||
expect(api.connectSources).toHaveBeenCalledOnce();
|
||||
expect(api.getSourceConnection).toHaveBeenCalledOnce();
|
||||
expect(api.getSourceConnection).toHaveBeenCalledWith(requestId);
|
||||
expect(screen.getByRole("alert").textContent).toContain("尚未确认");
|
||||
vi.mocked(api.getSourceConnection).mockResolvedValueOnce({
|
||||
request_id: requestId,
|
||||
accepted_source_ids: ["claude"],
|
||||
failed_sources: {},
|
||||
jobs: [],
|
||||
});
|
||||
await openApps();
|
||||
await user.click(screen.getByRole("button", { name: START }));
|
||||
await screen.findByRole("button", { name: "选择记忆模式" });
|
||||
await continueImport(user);
|
||||
expect(onFinish).toHaveBeenCalledWith("sources");
|
||||
expect(api.connectSources).toHaveBeenCalledOnce();
|
||||
expect(api.getSourceConnection).toHaveBeenCalledTimes(3);
|
||||
expect(api.createImportJob).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
defaultHostSourceIds,
|
||||
FirstImportReview,
|
||||
type ImportConnectionDraft,
|
||||
type ImportSubmissionState,
|
||||
} from "./FirstImportReview";
|
||||
import {
|
||||
enableHostConnection,
|
||||
@@ -135,8 +134,6 @@ export function OnboardingFlow({
|
||||
const [startingImport, setStartingImport] = useState<SourceResponse[] | null>(
|
||||
null,
|
||||
);
|
||||
const [importSubmissionState, setImportSubmissionState] =
|
||||
useState<ImportSubmissionState>("confirmed");
|
||||
const resumedStep =
|
||||
state.mode_intent &&
|
||||
state.mode_intent !== "lite" &&
|
||||
@@ -1061,7 +1058,6 @@ export function OnboardingFlow({
|
||||
|
||||
async function submitImport(approved: SourceResponse[]) {
|
||||
sourceFlight.current = true;
|
||||
setImportSubmissionState("checking");
|
||||
if (window.oneMemory) setStartingImport(approved);
|
||||
const version = scopeVersion.current;
|
||||
const retryFailed = Object.keys(itemErrors).length > 0;
|
||||
@@ -1073,11 +1069,8 @@ export function OnboardingFlow({
|
||||
try {
|
||||
current = (await imports.refresh()).sources;
|
||||
} catch {
|
||||
if (alive.current) {
|
||||
setStartingImport(null);
|
||||
setImportSubmissionState("refused");
|
||||
if (alive.current)
|
||||
setError("暂时无法核对来源,尚未提交授权。选择已保留,可以重试。");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!alive.current) return;
|
||||
@@ -1100,7 +1093,6 @@ export function OnboardingFlow({
|
||||
setItemErrors(result.failed_sources);
|
||||
if (result.accepted_source_ids.length === 0) {
|
||||
setStartingImport(null);
|
||||
setImportSubmissionState("refused");
|
||||
setError(
|
||||
"本次没有来源获准导入。请核对下方原因后重试,或到应用页查看状态。",
|
||||
);
|
||||
@@ -1109,25 +1101,16 @@ export function OnboardingFlow({
|
||||
// Keep the accepted intent and job IDs while the user inspects early originals.
|
||||
setAccepted(result);
|
||||
setStartingImport(null);
|
||||
setImportSubmissionState("confirmed");
|
||||
setError(null);
|
||||
} catch (reason) {
|
||||
if (alive.current) {
|
||||
const refused = isDefiniteAccessRefusal(reason);
|
||||
if (refused) {
|
||||
setStartingImport(null);
|
||||
setImportSubmissionState("refused");
|
||||
setError(
|
||||
"本次没有来源获准导入。请核对下方原因后重试,或到应用页查看状态。",
|
||||
);
|
||||
} else {
|
||||
setImportSubmissionState("checking");
|
||||
setError(
|
||||
window.oneMemory
|
||||
? null
|
||||
: "开始导入的结果尚未确认,选择已保留。可以重试,或进入应用页核对。",
|
||||
);
|
||||
}
|
||||
if (refused) setStartingImport(null);
|
||||
setError(
|
||||
refused
|
||||
? "本次没有来源获准导入。请核对下方原因后重试,或到应用页查看状态。"
|
||||
: "开始导入的结果尚未确认,选择已保留。可以重试,或进入应用页核对。",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
connectSent.current = false;
|
||||
@@ -1203,7 +1186,6 @@ export function OnboardingFlow({
|
||||
jobs={reviewJobs}
|
||||
failedSources={accepted?.failed_sources}
|
||||
sourceSnapshot={imports.sourceSnapshot}
|
||||
importSubmissionState={importSubmissionState}
|
||||
busy={completionBusy}
|
||||
error={error && text(error)}
|
||||
onContinue={(destination) => {
|
||||
|
||||
@@ -76,9 +76,7 @@ describe("OnboardingModelStep", () => {
|
||||
localStorage.setItem(LANGUAGE_STORAGE_KEY, locale);
|
||||
const title = guiMessages[locale]["配置记忆整理模型"];
|
||||
const back = guiMessages[locale]["返回选择模式"];
|
||||
const summary = guiMessages[locale]["记忆整理"];
|
||||
if (!title || !back || !summary)
|
||||
throw new Error(`missing ${locale} model copy`);
|
||||
if (!title || !back) throw new Error(`missing ${locale} model copy`);
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<OnboardingModelStep {...props()} />
|
||||
@@ -95,11 +93,10 @@ describe("OnboardingModelStep", () => {
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByLabelText(/API Key/)).toBeTruthy();
|
||||
expect(screen.getByRole("complementary", { name: summary })).toBeTruthy();
|
||||
expect(screen.getAllByRole("button").length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
it("keeps current behavior in the restored open-source composition", () => {
|
||||
it("keeps step 3 on memory mode in a generation-only column", () => {
|
||||
render(<OnboardingModelStep {...props()} />);
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "配置记忆整理模型" }),
|
||||
@@ -113,9 +110,12 @@ describe("OnboardingModelStep", () => {
|
||||
expect(screen.getByText("DeepSeek · deepseek-flash")).toBeTruthy();
|
||||
expect(screen.queryByText("本次沉淀")).toBeNull();
|
||||
expect(screen.queryByLabelText(/向量|Embedding/)).toBeNull();
|
||||
expect(document.querySelector(".onboarding-model-layout")).toBeTruthy();
|
||||
expect(document.querySelector(".onboarding-model-summary")).toBeTruthy();
|
||||
expect(screen.getByText("让记忆从这里开始")).toBeTruthy();
|
||||
expect(
|
||||
document.querySelector(
|
||||
".onboarding-model-layout--single-column-600" +
|
||||
'[data-responsive="single-column-600"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("focuses the page title when entering the model substep", async () => {
|
||||
@@ -181,18 +181,6 @@ describe("OnboardingModelStep", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns to the visible API key field when it fails in advanced mode", async () => {
|
||||
const p = props();
|
||||
p.advancedOpen = true;
|
||||
p.fieldErrors = { api_key: "请填写 API Key。" };
|
||||
const view = render(<OnboardingModelStep {...p} />);
|
||||
await waitFor(() => expect(p.onAdvancedChange).toHaveBeenCalledWith(false));
|
||||
view.rerender(<OnboardingModelStep {...p} advancedOpen={false} />);
|
||||
await waitFor(() =>
|
||||
expect(document.activeElement).toBe(screen.getByLabelText("API Key")),
|
||||
);
|
||||
});
|
||||
|
||||
it("links field errors and keeps secrets out of callback errors", async () => {
|
||||
const p = props();
|
||||
p.fieldErrors = { api_key: "请填写 API Key。" };
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
import { useOnboardingTranslation } from "./onboarding-language";
|
||||
import { R6ColdStartFrame } from "./R6ColdStartFrame";
|
||||
import { R6Button } from "./R6Controls";
|
||||
import "./onboarding-model-r6.css";
|
||||
|
||||
type Action = "test" | "save-and-enable";
|
||||
|
||||
@@ -61,10 +60,6 @@ export function OnboardingModelStepNext(props: OnboardingModelStepProps) {
|
||||
props.onAdvancedChange(true);
|
||||
return;
|
||||
}
|
||||
if (firstError === "api_key" && props.advancedOpen) {
|
||||
props.onAdvancedChange(false);
|
||||
return;
|
||||
}
|
||||
if (firstError && (!technicalError || props.advancedOpen)) {
|
||||
formRef.current
|
||||
?.querySelector<HTMLElement>(`[name="${firstError}"]`)
|
||||
@@ -178,7 +173,10 @@ export function OnboardingModelStepNext(props: OnboardingModelStepProps) {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="onboarding-model-layout">
|
||||
<div
|
||||
className="onboarding-model-layout onboarding-model-layout--single-column-600"
|
||||
data-responsive="single-column-600"
|
||||
>
|
||||
<section
|
||||
ref={formRef}
|
||||
className="onboarding-model-form"
|
||||
@@ -186,9 +184,6 @@ export function OnboardingModelStepNext(props: OnboardingModelStepProps) {
|
||||
data-advanced={props.advancedOpen}
|
||||
>
|
||||
<div className="onboarding-model-group">
|
||||
<p className="onboarding-model-intro">
|
||||
{text("选择服务商,填入 API Key。核对已有配置后再继续。")}
|
||||
</p>
|
||||
<label
|
||||
className="onboarding-model-field onboarding-model-provider"
|
||||
htmlFor={`${id}-provider`}
|
||||
@@ -268,6 +263,16 @@ export function OnboardingModelStepNext(props: OnboardingModelStepProps) {
|
||||
)}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="onboarding-model-disclosure">
|
||||
<p>
|
||||
{text("相关对话会发送给所选服务商。")}
|
||||
{sourceSummary ? ` ${sourceSummary}` : ""}
|
||||
</p>
|
||||
<p>
|
||||
{text("密钥保存在系统安全存储中,不写入对话或日志。")}{" "}
|
||||
{text("测试只向此接口发送测试内容,不发送历史对话。")}
|
||||
</p>
|
||||
</div>
|
||||
<GenerationTechnicalDisclosure
|
||||
open={props.advancedOpen}
|
||||
disabled={fieldsLocked}
|
||||
@@ -343,35 +348,6 @@ export function OnboardingModelStepNext(props: OnboardingModelStepProps) {
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
<aside
|
||||
className="onboarding-model-summary"
|
||||
aria-label={text("记忆整理")}
|
||||
>
|
||||
<div className="onboarding-model-summary-heading">
|
||||
<p>{text("记忆整理")}</p>
|
||||
<h2>{text("让记忆从这里开始")}</h2>
|
||||
</div>
|
||||
<div className="onboarding-model-connections">
|
||||
<p>{text("对话来源")}</p>
|
||||
<strong>{sourceSummary || text("尚无已选来源")}</strong>
|
||||
<small>
|
||||
{t("onboardingImportedConversations", {
|
||||
count: props.importedConversationCount ?? 0,
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
<div className="onboarding-model-summary-status">
|
||||
<small>
|
||||
{text("处理服务:")}
|
||||
{text(provider?.label ?? "待选择")}
|
||||
</small>
|
||||
<small>
|
||||
{text("相关对话会发送给所选服务商。")}
|
||||
<br />
|
||||
{text("测试只向此接口发送测试内容,不发送历史对话。")}
|
||||
</small>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</R6ColdStartFrame>
|
||||
</div>
|
||||
|
||||
@@ -160,7 +160,7 @@ it.each(["missing", "pending"] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
it("centers one to three apps and keeps four or more in a scrolling row", () => {
|
||||
it("centers one or two apps and keeps three or more in a scrolling row", () => {
|
||||
function shown(ids: string[]) {
|
||||
const view = render(
|
||||
<R6ImportProgress
|
||||
@@ -181,7 +181,7 @@ it("centers one to three apps and keeps four or more in a scrolling row", () =>
|
||||
}
|
||||
expect(shown(["cursor"])).toBe("1");
|
||||
expect(shown(["cursor", "codex"])).toBe("2");
|
||||
expect(shown(["cursor", "codex", "claude-code"])).toBe("3");
|
||||
expect(shown(["cursor", "codex", "claude-code"])).toBe("many");
|
||||
expect(shown(["cursor", "codex", "claude-code", "workbuddy"])).toBe("many");
|
||||
const four = render(
|
||||
<R6ImportProgress
|
||||
@@ -200,38 +200,6 @@ it("centers one to three apps and keeps four or more in a scrolling row", () =>
|
||||
.querySelector(".r6-import-progress")
|
||||
?.getAttribute("data-count"),
|
||||
).toBe("many");
|
||||
expect(
|
||||
four.container
|
||||
.querySelector(".r6-import-progress")
|
||||
?.getAttribute("tabindex"),
|
||||
).toBe("0");
|
||||
});
|
||||
|
||||
it("scrolls one card at a time from the keyboard", () => {
|
||||
const ids = ["cursor", "codex", "claude-code", "workbuddy"];
|
||||
render(
|
||||
<R6ImportProgress
|
||||
sourceIds={ids}
|
||||
sources={ids.map((id) => ({
|
||||
...source,
|
||||
source_id: id,
|
||||
display_name: id,
|
||||
}))}
|
||||
jobs={[]}
|
||||
/>,
|
||||
);
|
||||
const progress = screen.getByRole("region", {
|
||||
name: "本次应用导入进度",
|
||||
});
|
||||
Object.defineProperty(progress.firstElementChild, "offsetWidth", {
|
||||
configurable: true,
|
||||
value: 280,
|
||||
});
|
||||
progress.scrollBy = ((options: ScrollToOptions) => {
|
||||
progress.scrollLeft += Number(options.left ?? 0);
|
||||
}) as typeof progress.scrollBy;
|
||||
fireEvent.keyDown(progress, { key: "ArrowRight" });
|
||||
expect(progress.scrollLeft).toBe(296);
|
||||
});
|
||||
|
||||
it("keeps the first app anchored across receipts unless the user scrolls", () => {
|
||||
|
||||
@@ -70,39 +70,18 @@ export function R6ImportProgress({
|
||||
});
|
||||
|
||||
const ids = [...new Set(sourceIds)];
|
||||
const scrollable = ids.length > 3;
|
||||
return (
|
||||
<section
|
||||
className="r6-import-progress"
|
||||
aria-label={text("本次应用导入进度")}
|
||||
aria-keyshortcuts={
|
||||
scrollable ? "ArrowLeft ArrowRight Home End" : undefined
|
||||
}
|
||||
data-count={ids.length <= 3 ? String(ids.length) : "many"}
|
||||
data-count={ids.length <= 2 ? String(ids.length) : "many"}
|
||||
ref={scroller}
|
||||
tabIndex={scrollable ? 0 : -1}
|
||||
onPointerDown={markUserScrollIntent}
|
||||
onTouchStart={markUserScrollIntent}
|
||||
onWheel={markUserScrollIntent}
|
||||
onKeyDown={(event) => {
|
||||
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key))
|
||||
return;
|
||||
markUserScrollIntent();
|
||||
const element = event.currentTarget;
|
||||
const card = element.querySelector<HTMLElement>(
|
||||
".r6-import-progress-card",
|
||||
);
|
||||
const step = (card?.offsetWidth ?? 0) + 16;
|
||||
event.preventDefault();
|
||||
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
||||
element.scrollBy({
|
||||
left: event.key === "ArrowRight" ? step : -step,
|
||||
});
|
||||
} else {
|
||||
element.scrollTo({
|
||||
left: event.key === "End" ? element.scrollWidth : 0,
|
||||
});
|
||||
}
|
||||
if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key))
|
||||
markUserScrollIntent();
|
||||
}}
|
||||
onScroll={(event) => {
|
||||
if (userScrollIntent.current)
|
||||
@@ -154,7 +133,7 @@ export function R6ImportProgress({
|
||||
<div className="r6-import-progress-orbit" aria-hidden="true">
|
||||
<span />
|
||||
<div className="r6-import-progress-icon">
|
||||
<AppMark id={id} label={source?.display_name ?? id} size={40} />
|
||||
<AppMark id={id} label={source?.display_name ?? id} size={30} />
|
||||
</div>
|
||||
</div>
|
||||
<h2>{source?.display_name ?? id}</h2>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
.first-import-r6-intro {
|
||||
position: relative;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.first-import-r6-intro h2 {
|
||||
@@ -45,104 +44,97 @@
|
||||
margin-inline: auto;
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress[data-count="1"] {
|
||||
grid-auto-columns: min(320px, 100%);
|
||||
justify-content: center;
|
||||
width: min(680px, 100%);
|
||||
grid-auto-columns: 100%;
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress[data-count="2"] {
|
||||
grid-auto-columns: min(320px, calc((100% - 16px) / 2));
|
||||
justify-content: center;
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress[data-count="3"] {
|
||||
grid-auto-columns: calc((100% - 32px) / 3);
|
||||
justify-content: center;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress[data-count="many"] {
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: calc((100% - 32px) / 3);
|
||||
justify-content: start;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scroll-snap-type: x mandatory;
|
||||
scrollbar-color: rgb(102 128 145 / 50%) transparent;
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress:focus-visible {
|
||||
outline: 2px solid #557c93;
|
||||
outline-offset: 3px;
|
||||
width: min(720px, 100%);
|
||||
grid-auto-columns: calc((100% - 16px) / 2);
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress .r6-import-progress-card {
|
||||
min-height: 128px;
|
||||
padding: 16px 20px;
|
||||
min-height: 112px;
|
||||
padding: 12px 18px;
|
||||
display: grid;
|
||||
grid-template-columns: 92px minmax(0, 1fr);
|
||||
grid-template-columns: 82px minmax(0, 1fr);
|
||||
grid-template-rows: auto auto auto;
|
||||
align-content: center;
|
||||
column-gap: 18px;
|
||||
column-gap: 16px;
|
||||
text-align: left;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress .r6-import-progress-orbit {
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 4;
|
||||
margin: 0;
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress .r6-import-progress-icon {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress :is(h2, p, small) {
|
||||
grid-column: 2;
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress .r6-import-progress-card h2 {
|
||||
align-self: end;
|
||||
}
|
||||
.first-import-r6-intro .first-import-r6-submission-status {
|
||||
min-height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: #41697d;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
.first-import-r6-main > .r6-import-progress[data-count="many"] {
|
||||
grid-auto-flow: row;
|
||||
grid-auto-columns: unset;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.first-import-r6-submission-status::before {
|
||||
content: "";
|
||||
flex: 0 0 auto;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1.5px solid rgb(88 124 144 / 28%);
|
||||
border-top-color: #587c90;
|
||||
border-radius: 50%;
|
||||
animation: first-import-r6-checking 1.2s linear infinite;
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:not(:has(> :nth-child(4))) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
.first-import-r6-intro:has(.first-import-r6-submission-status) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(4)):not(
|
||||
:has(> :nth-child(5))
|
||||
) {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
.first-import-r6-intro:has(.first-import-r6-submission-status) h2 {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(5)):not(
|
||||
:has(> :nth-child(6))
|
||||
) {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
.first-import-r6-intro .first-import-r6-scroll-hint {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
color: #6b7e8b;
|
||||
font-size: 11px;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(6)):not(
|
||||
:has(> :nth-child(7))
|
||||
) {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
.first-import-r6-intro:has(.first-import-r6-scroll-hint) {
|
||||
padding-right: 190px;
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(7)) {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
@keyframes first-import-r6-checking {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(4))
|
||||
.r6-import-progress-card {
|
||||
min-height: 112px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
justify-items: center;
|
||||
gap: 1px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(4))
|
||||
.r6-import-progress-orbit {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
grid-column: 1;
|
||||
grid-row: auto;
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(4))
|
||||
:is(h2, p, small) {
|
||||
grid-column: 1;
|
||||
align-self: auto;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(4))
|
||||
.r6-import-progress-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.first-import-r6-feed {
|
||||
flex: 0 0 auto;
|
||||
@@ -520,22 +512,6 @@ dialog.first-import-r6-original[open] {
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 761px) and (max-height: 859px) {
|
||||
.onboarding-r6:has(.first-import-r6-layout)
|
||||
> :is(.onboarding-r6-titlebar, .onboarding-r6-local-footer) {
|
||||
display: none;
|
||||
}
|
||||
.onboarding-r6:has(.first-import-r6-layout) > .onboarding-r6-stage {
|
||||
min-height: 0;
|
||||
height: 100dvh;
|
||||
padding: 10px 24px 2px;
|
||||
}
|
||||
.onboarding-r6:has(.first-import-r6-layout)
|
||||
> .onboarding-r6-stage
|
||||
> .onboarding-r6-shell {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@media (min-width: 761px) and (max-height: 959px) {
|
||||
.onboarding-r6-body:has(> .first-import-r6-layout) {
|
||||
overflow-y: auto;
|
||||
@@ -654,10 +630,6 @@ dialog.first-import-r6-original[open] {
|
||||
outline-offset: 3px;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.first-import-r6-submission-status::before {
|
||||
animation: none;
|
||||
border-color: #587c90;
|
||||
}
|
||||
.first-import-r6-selection,
|
||||
.first-import-r6-selection input::after {
|
||||
transition: none;
|
||||
|
||||
@@ -588,44 +588,51 @@
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* The current controller keeps generation-only behavior. The presentation
|
||||
intentionally returns to the original open-source setup composition:
|
||||
an open form column, a separate context column, and a distinct advanced state. */
|
||||
.onboarding-model-group {
|
||||
/* Generation-only cold start. Embedding remains a separate settings lane. */
|
||||
.onboarding-model-r6 .onboarding-r6-head > div {
|
||||
width: min(760px, 100%);
|
||||
}
|
||||
.onboarding-model-layout {
|
||||
display: block;
|
||||
width: min(880px, 100%);
|
||||
height: auto;
|
||||
margin-inline: auto;
|
||||
}
|
||||
.onboarding-model-group > .onboarding-model-intro {
|
||||
margin-bottom: 0;
|
||||
.onboarding-model-form {
|
||||
overflow: visible;
|
||||
}
|
||||
.onboarding-model-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px 20px;
|
||||
padding: 24px 26px;
|
||||
border: 1px solid #d8e2e8;
|
||||
border-radius: 16px;
|
||||
background: rgb(255 255 255 / 72%);
|
||||
box-shadow: inset 0 1px rgb(255 255 255 / 88%);
|
||||
}
|
||||
.onboarding-model-provider,
|
||||
.onboarding-model-key {
|
||||
margin-top: 0;
|
||||
}
|
||||
.onboarding-model-field {
|
||||
gap: 7px;
|
||||
}
|
||||
.onboarding-model-field :is(input, select),
|
||||
.onboarding-model-field .om-option-menu-button,
|
||||
.generation-model-field :is(input, select) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 54px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #c0ccd5;
|
||||
height: 44px;
|
||||
padding: 9px 12px;
|
||||
border: 1px solid #c8d3db;
|
||||
border-radius: 8px;
|
||||
background: #f0f5f6;
|
||||
background: #fff;
|
||||
color: #262a2e;
|
||||
font:
|
||||
400 14px / 23px "Noto Sans SC",
|
||||
400 14px / 22px "Noto Sans SC",
|
||||
"PingFang SC",
|
||||
sans-serif;
|
||||
transition:
|
||||
border-color var(--r6-fast, 140ms),
|
||||
background-color var(--r6-fast, 140ms);
|
||||
}
|
||||
.onboarding-model-field .om-option-menu-button:hover:not(:disabled),
|
||||
.generation-model-field :is(input, select):hover:not(:disabled) {
|
||||
border-color: #9fb6ad;
|
||||
background: #eaf1f2;
|
||||
}
|
||||
.generation-model-field select:not(:disabled),
|
||||
.generation-technical-toggle:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
:is(.onboarding-model-field, .generation-model-field) [aria-invalid="true"] {
|
||||
border-color: #a24646;
|
||||
}
|
||||
.onboarding-model-field small,
|
||||
.generation-model-field small {
|
||||
@@ -634,10 +641,14 @@
|
||||
line-height: 19px;
|
||||
}
|
||||
.onboarding-model-default-summary {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-height: 54px;
|
||||
margin-top: 12px;
|
||||
padding: 14px 0;
|
||||
border-block: 1px solid #e4eaee;
|
||||
}
|
||||
.onboarding-model-default-summary span,
|
||||
.onboarding-model-disclosure {
|
||||
color: #63707b;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
@@ -647,131 +658,154 @@
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.onboarding-model-disclosure {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.generation-technical-disclosure {
|
||||
margin-top: 23px;
|
||||
grid-column: 1 / -1;
|
||||
border-top: 1px solid #e4eaee;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.generation-technical-toggle {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 28px;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
padding: 8px 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #4e6478;
|
||||
font:
|
||||
500 13px / 21px "Noto Sans SC",
|
||||
"PingFang SC",
|
||||
sans-serif;
|
||||
transition:
|
||||
background-color var(--r6-fast, 140ms),
|
||||
color var(--r6-fast, 140ms);
|
||||
}
|
||||
.generation-technical-toggle:hover:not(:disabled) {
|
||||
background: #ffffff66;
|
||||
color: #33424d;
|
||||
}
|
||||
.generation-technical-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 19px 30px;
|
||||
margin-top: 34px;
|
||||
}
|
||||
.generation-technical-fields[hidden] {
|
||||
display: none;
|
||||
gap: 16px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.generation-model-field {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 5px;
|
||||
gap: 6px;
|
||||
color: #63707b;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.generation-model-field:has([name="base_url"]),
|
||||
.generation-model-field:has([name="base-url"]),
|
||||
.generation-runtime-fields,
|
||||
.generation-runtime-note,
|
||||
.generation-thinking {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.generation-model-field :is(input, select) {
|
||||
height: 42px;
|
||||
padding: 9px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
}
|
||||
.generation-runtime-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 24px;
|
||||
gap: 12px;
|
||||
}
|
||||
.generation-technical-fields[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.generation-runtime-note {
|
||||
grid-column: 1 / -1;
|
||||
color: #63707b;
|
||||
font-size: 12px;
|
||||
line-height: 19px;
|
||||
}
|
||||
.generation-thinking {
|
||||
display: flex;
|
||||
min-height: 28px;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 9px;
|
||||
color: #416e5c;
|
||||
font-size: 14px;
|
||||
line-height: 23px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.onboarding-model-r6 .generation-model-error {
|
||||
color: #a24646;
|
||||
}
|
||||
.onboarding-model-form[data-advanced="true"]
|
||||
:is(
|
||||
.onboarding-model-intro,
|
||||
.onboarding-model-provider,
|
||||
.onboarding-model-key,
|
||||
.onboarding-model-default-summary
|
||||
) {
|
||||
display: none;
|
||||
.onboarding-model-recovery-actions {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.onboarding-model-form[data-advanced="true"] .generation-technical-disclosure {
|
||||
margin-top: 0;
|
||||
}
|
||||
.onboarding-model-form[data-advanced="true"] .generation-technical-toggle {
|
||||
min-height: 28px;
|
||||
}
|
||||
.onboarding-model-recovery-actions,
|
||||
.onboarding-model-feedback {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.onboarding-model-r6 .onboarding-r6-foot {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 20px 32px;
|
||||
}
|
||||
.onboarding-model-r6 .onboarding-model-footer-actions {
|
||||
flex-wrap: nowrap;
|
||||
margin-left: 0;
|
||||
}
|
||||
.onboarding-model-r6 .onboarding-model-footer-actions .r6-button {
|
||||
min-width: 132px;
|
||||
}
|
||||
@media (min-width: 761px) {
|
||||
.onboarding-model-group {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
.onboarding-model-r6 .onboarding-r6-body {
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-gutter: stable;
|
||||
scroll-padding: 12px;
|
||||
padding-inline: 4px 16px;
|
||||
}
|
||||
.onboarding-model-form {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
@media (min-width: 761px) and (max-height: 959px) {
|
||||
.onboarding-model-form[data-advanced="false"]
|
||||
.onboarding-model-field
|
||||
:is(input, select, .om-option-menu-button) {
|
||||
height: 44px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
.onboarding-model-r6 .onboarding-r6-body {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.generation-technical-fields {
|
||||
margin-top: 16px;
|
||||
.onboarding-model-layout,
|
||||
.onboarding-model-form {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
.onboarding-model-form {
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.onboarding-model-group {
|
||||
gap: 14px 18px;
|
||||
padding: 18px 22px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.generation-technical-fields,
|
||||
.generation-runtime-fields {
|
||||
@media (max-width: 600px) {
|
||||
.onboarding-model-layout--single-column-600 .onboarding-model-group {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.onboarding-model-layout--single-column-600 .generation-technical-fields,
|
||||
.onboarding-model-layout--single-column-600 .generation-runtime-fields {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.onboarding-model-footer-actions .r6-button,
|
||||
.onboarding-model-footer-actions .r6-button[data-variant="primary"] {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.onboarding-model-r6 .onboarding-r6-foot {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.onboarding-model-r6 .onboarding-model-footer-actions {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.onboarding-model-r6 .onboarding-model-footer-actions .r6-button {
|
||||
flex: 1 1 160px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.onboarding-model-r6 .onboarding-model-footer-actions .r6-button,
|
||||
.onboarding-model-r6
|
||||
.onboarding-model-footer-actions
|
||||
.r6-button[data-variant="primary"] {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
safe center falls back to the start if the pair would overflow a narrow column.
|
||||
Three or more keep the existing row and scroll horizontally. */
|
||||
.r6-import-progress[data-count="1"],
|
||||
.r6-import-progress[data-count="2"],
|
||||
.r6-import-progress[data-count="3"] {
|
||||
.r6-import-progress[data-count="2"] {
|
||||
justify-content: safe center;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MemHub 安装与发行
|
||||
|
||||
MemHub `v1.0.6`(build 44)的发行目标如下:
|
||||
MemHub `v1.0.6`(build 43)的发行目标如下:
|
||||
|
||||
| 平台 | 发行物 | 构建与验收 |
|
||||
| --- | --- | --- |
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "one-memory",
|
||||
"version": "1.0.6",
|
||||
"release": {
|
||||
"build_number": 44,
|
||||
"build_number": 43,
|
||||
"macos_min_version": "13.0",
|
||||
"app_update": "manual",
|
||||
"runtime_manifest_schema": 1,
|
||||
|
||||
@@ -1063,9 +1063,6 @@ const baseEnglishMessages = {
|
||||
"少一些重复交代,多一些理解。": "Less repetition. More understanding.",
|
||||
onboardingMoreApps: "Scroll for more · {count} apps remaining",
|
||||
"onboardingMoreApps.one": "Scroll for more · {count} app remaining",
|
||||
onboardingMoreImportApps: "Scroll horizontally · {count} apps remaining",
|
||||
onboardingImportChecking:
|
||||
"Checking the import result; your selection is kept",
|
||||
onboardingAllApps: "All {count} apps shown",
|
||||
默认位置: "Default location",
|
||||
自定义位置: "Custom location",
|
||||
|
||||
@@ -83,10 +83,6 @@ export const onboardingSpanishMessages = {
|
||||
onboardingMoreApps: "Desplázate para ver más · Quedan {count} aplicaciones",
|
||||
"onboardingMoreApps.one":
|
||||
"Desplázate para ver más · Queda {count} aplicación",
|
||||
onboardingMoreImportApps:
|
||||
"Desplázate horizontalmente · Quedan {count} aplicaciones",
|
||||
onboardingImportChecking:
|
||||
"Comprobando el resultado de la importación; tu selección se conserva",
|
||||
onboardingAllApps: "Se muestran las {count} aplicaciones",
|
||||
默认位置: "Ubicación predeterminada",
|
||||
自定义位置: "Ubicación personalizada",
|
||||
@@ -397,8 +393,6 @@ export const onboardingChineseMessages = {
|
||||
onboardingImportHistory: "导入 {name} 历史",
|
||||
onboardingMoreApps: "向下查看更多 · 还有 {count} 个应用",
|
||||
"onboardingMoreApps.one": "向下查看更多 · 还有 {count} 个应用",
|
||||
onboardingMoreImportApps: "横向滑动查看更多 · 还有 {count} 个应用",
|
||||
onboardingImportChecking: "正在核对导入结果,已保存的选择不会丢失",
|
||||
onboardingAllApps: "已显示全部 {count} 个应用",
|
||||
onboardingImportRefused: "尚未获准导入({reason})。请核对来源后重试。",
|
||||
onboardingConfirmHistory: "确认这是 {name} 的历史位置",
|
||||
|
||||
@@ -1255,9 +1255,6 @@ export const germanMessages: Record<MessageKey, string> = {
|
||||
"少一些重复交代,多一些理解。": "Weniger Wiederholung. Mehr Verständnis.",
|
||||
onboardingMoreApps: "Für mehr scrollen · {count} Apps verbleibend",
|
||||
"onboardingMoreApps.one": "Für mehr scrollen · {count} App verbleibend",
|
||||
onboardingMoreImportApps: "Horizontal scrollen · Noch {count} Apps",
|
||||
onboardingImportChecking:
|
||||
"Importergebnis wird geprüft; Ihre Auswahl bleibt erhalten",
|
||||
onboardingAllApps: "Alle {count} Apps angezeigt",
|
||||
默认位置: "Standardort",
|
||||
自定义位置: "Eigener Ort",
|
||||
|
||||
@@ -1271,10 +1271,6 @@ export const frenchMessages: Record<MessageKey, string> = {
|
||||
"Faites défiler pour voir plus · {count} applications restantes",
|
||||
"onboardingMoreApps.one":
|
||||
"Faites défiler pour voir plus · {count} application restante",
|
||||
onboardingMoreImportApps:
|
||||
"Faites défiler horizontalement · encore {count} applications",
|
||||
onboardingImportChecking:
|
||||
"Vérification du résultat de l’importation ; votre sélection est conservée",
|
||||
onboardingAllApps: "Les {count} applications sont affichées",
|
||||
默认位置: "Emplacement par défaut",
|
||||
自定义位置: "Emplacement personnalisé",
|
||||
|
||||
@@ -1239,9 +1239,6 @@ export const indonesianMessages: Record<MessageKey, string> = {
|
||||
onboardingMoreApps: "Gulir untuk melihat lainnya · {count} aplikasi lagi",
|
||||
"onboardingMoreApps.one":
|
||||
"Gulir untuk melihat lainnya · {count} aplikasi lagi",
|
||||
onboardingMoreImportApps: "Gulir horizontal · {count} aplikasi lagi",
|
||||
onboardingImportChecking:
|
||||
"Memeriksa hasil impor; pilihan Anda tetap tersimpan",
|
||||
onboardingAllApps: "Semua {count} aplikasi ditampilkan",
|
||||
默认位置: "Lokasi default",
|
||||
自定义位置: "Lokasi kustom",
|
||||
|
||||
@@ -1234,9 +1234,6 @@ export const japaneseMessages: Record<MessageKey, string> = {
|
||||
onboardingMoreApps: "スクロールしてさらに表示 · 残り {count} 個のアプリ",
|
||||
"onboardingMoreApps.one":
|
||||
"スクロールしてさらに表示 · 残り {count} 個のアプリ",
|
||||
onboardingMoreImportApps: "横にスクロール · 残り {count} 個のアプリ",
|
||||
onboardingImportChecking:
|
||||
"インポート結果を確認しています。選択内容は保持されています",
|
||||
onboardingAllApps: "全 {count} 個のアプリを表示中",
|
||||
默认位置: "既定の場所",
|
||||
自定义位置: "カスタムの場所",
|
||||
|
||||
@@ -1210,9 +1210,6 @@ export const koreanMessages: Record<MessageKey, string> = {
|
||||
"少一些重复交代,多一些理解。": "반복은 줄이고, 이해는 깊게.",
|
||||
onboardingMoreApps: "스크롤하여 더 보기 · 앱 {count}개 남음",
|
||||
"onboardingMoreApps.one": "스크롤하여 더 보기 · 앱 {count}개 남음",
|
||||
onboardingMoreImportApps: "가로로 스크롤 · 앱 {count}개 남음",
|
||||
onboardingImportChecking:
|
||||
"가져오기 결과를 확인하는 중이며 선택 항목은 유지됩니다",
|
||||
onboardingAllApps: "앱 {count}개 모두 표시됨",
|
||||
默认位置: "기본 위치",
|
||||
自定义位置: "사용자 지정 위치",
|
||||
|
||||
@@ -1258,9 +1258,6 @@ export const portugueseMessages: Record<MessageKey, string> = {
|
||||
"少一些重复交代,多一些理解。": "Menos repetição. Mais compreensão.",
|
||||
onboardingMoreApps: "Role para ver mais · Apps restantes: {count}",
|
||||
"onboardingMoreApps.one": "Role para ver mais · {count} app restante",
|
||||
onboardingMoreImportApps: "Role horizontalmente · {count} apps restantes",
|
||||
onboardingImportChecking:
|
||||
"Verificando o resultado da importação; sua seleção foi mantida",
|
||||
onboardingAllApps: "Todos os apps exibidos ({count})",
|
||||
默认位置: "Local padrão",
|
||||
自定义位置: "Local personalizado",
|
||||
|
||||
@@ -1244,9 +1244,6 @@ export const russianMessages: Record<MessageKey, string> = {
|
||||
"Прокрутите, чтобы увидеть больше · осталось приложений: {count}",
|
||||
"onboardingMoreApps.one":
|
||||
"Прокрутите, чтобы увидеть больше · осталось приложений: {count}",
|
||||
onboardingMoreImportApps:
|
||||
"Прокрутите по горизонтали · осталось приложений: {count}",
|
||||
onboardingImportChecking: "Проверяем результат импорта; ваш выбор сохранён",
|
||||
onboardingAllApps: "Показаны все приложения: {count}",
|
||||
默认位置: "Расположение по умолчанию",
|
||||
自定义位置: "Своё расположение",
|
||||
|
||||
@@ -1133,8 +1133,6 @@ export const traditionalChineseMessages: Record<MessageKey, string> = {
|
||||
"少一些重复交代,多一些理解。": "少一些重複交代,多一些理解。",
|
||||
onboardingMoreApps: "向下捲動檢視更多 · 還有 {count} 個 App",
|
||||
"onboardingMoreApps.one": "向下捲動檢視更多 · 還有 {count} 個 App",
|
||||
onboardingMoreImportApps: "橫向捲動查看更多 · 還有 {count} 個 App",
|
||||
onboardingImportChecking: "正在確認匯入結果,已保留你的選擇",
|
||||
onboardingAllApps: "已顯示全部 {count} 個 App",
|
||||
默认位置: "預設位置",
|
||||
自定义位置: "自訂位置",
|
||||
|
||||
Reference in new issue
Block a user