ソースを参照

feat: onedrive file picker integration

hurxxxx 2 ヶ月 前
コミット
4cc3102758

+ 12 - 0
backend/open_webui/config.py

@@ -1570,6 +1570,18 @@ GOOGLE_DRIVE_API_KEY = PersistentConfig(
     os.environ.get("GOOGLE_DRIVE_API_KEY", ""),
 )
 
+ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig(
+    "ENABLE_ONEDRIVE_INTEGRATION",
+    "onedrive.enable",
+    os.getenv("ENABLE_ONEDRIVE_INTEGRATION", "False").lower() == "true",
+)
+
+ONEDRIVE_CLIENT_ID = PersistentConfig(
+    "ONEDRIVE_CLIENT_ID",
+    "onedrive.client_id",
+    os.environ.get("ONEDRIVE_CLIENT_ID", ""),
+)
+
 # RAG Content Extraction
 CONTENT_EXTRACTION_ENGINE = PersistentConfig(
     "CONTENT_EXTRACTION_ENGINE",

+ 8 - 0
backend/open_webui/main.py

@@ -95,6 +95,7 @@ from open_webui.config import (
     OLLAMA_API_CONFIGS,
     # OpenAI
     ENABLE_OPENAI_API,
+    ONEDRIVE_CLIENT_ID,
     OPENAI_API_BASE_URLS,
     OPENAI_API_KEYS,
     OPENAI_API_CONFIGS,
@@ -217,11 +218,13 @@ from open_webui.config import (
     GOOGLE_PSE_ENGINE_ID,
     GOOGLE_DRIVE_CLIENT_ID,
     GOOGLE_DRIVE_API_KEY,
+    ONEDRIVE_CLIENT_ID,
     ENABLE_RAG_HYBRID_SEARCH,
     ENABLE_RAG_LOCAL_WEB_FETCH,
     ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
     ENABLE_RAG_WEB_SEARCH,
     ENABLE_GOOGLE_DRIVE_INTEGRATION,
+    ENABLE_ONEDRIVE_INTEGRATION,
     UPLOAD_DIR,
     # WebUI
     WEBUI_AUTH,
@@ -568,6 +571,7 @@ app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = RAG_WEB_SEARCH_FULL_CONTEXT
 app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
 
 app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
+app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
 app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
 app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
 app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
@@ -1150,6 +1154,7 @@ async def get_app_config(request: Request):
                     "enable_admin_export": ENABLE_ADMIN_EXPORT,
                     "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
                     "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
+                    "enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
                 }
                 if user is not None
                 else {}
@@ -1181,6 +1186,9 @@ async def get_app_config(request: Request):
                     "client_id": GOOGLE_DRIVE_CLIENT_ID.value,
                     "api_key": GOOGLE_DRIVE_API_KEY.value,
                 },
+                "onedrive": {
+                    "client_id": ONEDRIVE_CLIENT_ID.value
+                }
             }
             if user is not None
             else {}

+ 9 - 0
backend/open_webui/routers/retrieval.py

@@ -353,6 +353,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
         "pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
         "RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
         "enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
