fix: Z39 isolate module data by slug

Keep agent-bound modules in per-app schemas and prevent stale frontend routes from displaying another module's blueprint or rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-07 17:31:12 +08:00
parent 43dc708f1e
commit 473113682f
13 changed files with 402 additions and 207 deletions

File diff suppressed because one or more lines are too long

2
web/dist/index.html vendored
View File

@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="/assets/index-aOZIXFeC.js"></script>
<script type="module" crossorigin src="/assets/index-Pe8anoMp.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CtqUfnO-.css">
</head>
<body>

View File

@@ -93,6 +93,7 @@ export default function App() {
const excelInputRef = useRef<HTMLInputElement>(null);
const imagesInputRef = useRef<HTMLInputElement>(null);
const layoutsInputRef = useRef<HTMLInputElement>(null);
const routeRequestRef = useRef(0);
const [storageMode, setStorageMode] = useState<"schema_per_app" | "database_per_app">("schema_per_app");
const [llmProvider, setLlmProvider] = useState("deepseek");
const [llmModel, setLlmModel] = useState("deepseek-chat");
@@ -226,22 +227,25 @@ export default function App() {
useEffect(() => {
let cancelled = false;
async function syncRoute() {
const requestID = ++routeRequestRef.current;
const isStale = () => cancelled || requestID !== routeRequestRef.current;
const pid = previewRouteId();
if (pid) {
if (!cancelled) {
if (!isStale()) {
setBusy(true);
setError("");
setBlueprint(null);
}
try {
const pack = await fetchPreview(pid);
if (cancelled) return;
if (isStale()) return;
setBlueprint(pack.blueprint);
setDraft(pack.blueprint);
setSlug(pack.blueprint?.meta?.slug || "preview");
setResource(pack.resource || "records");
setView("app");
} catch (e: any) {
if (!cancelled) {
if (!isStale()) {
clearActivePreview();
setError(e.message || String(e));
setView("console");
@@ -249,7 +253,7 @@ export default function App() {
navigateToConsole();
}
} finally {
if (!cancelled) setBusy(false);
if (!isStale()) setBusy(false);
}
return;
}
@@ -257,41 +261,46 @@ export default function App() {
clearActivePreview();
const routeSlug = appRouteSlug();
if (!routeSlug) {
if (!cancelled) {
if (!isStale()) {
setView("console");
setBlueprint(null);
}
return;
}
if (!cancelled) {
if (!isStale()) {
setBusy(true);
setError("");
setSlug(routeSlug);
setBlueprint(null);
}
try {
const sess = session || loadSession();
const bp = await getBlueprint(sess, routeSlug);
if (cancelled) return;
if (isStale()) return;
if (String(bp?.meta?.slug || "") !== routeSlug) {
throw new Error(`模块路由不匹配:请求 ${routeSlug},返回 ${String(bp?.meta?.slug || "空")}`);
}
setBlueprint(bp);
setDraft(bp);
const r0 = bp?.apis?.resources?.[0]?.path?.replace(/^\//, "") || resource;
setResource(r0);
setView("app");
} catch (e: any) {
if (!cancelled) {
if (!isStale()) {
setError(e.message || String(e));
setView("console");
setBlueprint(null);
navigateToConsole();
}
} finally {
if (!cancelled) setBusy(false);
if (!isStale()) setBusy(false);
}
}
void syncRoute();
window.addEventListener("hashchange", syncRoute);
return () => {
cancelled = true;
routeRequestRef.current++;
window.removeEventListener("hashchange", syncRoute);
};
}, [session]);
@@ -576,6 +585,7 @@ export default function App() {
if (view === "app" && blueprint) {
return (
<GeneratedApp
key={String(blueprint?.meta?.slug || slug)}
session={session}
blueprint={blueprint}
onBack={() => {

View File

@@ -352,12 +352,13 @@ export async function getBlueprint(session: Session | null | undefined, slug: st
if (session?.accessToken) {
const res = await apiFetch(`${PLATFORM}/api/v1/apps/${slug}/blueprint`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
cache: "no-store",
});
const data = await readJson(res);
throwIfBad(res, data, "blueprint failed");
return data;
}
const res = await apiFetch(`${PLATFORM}/api/v1/public/apps/${slug}/blueprint`);
const res = await apiFetch(`${PLATFORM}/api/v1/public/apps/${slug}/blueprint`, { cache: "no-store" });
const data = await readJson(res);
throwIfBad(res, data, "blueprint failed");
return data;
@@ -400,7 +401,7 @@ export async function listRows(
}
const headers: Record<string, string> = {};
if (session?.accessToken) headers.Authorization = `Bearer ${session.accessToken}`;
const res = await apiFetch(path, { headers });
const res = await apiFetch(path, { headers, cache: "no-store" });
const data = await readJson(res);
throwIfBad(res, data, "list failed");
return data as { items: Record<string, unknown>[]; total: number };