feat(notebook): load notebook space via token and pre-select docs/sources

- NotebookAIWritingPage reads `token` from URL, fetches space data from
  notebook-token API (search_space_id, doc_ids, user_token) and space
  name from searchspaces API; keep-alive ping every 50s resets Redis TTL
- Pass user_token as studioToken through AIWritingPage → AIWritingLayout
  → WritingFormPanel → AIWritingPanel → AIWritingForm to skip
  getTokenByEmail when studio token is already available
- useStudioNotebooks accepts enabled flag; useNotebookSources accepts
  overrideToken to bypass the internal token fetch
- AIWritingForm hides notebook selector when studioToken is provided
  (notebook is pre-locked) and pre-selects initialDocumentIds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
xuhdandClaude Sonnet 4.6 committed 2026-06-05 13:38:08 +08:00
1 parent e53c1a0f7b
commit 0e7149eec3
6 files changed
+140 -85

No files matched your search

@@ -19,9 +19,6 @@ import {
import { Textarea } from "@/components/ui/textarea";
import {
Search,
Sparkles,
Moon,
BookOpen,
Loader2,
Plus,
Trash2,
@@ -33,7 +30,6 @@ import Empty from "./empty";
import { NetworkActor } from "./network-actor";
// ── 适配层 ─────────────────────────────────────────────────
const writeYLog = (..._args: unknown[]) => {};
function authenticatedFetch(url: string, init?: RequestInit) {
const token = getStoredAuth()?.access_token;
@@ -58,13 +54,7 @@ function getYUserId(): string {
return "1157761008018653184";
}
type SearchMode = "A" | "Y" | "knowledge";
const searchModes = [
{ value: "A", label: "A系统", icon: Sparkles },
{ value: "Y", label: "Y系统", icon: Moon },
{ value: "knowledge", label: "知识库", icon: BookOpen },
] as const;
const searchTypes = [
{ value: "human", label: "人物基因库" },
@@ -274,7 +264,7 @@ const DocumentUploadPopupContent: FC<{
data?: { query: string; mode: string }
) => Promise<void>;
initialSearchText?: string;
}> = ({ isOpen, onOpenChange, searchSpace, onUploadSuccess, closeDialog, initialSearchText }) => {
}> = ({ isOpen, onOpenChange, searchSpace, onUploadSuccess, initialSearchText }) => {
const searchSpaceId = String(searchSpace?.id);
const backendUrl =
typeof window !== "undefined"
@@ -286,7 +276,6 @@ const DocumentUploadPopupContent: FC<{
const [searchValue, setSearchValue] = useState("");
const [activeTab, setActiveTab] = useState<"search" | "Y" | "upload" | "A" | "actor">("search");
const [isSearching, setIsSearching] = useState(false);
const [importSources, setImportSources] = useState<unknown[]>([]);
const [isImporting, setIsImporting] = useState(false);
const [listData, setListData] = useState<listDataItem[]>([]);
const [searchType, setSearchType] = useState("human");
@@ -396,34 +385,7 @@ const DocumentUploadPopupContent: FC<{
setSelectedSearchSources([]);
};
const handleAllSearch = async (searchMode: SearchMode, text: string) => {
if (isSearching || !text.trim()) return;
setIsSearching(true);
const reqBody = {
query: text,
search_space_id: searchSpaceId,
search_mode: searchMode,
};
const reqUrl = `${backendUrl}/api/v1/source/new`;
try {
const response = await authenticatedFetch(reqUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(reqBody),
});
if (response.ok) {
const data = await response.json() as { result: unknown[] };
writeYLog({ optDetail: "笔记检索数据", optType: 3, reqParam: reqBody, url: reqUrl, returnParam: JSON.stringify({ data }), optResult: "0" });
const results = data.result as listDataItem[];
results.forEach((item) => { item.checked = false; });
setImportSources(results);
}
} catch (error) {
writeYLog({ optDetail: "笔记检索数据", optType: 3, reqParam: reqBody, url: reqUrl, returnParam: JSON.stringify({ data: error }), optResult: "1" });
} finally {
setIsSearching(false);
}
};
function urlSafeBtoa(str: string): string {
const encoder = new TextEncoder();
@@ -599,7 +561,11 @@ const DocumentUploadPopupContent: FC<{
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="w-[90vw] max-w-[90vw] sm:max-w-[90vw] h-[90vh] flex flex-col p-0 gap-0 overflow-hidden border border-border bg-muted text-foreground [&>button]:right-3 [&>button]:top-4 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button]:z-[100] [&>button_svg]:size-4">
<DialogContent
className="w-[90vw] max-w-[90vw] sm:max-w-[90vw] h-[90vh] flex flex-col p-0 gap-0 overflow-hidden border border-border bg-muted text-foreground [&>button]:right-3 [&>button]:top-4 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button]:z-[100] [&>button_svg]:size-4"
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<DialogTitle className="sr-only">添加来源</DialogTitle>
<DialogDescription className="sr-only">从外部系统搜索并导入来源</DialogDescription>
<div className="flex-1 min-h-0 relative overflow-hidden">