+        "enable_onedrive_integration": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
         "content_extraction": {
             "engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
             "tika_server_url": request.app.state.config.TIKA_SERVER_URL,
@@ -381,6 +382,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
             "search": {
                 "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
                 "drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
+                "onedrive": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
                 "engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
                 "searxng_query_url": request.app.state.config.SEARXNG_QUERY_URL,
                 "google_pse_api_key": request.app.state.config.GOOGLE_PSE_API_KEY,
@@ -478,6 +480,7 @@ class ConfigUpdateForm(BaseModel):
     RAG_FULL_CONTEXT: Optional[bool] = None
     pdf_extract_images: Optional[bool] = None
     enable_google_drive_integration: Optional[bool] = None
+    enable_onedrive_integration: Optional[bool] = None
     file: Optional[FileConfig] = None
     content_extraction: Optional[ContentExtractionConfig] = None
     chunk: Optional[ChunkParamUpdateForm] = None
@@ -507,6 +510,12 @@ async def update_rag_config(
         else request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION
     )
 
+    request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION = (
+        form_data.enable_onedrive_integration
+        if form_data.enable_onedrive_integration is not None
+        else request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION
+    )
+
     if form_data.file is not None:
         request.app.state.config.FILE_MAX_SIZE = form_data.file.max_size
         request.app.state.config.FILE_MAX_COUNT = form_data.file.max_count

+ 0 - 22
package-lock.json

@@ -8,7 +8,6 @@
 			"name": "open-webui",
 			"version": "0.5.16",
 			"dependencies": {
-				"@azure/msal-browser": "^4.4.0",
 				"@codemirror/lang-javascript": "^6.2.2",
 				"@codemirror/lang-python": "^6.1.6",
 				"@codemirror/language-data": "^6.5.1",
@@ -135,27 +134,6 @@
 				"node": ">=6.0.0"
 			}
 		},
-		"node_modules/@azure/msal-browser": {
-			"version": "4.4.0",
-			"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.4.0.tgz",
-			"integrity": "sha512-rU6juYXk67CKQmpgi6fDgZoPQ9InZ1760z1BSAH7RbeIc4lHZM/Tu+H0CyRk7cnrfvTkexyYE4pjYhMghpzheA==",
-			"license": "MIT",
-			"dependencies": {
-				"@azure/msal-common": "15.2.0"
-			},
-			"engines": {
-				"node": ">=0.8.0"
-			}
-		},
-		"node_modules/@azure/msal-common": {
-			"version": "15.2.0",
-			"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.2.0.tgz",
-			"integrity": "sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==",
-			"license": "MIT",
-			"engines": {
-				"node": ">=0.8.0"
-			}
-		},
 		"node_modules/@babel/runtime": {
 			"version": "7.24.1",
 			"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",

+ 0 - 1
package.json

@@ -51,7 +51,6 @@
 	},
 	"type": "module",
 	"dependencies": {
-		"@azure/msal-browser": "^4.4.0",
 		"@codemirror/lang-javascript": "^6.2.2",
 		"@codemirror/lang-python": "^6.1.6",
 		"@codemirror/language-data": "^6.5.1",

+ 1 - 0
src/lib/apis/retrieval/index.ts

@@ -52,6 +52,7 @@ type YoutubeConfigForm = {
 type RAGConfigForm = {
 	pdf_extract_images?: boolean;
 	enable_google_drive_integration?: boolean;
+	enable_onedrive_integration?: boolean;
 	chunk?: ChunkConfigForm;
 	content_extraction?: ContentExtractConfigForm;
 	web_loader_ssl_verification?: boolean;

+ 15 - 0
src/lib/components/admin/Settings/Documents.svelte

@@ -61,6 +61,7 @@
 	let RAG_FULL_CONTEXT = false;
 
 	let enableGoogleDriveIntegration = false;
+	let enableOneDriveIntegration = false;
 
 	let OpenAIUrl = '';
 	let OpenAIKey = '';
@@ -189,6 +190,7 @@
 		const res = await updateRAGConfig(localStorage.token, {
 			pdf_extract_images: pdfExtractImages,
 			enable_google_drive_integration: enableGoogleDriveIntegration,
+			enable_onedrive_integration: enableOneDriveIntegration,
 			file: {
 				max_size: fileMaxSize === '' ? null : fileMaxSize,
 				max_count: fileMaxCount === '' ? null : fileMaxCount
@@ -271,6 +273,7 @@
 			fileMaxCount = res?.file.max_count ?? '';
 
 			enableGoogleDriveIntegration = res.enable_google_drive_integration;
+			enableOneDriveIntegration = res.enable_onedrive_integration;
 		}
 	});
 </script>
@@ -653,6 +656,18 @@
 			</div>
 		</div>
 
+		<div class="text-sm font-medium mb-1">{$i18n.t('OneDrive')}</div>
+
+		<div class="">
+			<div class="flex justify-between items-center text-xs">
+				<div class="text-xs font-medium">{$i18n.t('Enable OneDrive')}</div>
+				<div>
+					<Switch bind:state={enableOneDriveIntegration} />
+				</div>
+			</div>
+		</div>
+
+
 		<hr class=" border-gray-100 dark:border-gray-850" />
 
 		<div class=" ">

+ 3 - 8
src/lib/components/chat/MessageInput.svelte

@@ -2,7 +2,7 @@
 	import { toast } from 'svelte-sonner';
 	import { v4 as uuidv4 } from 'uuid';
 	import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker';
-	import { openOneDrivePicker } from '$lib/utils/onedrive-file-picker';
+	import { pickAndDownloadFile } from '$lib/utils/onedrive-file-picker';
 
 	import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
 	const dispatch = createEventDispatcher();
@@ -1111,10 +1111,10 @@
 											}}
 											uploadOneDriveHandler={async () => {
 												try {
-													const fileData = await openOneDrivePicker();
+													const fileData = await pickAndDownloadFile();
 													if (fileData) {
 														const file = new File([fileData.blob], fileData.name, {
-															type: fileData.blob.type
+															type: fileData.blob.type || 'application/octet-stream'
 														});
 														await uploadFileHandler(file);
 													} else {
@@ -1122,11 +1122,6 @@
 													}
 												} catch (error) {
 													console.error('OneDrive Error:', error);
-													toast.error(
-														$i18n.t('Error accessing OneDrive: {{error}}', {
-															error: error.message
-														})
-													);
 												}
 											}}
 											onClose={async () => {

+ 29 - 18
src/lib/components/chat/MessageInput/InputMenu.svelte

@@ -228,30 +228,41 @@
 				</DropdownMenu.Item>
 			{/if}
 
-			{#if $config?.features?.enable_onedrive_integration || true}
+			{#if $config?.features?.enable_onedrive_integration}
 				<DropdownMenu.Item
 					class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
 					on:click={() => {
 						uploadOneDriveHandler();
 					}}
 				>
-					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-5 h-5">
-						<path
-							d="M21.69 13.91l-5.5-3.16l-4.08 3.45l-1.87-1.08l-4.86 4.47l.86.5a2.998 2.998 0 0 0 4.09-1.11a3 3 0 0 0 4.09-1.11a3.06 3.06 0 0 0 1.27-.13a3 3 0 0 0 4.09-1.11a2.81 2.81 0 0 0 1.91-.72z"
-							fill="#0364B8"
-						/>
-						<path
-							d="M7.5 13.5L2 10.5l5-3l5.5 3.16l-5 2.84z"
-							fill="#0078D4"
-						/>
-						<path
-							d="M16.19 10.75L12 7.94V4.5l5.5 3.16l-1.31 3.09z"
-							fill="#1490DF"
-						/>
-						<path
-							d="M12 4.5l-5 3l-5-3l5-3l5 3z"
-							fill="#28A8EA"
-						/>
+					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none">
+						<mask id="mask0_87_7796" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="6" width="32" height="20">
+							<path d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z" fill="#C4C4C4"/>
+						</mask>
+						<g mask="url(#mask0_87_7796)">
+							<path d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z" fill="url(#paint0_linear_87_7796)"/>
+							<path d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z" fill="url(#paint1_linear_87_7796)"/>
+							<path d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z" fill="url(#paint2_linear_87_7796)"/>
+							<path d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z" fill="url(#paint3_linear_87_7796)"/>
+						</g>
+						<defs>
+							<linearGradient id="paint0_linear_87_7796" x1="4.42591" y1="24.6668" x2="27.2309" y2="23.2764" gradientUnits="userSpaceOnUse">
+								<stop stop-color="#2086B8"/>
+								<stop offset="1" stop-color="#46D3F6"/>
+							</linearGradient>
+							<linearGradient id="paint1_linear_87_7796" x1="23.8302" y1="19.6668" x2="30.2108" y2="15.2082" gradientUnits="userSpaceOnUse">
+								<stop stop-color="#1694DB"/>
+								<stop offset="1" stop-color="#62C3FE"/>
+							</linearGradient>
+							<linearGradient id="paint2_linear_87_7796" x1="8.51037" y1="7.33333" x2="23.3335" y2="15.9348" gradientUnits="userSpaceOnUse">
+								<stop stop-color="#0D3D78"/>
+								<stop offset="1" stop-color="#063B83"/>
+							</linearGradient>
+							<linearGradient id="paint3_linear_87_7796" x1="-0.340429" y1="19.9998" x2="14.5634" y2="14.4649" gradientUnits="userSpaceOnUse">
+								<stop stop-color="#16589B"/>
+								<stop offset="1" stop-color="#1464B7"/>
+							</linearGradient>
+						</defs>
 					</svg>
 					<div class="line-clamp-1">{$i18n.t('OneDrive')}</div>
 				</DropdownMenu.Item>

+ 1 - 0
src/lib/stores/index.ts

@@ -204,6 +204,7 @@ type Config = {
 		enable_login_form: boolean;
 		enable_web_search?: boolean;
 		enable_google_drive_integration: boolean;
+		enable_onedrive_integration: boolean;
 		enable_image_generation: boolean;
 		enable_admin_export: boolean;
 		enable_admin_chat_access: boolean;

+ 0 - 42
src/lib/utils/onedrive-auth.ts

@@ -1,42 +0,0 @@
-import { PublicClientApplication } from '@azure/msal-browser';
-
-const msalParams = {
-  auth: {
-    authority: 'https://login.microsoftonline.com/consumers',
-    clientId: '2ab80a1e-7300-4cb1-beac-c38c730e8b7f'
-  }
-};
-
-// MSAL 초기화
-const app = new PublicClientApplication(msalParams);
-
-export async function initializeMsal() {
-    try {
-      await app.initialize();
-      console.log('MSAL initialized successfully');
-    } catch (error) {
-      console.error('MSAL initialization error:', error);
-    }
-  }
-
-  export async function getToken(): Promise<string> {
-    const authParams = { scopes: ['OneDrive.ReadWrite'] };
-    let accessToken = '';
-  
-    try {
-      // Ensure initialization happens early
-      await initializeMsal();  
-      const resp = await app.acquireTokenSilent(authParams);
-      accessToken = resp.accessToken;
-    } catch (err) {
-      const resp = await app.loginPopup(authParams);
-      app.setActiveAccount(resp.account);
-  
-      if (resp.idToken) {
-        const resp2 = await app.acquireTokenSilent(authParams);
-        accessToken = resp2.accessToken;
-      }
-    }
-  
-    return accessToken;
-  }

+ 251 - 196
src/lib/utils/onedrive-file-picker.ts

@@ -1,211 +1,266 @@
-// src/lib/utils/onedrive-file-picker.ts
-import { getToken } from './onedrive-auth';
+let CLIENT_ID = '';
 
+async function getCredentials() {
+  if (CLIENT_ID) return;
+  const response = await fetch('/api/config');
+  if (!response.ok) {
+    throw new Error('Failed to fetch OneDrive credentials');
+  }
+  const config = await response.json();
+  CLIENT_ID = config.onedrive?.client_id;
+  if (!CLIENT_ID) {
+    throw new Error('OneDrive client ID not configured');
+  }
+}
 
-const baseUrl = "https://onedrive.live.com/picker";
-const params = {
-	sdk: '8.0',
-	entry: {
-		oneDrive: {
-			files: {}
-		}
-	},
-	authentication: {},
-	messaging: {
-		origin: 'http://localhost:3000', // 현재 부모 페이지의 origin
-		channelId: '27' // 메시징 채널용 임의의 ID
-	},
-	typesAndSources: {
-		mode: 'files',
-		pivots: {
-			oneDrive: true,
-			recent: true
-		}
-	}
-};
-
-/**
- * OneDrive 파일 피커 창을 열고, 사용자가 선택한 파일 메타데이터를 받아오는 함수
- */
-export async function openOneDrivePicker(): Promise<any> {
-	// SSR 환경(SvelteKit)에서 window 객체가 없을 수 있으므로 가드
-	if (typeof window === 'undefined') {
-		throw new Error('Not in browser environment');
-	}
-
-	return new Promise<any>(async (resolve, reject) => {
-		let pickerWindow: Window | null = null;
-		let channelPort: MessagePort | null = null;
-
-		try {
-			const authToken = await getToken();
-			if (!authToken) {
-				return reject(new Error('Failed to acquire access token'));
-			}
-
-			// 팝업 창 오픈
-			pickerWindow = window.open('', 'OneDrivePicker', 'width=800,height=600');
-			if (!pickerWindow) {
-				return reject(new Error('Failed to open OneDrive picker window'));
-			}
-
-			// 쿼리스트링 구성
-			const queryString = new URLSearchParams({
-				filePicker: JSON.stringify(params)
-			});
-			const url = `${baseUrl}?${queryString.toString()}`;
-
-			// 새로 연 window에 form을 동적으로 추가하여 POST
-			const form = pickerWindow.document.createElement('form');
-			form.setAttribute('action', url);
-			form.setAttribute('method', 'POST');
-
-			const input = pickerWindow.document.createElement('input');
-			input.setAttribute('type', 'hidden');
-			input.setAttribute('name', 'access_token');
-			input.setAttribute('value', authToken);
-
-			form.appendChild(input);
-			pickerWindow.document.body.appendChild(form);
-			form.submit();
-
-			// 부모 창에서 message 이벤트 수신
-			const handleWindowMessage = (event: MessageEvent) => {
-				// pickerWindow가 아닌 다른 window에서 온 메시지는 무시
-				if (event.source !== pickerWindow) return;
-
-				const message = event.data;
-
-				// 초기화 메시지 => SharedWorker(MessageChannel) 식으로 포트 받기
-				if (
-					message?.type === 'initialize' &&
-					message?.channelId === params.messaging.channelId
-				) {
-					channelPort = event.ports?.[0];
-					if (!channelPort) return;
-
-					channelPort.addEventListener('message', handlePortMessage);
-					channelPort.start();
+function loadMsalScript(): Promise<void> {
+  return new Promise((resolve, reject) => {
+    const win = window;
+    if (win.msal) {
+      resolve();
+      return;
+    }
+    const script = document.createElement('script');
+    script.src = 'https://alcdn.msauth.net/browser/2.19.0/js/msal-browser.min.js';
+    script.async = true;
+    script.onload = () => resolve();
+    script.onerror = () => reject(new Error('Failed to load MSAL script'));
+    document.head.appendChild(script);
+  });
+}
 
-					// picker iframe에 'activate' 전달
-					channelPort.postMessage({
-						type: 'activate'
-					});
-				}
-			};
+let msalInstance: any;
 
-			// 포트 메시지 핸들러
-			const handlePortMessage = async (portEvent: MessageEvent) => {
-				const portData = portEvent.data;
-				switch (portData.type) {
-					case 'notification':
-						console.log('notification:', portData);
-						break;
+// Initialize MSAL authentication
+async function initializeMsal() {
+  if (!CLIENT_ID) {
+    await getCredentials();
+  }
+  const msalParams = {
+    auth: {
+      authority: 'https://login.microsoftonline.com/consumers',
+      clientId: CLIENT_ID
+    }
+  };
+  try {
+    await loadMsalScript();
+    const win = window;
+    msalInstance = new win.msal.PublicClientApplication(msalParams);
+    if (msalInstance.initialize) {
+      await msalInstance.initialize();
+    }
+  } catch (error) {
+    console.error('MSAL initialization error:', error);
+  }
+}
 
-					case 'command': {
-						// picker에 응답
-						channelPort?.postMessage({
-							type: 'acknowledge',
-							id: portData.id
-						});
+// Retrieve OneDrive access token
+async function getToken(): Promise<string> {
+  const authParams = { scopes: ['OneDrive.ReadWrite'] };
+  let accessToken = '';
+  try {
+    await initializeMsal();
+    const resp = await msalInstance.acquireTokenSilent(authParams);
+    accessToken = resp.accessToken;
+  } catch (err) {
+    const resp = await msalInstance.loginPopup(authParams);
+    msalInstance.setActiveAccount(resp.account);
+    if (resp.idToken) {
+      const resp2 = await msalInstance.acquireTokenSilent(authParams);
+      accessToken = resp2.accessToken;
+    }
+  }
+  return accessToken;
+}
 
-						const command = portData.data;
+const baseUrl = "https://onedrive.live.com/picker";
+const params = {
+  sdk: '8.0',
+  entry: {
+    oneDrive: {
+      files: {}
+    }
+  },
+  authentication: {},
+  messaging: {
+    origin: window?.location?.origin,
+    channelId: crypto.randomUUID()
+  },
+  typesAndSources: {
+    mode: 'files',
+    pivots: {
+      oneDrive: true,
+      recent: true
+    }
+  }
+};
 
-						switch (command.command) {
-							case 'authenticate': {
-								// 재인증
-								try {
-									const newToken = await getToken();
-									if (newToken) {
-										channelPort?.postMessage({
-											type: 'result',
-											id: portData.id,
-											data: {
-												result: 'token',
-												token: newToken
-											}
-										});
-									} else {
-										throw new Error('Could not retrieve auth token');
-									}
-								} catch (err) {
-									console.error(err);
-									channelPort?.postMessage({
-										result: 'error',
-										error: {
-											code: 'tokenError',
-											message: 'Failed to get token'
-										},
-										isExpected: true
-									});
-								}
-								break;
-							}
+// Download file from OneDrive
+async function downloadOneDriveFile(fileInfo: any): Promise<Blob> {
+  const accessToken = await getToken();
+  if (!accessToken) {
+    throw new Error('Unable to retrieve OneDrive access token.');
+  }
+  const fileInfoUrl = `${fileInfo["@sharePoint.endpoint"]}/drives/${fileInfo.parentReference.driveId}/items/${fileInfo.id}`;
+  const response = await fetch(fileInfoUrl, {
+    headers: {
+      'Authorization': `Bearer ${accessToken}`
+    }
+  });
+  if (!response.ok) {
+    throw new Error('Failed to fetch file information.');
+  }
+  const fileData = await response.json();
+  const downloadUrl = fileData['@content.downloadUrl'];
+  const downloadResponse = await fetch(downloadUrl);
+  if (!downloadResponse.ok) {
+    throw new Error('Failed to download file.');
+  }
+  return await downloadResponse.blob();
+}
 
-							case 'close': {
-								// 사용자가 취소하거나 닫았을 경우
-								cleanup();
-								resolve(null);
-								break;
-							}
+// Open OneDrive file picker and return selected file metadata
+export async function openOneDrivePicker(): Promise<any | null> {
+  if (typeof window === 'undefined') {
+    throw new Error('Not in browser environment');
+  }
+  return new Promise((resolve, reject) => {
+    let pickerWindow: Window | null = null;
+    let channelPort: MessagePort | null = null;
 
-							case 'pick': {
-								// 사용자가 파일 선택 완료
-								console.log('Picked:', command);
-								/**
-								 * command 안에는 사용자가 선택한 파일들의 메타데이터 정보가 들어있습니다.
-								 * 필요하다면 Microsoft Graph API 등을 통해 Blob(실제 파일 데이터)을 받아와야 할 수 있습니다.
-								 */
+    const handleWindowMessage = (event: MessageEvent) => {
+      if (event.source !== pickerWindow) return;
+      const message = event.data;
+      if (message?.type === 'initialize' && message?.channelId === params.messaging.channelId) {
+        channelPort = event.ports?.[0];
+        if (!channelPort) return;
+        channelPort.addEventListener('message', handlePortMessage);
+        channelPort.start();
+        channelPort.postMessage({ type: 'activate' });
+      }
+    };
 
-								// picker에 응답
-								channelPort?.postMessage({
-									type: 'result',
-									id: portData.id,
-									data: {
-										result: 'success'
-									}
-								});
+    const handlePortMessage = async (portEvent: MessageEvent) => {
+      const portData = portEvent.data;
+      switch (portData.type) {
+        case 'notification':
+          break;
+        case 'command': {
+          channelPort?.postMessage({ type: 'acknowledge', id: portData.id });
+          const command = portData.data;
+          switch (command.command) {
+            case 'authenticate': {
+              try {
+                const newToken = await getToken();
+                if (newToken) {
+                  channelPort?.postMessage({
+                    type: 'result',
+                    id: portData.id,
+                    data: { result: 'token', token: newToken }
+                  });
+                } else {
+                  throw new Error('Could not retrieve auth token');
+                }
+              } catch (err) {
+                console.error(err);
+                channelPort?.postMessage({
+                  result: 'error',
+                  error: { code: 'tokenError', message: 'Failed to get token' },
+                  isExpected: true
+                });
+              }
+              break;
+            }
+            case 'close': {
+              cleanup();
+              resolve(null);
+              break;
+            }
+            case 'pick': {
+              channelPort?.postMessage({
+                type: 'result',
+                id: portData.id,
+                data: { result: 'success' }
+              });
+              cleanup();
+              resolve(command);
+              break;
+            }
+            default: {
+              console.warn('Unsupported command:', command);
+              channelPort?.postMessage({
+                result: 'error',
+                error: { code: 'unsupportedCommand', message: command.command },
+                isExpected: true
+              });
+              break;
+            }
+          }
+          break;
+        }
+      }
+    };
 
-								// 선택한 파일들(메타정보)을 resolve
-								cleanup();
-								resolve(command);
-								break;
-							}
+    function cleanup() {
+      window.removeEventListener('message', handleWindowMessage);
+      if (channelPort) {
+        channelPort.removeEventListener('message', handlePortMessage);
+      }
+      if (pickerWindow) {
+        pickerWindow.close();
+        pickerWindow = null;
+      }
+    }
 
-							default: {
-								console.warn('Unsupported command:', command);
-								channelPort?.postMessage({
-									result: 'error',
-									error: {
-										code: 'unsupportedCommand',
-										message: command.command
-									},
-									isExpected: true
-								});
-								break;
-							}
-						}
-						break;
-					}
-				}
-			};
+    const initializePicker = async () => {
+      try {
+        const authToken = await getToken();
+        if (!authToken) {
+          return reject(new Error('Failed to acquire access token'));
+        }
+        pickerWindow = window.open('', 'OneDrivePicker', 'width=800,height=600');
+        if (!pickerWindow) {
+          return reject(new Error('Failed to open OneDrive picker window'));
+        }
+        const queryString = new URLSearchParams({
+          filePicker: JSON.stringify(params)
+        });
+        const url = `${baseUrl}?${queryString.toString()}`;
+        const form = pickerWindow.document.createElement('form');
+        form.setAttribute('action', url);
+        form.setAttribute('method', 'POST');
+        const input = pickerWindow.document.createElement('input');
+        input.setAttribute('type', 'hidden');
+        input.setAttribute('name', 'access_token');
+        input.setAttribute('value', authToken);
+        form.appendChild(input);
+        pickerWindow.document.body.appendChild(form);
+        form.submit();
+        window.addEventListener('message', handleWindowMessage);
+      } catch (err) {
+        if (pickerWindow) pickerWindow.close();
+        reject(err);
+      }
+    };
 
-			function cleanup() {
-				window.removeEventListener('message', handleWindowMessage);
-				if (channelPort) {
-					channelPort.removeEventListener('message', handlePortMessage);
-				}
-				if (pickerWindow) {
-					pickerWindow.close();
-					pickerWindow = null;
-				}
-			}
+    initializePicker();
+  });
+}
 
-			// 메시지 이벤트 등록
-			window.addEventListener('message', handleWindowMessage);
-		} catch (err) {
-			if (pickerWindow) pickerWindow.close();
-			reject(err);
-		}
-	});
+// Pick and download file from OneDrive
+export async function pickAndDownloadFile(): Promise<{ blob: Blob; name: string } | null> {
+  try {
+    const pickerResult = await openOneDrivePicker();
+    if (!pickerResult || !pickerResult.items || pickerResult.items.length === 0) {
+      return null;
+    }
+    const selectedFile = pickerResult.items[0];
+    const blob = await downloadOneDriveFile(selectedFile);
+    return { blob, name: selectedFile.name };
+  } catch (error) {
+    console.error('Error occurred during OneDrive file pick/download:', error);
+    throw error;
+  }
 }
+
+export { downloadOneDriveFile };