fix(web): support Docker access over HTTP
This commit is contained in:
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;
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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}$/);
|
||||
});
|
||||
});
|
||||
@@ -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,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,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>) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user