fix(web): support Docker access over HTTP

This commit is contained in:
tianma-if committed 2026-08-24 16:07:12 +08:00
1 parent 6faffbc2f2
commit 51a9024d7d
8 files changed
+79 -14

No files matched your search

@@ -63,9 +63,13 @@ export class DesktopRendererErrorBoundary extends React.Component<{ children: Re
<div className="mb-4 flex h-11 w-11 items-center justify-center rounded-full bg-rose-50 text-xl text-rose-700">!</div>
<h1 className="text-lg font-semibold">{zh ? "EdgeEver 页面出现异常" : "EdgeEver encountered a page error"}</h1>
<p className="mt-2 text-sm leading-6 text-slate-600">
{zh
? "问题已经记录到这台设备。你可以重新加载继续使用,并在提交前检查脱敏后的 GitHub Issue。"
: "The problem was recorded on this device. You can reload to continue and review the redacted GitHub Issue before submitting it."}
{desktop
? (zh
? "问题已经记录到这台设备。你可以重新加载继续使用,并在提交前检查脱敏后的 GitHub Issue。"
: "The problem was recorded on this device. You can reload to continue and review the redacted GitHub Issue before submitting it.")
: (zh
? "你可以重新加载继续使用。如果问题仍然出现,请检查浏览器控制台,并在提交 GitHub Issue 前移除敏感信息。"
: "Reload to continue. If the problem persists, check the browser console and remove sensitive information before submitting a GitHub Issue.")}
</p>
<div className="mt-5 flex flex-wrap gap-3">
<button
@@ -1,6 +1,7 @@
import { useEffect, useRef } from "react";
import { isBrowserOffline, verifyBrowserConnectivity } from "@/lib/network-status";
import { SYNC_QUEUE_DEFERRED_EVENT } from "@/lib/sync-events";
import { createClientUuid } from "@/lib/client-id";
import {
BACKGROUND_WORKSPACE_REFRESH_INTERVAL_MS,
claimBackgroundRefreshLease,
@@ -30,7 +31,7 @@ export const useWorkspaceSyncLifecycle = ({
const deferredSyncPendingRef = useRef(false);
const runQueuedSyncRef = useRef(runQueuedSync);
const refreshWorkspaceRef = useRef(refreshWorkspace);
const backgroundRefreshOwnerRef = useRef(crypto.randomUUID());
const backgroundRefreshOwnerRef = useRef(createClientUuid());
useEffect(() => {
refreshWorkspaceRef.current = refreshWorkspace;
+2 -4
View File
@@ -39,6 +39,7 @@ import type {
import { resolveInstanceUrlInput } from "@edgeever/shared";
import type { MemoFilterMode, MemoSortMode } from "./app-helpers";
import { readAiStreamingPreference } from "./ai-generation-preference";
import { createClientUuid } from "./client-id";
type ListNotebooksResponse = {
notebooks: Notebook[];
@@ -227,10 +228,7 @@ export const saveDesktopApiBaseUrl = async (value: string) => {
};
const createWebDeviceId = () => {
const uuid = globalThis.crypto?.randomUUID?.();
return uuid
? `web-${uuid}`
: `web-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
return `web-${createClientUuid()}`;
};
const getOrCreateWebDeviceId = () => {
+32
View File
@@ -0,0 +1,32 @@
import { strict as assert } from "node:assert";
import { describe, it } from "node:test";
import { createClientUuid } from "./client-id.ts";
describe("createClientUuid", () => {
it("prefers the browser randomUUID implementation", () => {
const expected = "12345678-1234-4123-8123-123456789abc";
const cryptoApi = {
randomUUID: () => expected,
getRandomValues: () => {
throw new Error("getRandomValues should not be called");
},
};
assert.equal(createClientUuid(cryptoApi), expected);
});
it("creates an RFC 4122 v4 UUID when randomUUID is unavailable on HTTP", () => {
const cryptoApi = {
getRandomValues: (bytes) => {
bytes.fill(0xab);
return bytes;
},
};
assert.equal(createClientUuid(cryptoApi), "abababab-abab-4bab-abab-abababababab");
});
it("still returns a UUID when the Crypto API is unavailable", () => {
assert.match(createClientUuid(null), /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
});
});
+27
View File
@@ -0,0 +1,27 @@
type ClientCrypto = Pick<Crypto, "getRandomValues"> & Partial<Pick<Crypto, "randomUUID">>;
const fillWithPseudoRandomBytes = (bytes: Uint8Array) => {
for (let index = 0; index < bytes.length; index += 1) {
bytes[index] = Math.floor(Math.random() * 256);
}
};
export const createClientUuid = (cryptoApi: ClientCrypto | null | undefined = globalThis.crypto) => {
if (typeof cryptoApi?.randomUUID === "function") {
return cryptoApi.randomUUID();
}
const bytes = new Uint8Array(16);
if (typeof cryptoApi?.getRandomValues === "function") {
cryptoApi.getRandomValues(bytes);
} else {
fillWithPseudoRandomBytes(bytes);
}
// RFC 4122 version 4 and variant bits. getRandomValues remains available in
// browsers on plain-HTTP LAN origins where randomUUID is intentionally hidden.
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
const hex = Array.from(bytes, (value) => value.toString(16).padStart(2, "0"));
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
};
+5 -4
View File
@@ -5,6 +5,7 @@ import { localDb, selectNewestLocalDraft, type LocalDraft, type LocalMemo, type
import { cacheLocalResourceBytes, localResourceUrl, removeCachedLocalResourceBytes } from "@/lib/local-resource-cache";
import { isBrowserOffline } from "@/lib/network-status";
import { parseTagsText } from "@/lib/utils";
import { createClientUuid } from "@/lib/client-id";
export type LocalMemoListParams = {
notebookId?: string | null;
@@ -275,7 +276,7 @@ export const createLocalMemo = async (
const contentMarkdown = input.contentMarkdown ?? docToMarkdown(contentJson);
const contentText = docToText(contentJson);
const memo: MemoDetail = {
id: `local_${crypto.randomUUID()}`,
id: `local_${createClientUuid()}`,
notebookId: input.notebookId,
title: input.title?.trim() || null,
excerpt: createExcerpt(contentText),
@@ -460,7 +461,7 @@ export const deleteLocalResource = async (scope: string, resourceId: string) =>
export const createLocalResource = async (scope: string, memoId: string, file: File) => {
const now = new Date().toISOString();
const id = `local_resource_${crypto.randomUUID()}`;
const id = `local_resource_${createClientUuid()}`;
const url = localResourceUrl(id);
await cacheLocalResourceBytes(url, file);
const resource: ResourceListItem = {
@@ -517,7 +518,7 @@ export const deleteLocalNotebook = async (scope: string, notebookId: string) =>
export const createLocalNotebook = async (scope: string, input: { name: string; parentId?: string | null }) => {
const now = new Date().toISOString();
const notebook: Notebook = {
id: `local_${crypto.randomUUID()}`,
id: `local_${createClientUuid()}`,
parentId: input.parentId ?? null,
name: input.name.trim(),
slug: null,
@@ -537,7 +538,7 @@ export const createLocalTemplate = async (scope: string, input: { name: string;
const now = new Date().toISOString();
const contentMarkdown = input.contentMarkdown ?? "";
const template: MemoTemplate = {
id: `local_${crypto.randomUUID()}`,
id: `local_${createClientUuid()}`,
name: input.name.trim(),
description: input.description ?? null,
title: input.title ?? null,
+2 -1
View File
@@ -2,6 +2,7 @@ import { DOMSerializer } from "@tiptap/pm/model";
import type { Editor } from "@tiptap/react";
import type { TiptapDoc } from "@edgeever/shared";
import { getAppPagePath, getMessageTargetOrigin } from "@/lib/app-page-path";
import { createClientUuid } from "@/lib/client-id";
export const NOTE_PRINT_MESSAGE = "edgeever:note-print";
export const NOTE_PRINT_READY_MESSAGE = "edgeever:note-print-ready";
@@ -44,7 +45,7 @@ export const openNotePrintPreview = (
payload: Omit<NotePrintPayload, "type" | "token">,
preopenedWindow?: Window,
) => {
const token = crypto.randomUUID();
const token = createClientUuid();
let printWindow: Window | null = preopenedWindow ?? null;
const handleMessage = (event: MessageEvent<NotePrintReadyMessage>) => {
+2 -1
View File
@@ -31,6 +31,7 @@ import { getMemoSaveConflictInfo, parseMemoSaveConflictDetails } from "@/lib/mem
import { getCachedLocalResourceBytes, removeCachedLocalResourceBytes } from "@/lib/local-resource-cache";
import { isBrowserOffline } from "@/lib/network-status";
import { parseTagsText } from "@/lib/utils";
import { createClientUuid } from "@/lib/client-id";
export type { SyncQueueSummary, SyncRunResult } from "@edgeever/shared";
export type SyncQueueResult = MemoDetail | Notebook | MemoTemplate | Resource | null;
@@ -458,7 +459,7 @@ const claimQueueItem = (id: string) =>
return null;
}
const claimId = crypto.randomUUID();
const claimId = createClientUuid();
const claimedItem: SyncQueueItem = {
...item,
status: "syncing",