From 80e60a58c5ea505222575482a77680724a9a862d Mon Sep 17 00:00:00 2001 From: yiboyasss <3359595624@qq.com> Date: Fri, 26 Sep 2025 11:27:01 +0800 Subject: [PATCH 01/25] feat: Add model feature in Launch Model list --- xinference/ui/web/ui/src/locales/en.json | 25 +- xinference/ui/web/ui/src/locales/ja.json | 25 +- xinference/ui/web/ui/src/locales/ko.json | 25 +- xinference/ui/web/ui/src/locales/zh.json | 25 +- .../launch_model/components/addModelDialog.js | 228 ++++++++++++++++++ .../web/ui/src/scenes/launch_model/index.js | 19 +- 6 files changed, 341 insertions(+), 6 deletions(-) create mode 100644 xinference/ui/web/ui/src/scenes/launch_model/components/addModelDialog.js diff --git a/xinference/ui/web/ui/src/locales/en.json b/xinference/ui/web/ui/src/locales/en.json index a12662732d..b39f1e2939 100644 --- a/xinference/ui/web/ui/src/locales/en.json +++ b/xinference/ui/web/ui/src/locales/en.json @@ -124,7 +124,30 @@ "featured": "featured", "all": "all", "cancelledSuccessfully": "Cancelled Successfully!", - "mustBeUnique": "{{key}} must be unique" + "mustBeUnique": "{{key}} must be unique", + "addModel": "Add Model", + "addModelDialog": { + "introPrefix": "To add a model, please use", + "platformLinkText": "Model Management Platform", + "introSuffix": " and paste the model's URL", + "example": "Example: The URL for {{modelName}} on the platform is {{modelUrl}}", + "urlLabel": "URL" + }, + "loginDialog": { + "title": "No permission to download this model. Please log in and try again.", + "usernameOrEmail": "Username or Email", + "password": "Password", + "login": "Login" + }, + "error": { + "cannotExtractModelId": "Unable to extract model_id from URL. Please check your input.", + "downloadFailed": "Download failed: {{status}} {{text}}", + "requestFailed": "Request failed", + "loginFailedText": "Login failed: {{status}} {{text}}", + "noTokenAfterLogin": "Login succeeded but no token was returned", + "modelPrivate": "This model is private and requires download permission.", + "noPermissionAfterLogin": "The logged-in account does not have permission to download this model. Please contact the administrator or use a different account." + } }, "runningModels": { diff --git a/xinference/ui/web/ui/src/locales/ja.json b/xinference/ui/web/ui/src/locales/ja.json index dc1636bfd3..2dd70bc1ab 100644 --- a/xinference/ui/web/ui/src/locales/ja.json +++ b/xinference/ui/web/ui/src/locales/ja.json @@ -124,7 +124,30 @@ "featured": "おすすめとお気に入り", "all": "すべて", "cancelledSuccessfully": "正常にキャンセルされました!", - "mustBeUnique": "{{key}} は一意でなければなりません" + "mustBeUnique": "{{key}} は一意でなければなりません", + "addModel": "モデルを追加", + "addModelDialog": { + "introPrefix": "モデルを追加するには", + "platformLinkText": "モデル管理プラットフォーム", + "introSuffix": "に基づき、対応するURLを入力してください", + "example": "例:{{modelName}} のモデル管理プラットフォーム上のURLは {{modelUrl}} です", + "urlLabel": "URL" + }, + "loginDialog": { + "title": "このモデルをダウンロードする権限がありません。ログイン後に再度お試しください", + "usernameOrEmail": "ユーザー名またはメールアドレス", + "password": "パスワード", + "login": "ログイン" + }, + "error": { + "cannotExtractModelId": "URLから model_id を抽出できません。入力内容を確認してください", + "downloadFailed": "ダウンロード失敗: {{status}} {{text}}", + "requestFailed": "リクエスト失敗", + "loginFailedText": "ログイン失敗: {{status}} {{text}}", + "noTokenAfterLogin": "ログインは成功しましたが、トークンを取得できませんでした", + "modelPrivate": "このモデルは非公開であり、ダウンロード権限が必要です。", + "noPermissionAfterLogin": "このアカウントにはモデルをダウンロードする権限がありません。管理者に連絡するか、別のアカウントを使用してください。" + } }, "runningModels": { diff --git a/xinference/ui/web/ui/src/locales/ko.json b/xinference/ui/web/ui/src/locales/ko.json index 17ad7626a6..f6eeb9b51d 100644 --- a/xinference/ui/web/ui/src/locales/ko.json +++ b/xinference/ui/web/ui/src/locales/ko.json @@ -124,7 +124,30 @@ "featured": "추천 및 즐겨찾기", "all": "모두", "cancelledSuccessfully": "성공적으로 취소되었습니다!", - "mustBeUnique": "{{key}} 는 고유해야 합니다" + "mustBeUnique": "{{key}} 는 고유해야 합니다", + "addModel": "모델 추가", + "addModelDialog": { + "introPrefix": "모델을 추가하려면", + "platformLinkText": "모델 관리 플랫폼", + "introSuffix": "을(를) 기반으로 해당 URL을 입력하세요", + "example": "예: {{modelName}}의 모델 관리 플랫폼 URL은 {{modelUrl}} 입니다", + "urlLabel": "URL" + }, + "loginDialog": { + "title": "이 모델을 다운로드할 권한이 없습니다. 로그인 후 다시 시도하세요", + "usernameOrEmail": "사용자 이름 또는 이메일", + "password": "비밀번호", + "login": "로그인" + }, + "error": { + "cannotExtractModelId": "URL에서 model_id를 추출할 수 없습니다. 입력을 확인하세요", + "downloadFailed": "다운로드 실패: {{status}} {{text}}", + "requestFailed": "요청 실패", + "loginFailedText": "로그인 실패: {{status}} {{text}}", + "noTokenAfterLogin": "로그인은 성공했지만 토큰을 가져오지 못했습니다", + "modelPrivate": "이 모델은 비공개이며 다운로드 권한이 필요합니다.", + "noPermissionAfterLogin": "이 계정에는 해당 모델을 다운로드할 권한이 없습니다. 관리자에게 문의하거나 다른 계정을 사용하세요." + } }, "runningModels": { diff --git a/xinference/ui/web/ui/src/locales/zh.json b/xinference/ui/web/ui/src/locales/zh.json index 36daec1756..066781855a 100644 --- a/xinference/ui/web/ui/src/locales/zh.json +++ b/xinference/ui/web/ui/src/locales/zh.json @@ -124,7 +124,30 @@ "featured": "推荐和收藏", "all": "全部", "cancelledSuccessfully": "取消成功!", - "mustBeUnique": "{{key}} 必须唯一" + "mustBeUnique": "{{key}} 必须唯一", + "addModel": "添加模型", + "addModelDialog": { + "introPrefix": "添加模型需基于", + "platformLinkText": "模型管理平台", + "introSuffix": ",填写模型对应的 URL", + "example": "例:{{modelName}}在模型管理平台上对应的 URL 如下 {{modelUrl}}", + "urlLabel": "URL" + }, + "loginDialog": { + "title": "暂无权限下载该模型,登录后重新尝试下载", + "usernameOrEmail": "用户名或邮箱", + "password": "密码", + "login": "登录" + }, + "error": { + "cannotExtractModelId": "无法从 URL 中提取 model_id,请检查输入", + "downloadFailed": "下载失败: {{status}} {{text}}", + "requestFailed": "请求失败", + "loginFailedText": "登录失败: {{status}} {{text}}", + "noTokenAfterLogin": "登录成功但未获取到 token", + "modelPrivate": "该模型为私有,需要具有下载权限。", + "noPermissionAfterLogin": "该登录账户暂无权限下载该模型,请联系管理员或更换账户。" + } }, "runningModels": { diff --git a/xinference/ui/web/ui/src/scenes/launch_model/components/addModelDialog.js b/xinference/ui/web/ui/src/scenes/launch_model/components/addModelDialog.js new file mode 100644 index 0000000000..08484e70ba --- /dev/null +++ b/xinference/ui/web/ui/src/scenes/launch_model/components/addModelDialog.js @@ -0,0 +1,228 @@ + +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + TextField +} from '@mui/material' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' + +const API_BASE_URL = 'https://model.xinference.io' + +const AddModelDialog = ({ open, onClose }) => { + const { t } = useTranslation() + const [url, setUrl] = useState('') + const [loginOpen, setLoginOpen] = useState(false) + const [usernameOrEmail, setUsernameOrEmail] = useState('') + const [password, setPassword] = useState('') + const [pendingModelId, setPendingModelId] = useState(null) + const [loading, setLoading] = useState(false) + const [errorMsg, setErrorMsg] = useState('') + + const handleClose = (type) => { + setErrorMsg(''); + + const actions = { + add: onClose, + login: () => setLoginOpen(false), + }; + + actions[type]?.(); + }; + + const extractModelId = (input) => { + try { + const u = new URL(input) + const m1 = u.pathname.match(/\/(\d+)(?:\/?$)/) + if (m1 && m1[1]) return m1[1] + const qp = u.searchParams.get('model_id') + if (qp) return qp + } catch (e) { + const m2 = String(input).match(/(\d+)(?:\/?$)/) + if (m2 && m2[1]) return m2[1] + } + return null + } + + const performDownload = async (modelId, token, fromLogin = false) => { + const endpoint = `${API_BASE_URL}/api/models/download?model_id=${encodeURIComponent(modelId)}` + const headers = token ? { Authorization: `Bearer ${token}` } : {} + setLoading(true) + setErrorMsg('') + try { + const res = await fetch(endpoint, { + method: 'GET', + headers, + }) + if (res.status === 403) { + let detailMsg = '' + try { + const body = await res.json() + if (body?.error_code === 'MODEL_PRIVATE') { + detailMsg = t('launchModel.error.modelPrivate') + } else if (body?.message) { + detailMsg = body.message + } + } catch { + // ignore and use default message + } + + if (fromLogin) { + setErrorMsg(detailMsg || t('launchModel.error.noPermissionAfterLogin')) + return + } else { + setPendingModelId(modelId) + setLoginOpen(true) + return + } + } + if (!res.ok) { + const text = await res.text().catch(() => '') + throw new Error(t('launchModel.error.downloadFailed', { status: res.status, text })) + } + const data = await res.json() + console.log('models/download 响应:', data) + handleClose('add') + } catch (err) { + console.error(err) + setErrorMsg(err.message || t('launchModel.error.requestFailed')) + } finally { + setLoading(false) + } + } + + const handleFormSubmit = async (e) => { + e.preventDefault() + const modelId = extractModelId(url?.trim()) + if (!modelId) { + setErrorMsg(t('launchModel.error.cannotExtractModelId')) + return + } + await performDownload(modelId) + } + + const handleLoginSubmit = async (e) => { + e.preventDefault() + if (!pendingModelId) return + setLoading(true) + setErrorMsg('') + try { + const loginRes = await fetch(`${API_BASE_URL}/api/users/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + usernameOrEmail: usernameOrEmail.trim(), + password: password, + }), + }) + if (!loginRes.ok) { + const text = await loginRes.text().catch(() => '') + throw new Error(t('launchModel.error.loginFailedText', { status: loginRes.status, text })) + } + const loginJson = await loginRes.json() + const token = loginJson.data?.accessToken + if (!token) { + throw new Error(t('launchModel.error.noTokenAfterLogin')) + } + handleClose('login') + await performDownload(pendingModelId, token, true) + } catch (err) { + console.error(err) + setErrorMsg(err.message || t('launchModel.error.requestFailed')) + } finally { + setLoading(false) + } + } + + return ( + handleClose('add')} width={500}> + {t('launchModel.addModel')} + +
+
+ {t('launchModel.addModelDialog.introPrefix')}{' '} + + {t('launchModel.addModelDialog.platformLinkText')} + + {t('launchModel.addModelDialog.introSuffix')} +
+
+ {t('launchModel.addModelDialog.example', { + modelName: 'qwen3', + modelUrl: 'https://model.xinference.io/models/detail/250' + })} +
+
+ { + setUrl(e.target.value) + }} + disabled={loading} + /> + + {errorMsg &&
{errorMsg}
} +
+
+ + + + + + {/* 403 */} + handleClose('login')}> + {t('launchModel.loginDialog.title')} + +
+ setUsernameOrEmail(e.target.value)} + disabled={loading} + /> + setPassword(e.target.value)} + disabled={loading} + /> + + {errorMsg &&
{errorMsg}
} +
+ + + + +
+
+ ) +} + +export default AddModelDialog \ No newline at end of file diff --git a/xinference/ui/web/ui/src/scenes/launch_model/index.js b/xinference/ui/web/ui/src/scenes/launch_model/index.js index 24f886a80d..dda8595cbf 100644 --- a/xinference/ui/web/ui/src/scenes/launch_model/index.js +++ b/xinference/ui/web/ui/src/scenes/launch_model/index.js @@ -1,5 +1,6 @@ +import Add from '@mui/icons-material/Add' import { TabContext, TabList, TabPanel } from '@mui/lab' -import { Box, Tab } from '@mui/material' +import { Box, Button, Tab } from '@mui/material' import React, { useContext, useEffect, useState } from 'react' import { useCookies } from 'react-cookie' import { useTranslation } from 'react-i18next' @@ -11,6 +12,7 @@ import fetchWrapper from '../../components/fetchWrapper' import SuccessMessageSnackBar from '../../components/successMessageSnackBar' import Title from '../../components/Title' import { isValidBearerToken } from '../../components/utils' +import AddModelDialog from './components/addModelDialog' import { featureModels } from './data/data' import LaunchCustom from './launchCustom' import LaunchModelComponent from './LaunchModel' @@ -22,6 +24,7 @@ const LaunchModel = () => { : '/launch_model/llm' ) const [gpuAvailable, setGPUAvailable] = useState(-1) + const [open, setOpen] = useState(false) const { setErrorMsg } = useContext(ApiContext) const [cookie] = useCookies(['token']) @@ -65,7 +68,15 @@ const LaunchModel = () => { - + { value="/launch_model/custom/llm" /> + { + setOpen(false)} /> ) } From 0eab739b3d097bf1522508578d964c6d96c24933 Mon Sep 17 00:00:00 2001 From: yiboyasss <3359595624@qq.com> Date: Fri, 26 Sep 2025 11:29:59 +0800 Subject: [PATCH 02/25] fix: detail --- .../launch_model/components/addModelDialog.js | 449 ++++++++++-------- .../web/ui/src/scenes/launch_model/index.js | 6 +- 2 files changed, 250 insertions(+), 205 deletions(-) diff --git a/xinference/ui/web/ui/src/scenes/launch_model/components/addModelDialog.js b/xinference/ui/web/ui/src/scenes/launch_model/components/addModelDialog.js index 08484e70ba..7f6093e8a7 100644 --- a/xinference/ui/web/ui/src/scenes/launch_model/components/addModelDialog.js +++ b/xinference/ui/web/ui/src/scenes/launch_model/components/addModelDialog.js @@ -1,11 +1,10 @@ - import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - TextField + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + TextField, } from '@mui/material' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -13,216 +12,258 @@ import { useTranslation } from 'react-i18next' const API_BASE_URL = 'https://model.xinference.io' const AddModelDialog = ({ open, onClose }) => { - const { t } = useTranslation() - const [url, setUrl] = useState('') - const [loginOpen, setLoginOpen] = useState(false) - const [usernameOrEmail, setUsernameOrEmail] = useState('') - const [password, setPassword] = useState('') - const [pendingModelId, setPendingModelId] = useState(null) - const [loading, setLoading] = useState(false) - const [errorMsg, setErrorMsg] = useState('') + const { t } = useTranslation() + const [url, setUrl] = useState('') + const [loginOpen, setLoginOpen] = useState(false) + const [usernameOrEmail, setUsernameOrEmail] = useState('') + const [password, setPassword] = useState('') + const [pendingModelId, setPendingModelId] = useState(null) + const [loading, setLoading] = useState(false) + const [errorMsg, setErrorMsg] = useState('') - const handleClose = (type) => { - setErrorMsg(''); - - const actions = { - add: onClose, - login: () => setLoginOpen(false), - }; - - actions[type]?.(); - }; + const handleClose = (type) => { + setErrorMsg('') - const extractModelId = (input) => { - try { - const u = new URL(input) - const m1 = u.pathname.match(/\/(\d+)(?:\/?$)/) - if (m1 && m1[1]) return m1[1] - const qp = u.searchParams.get('model_id') - if (qp) return qp - } catch (e) { - const m2 = String(input).match(/(\d+)(?:\/?$)/) - if (m2 && m2[1]) return m2[1] - } - return null + const actions = { + add: onClose, + login: () => setLoginOpen(false), + } + + actions[type]?.() + } + + const extractModelId = (input) => { + try { + const u = new URL(input) + const m1 = u.pathname.match(/\/(\d+)(?:\/?$)/) + if (m1 && m1[1]) return m1[1] + const qp = u.searchParams.get('model_id') + if (qp) return qp + } catch (e) { + const m2 = String(input).match(/(\d+)(?:\/?$)/) + if (m2 && m2[1]) return m2[1] } + return null + } - const performDownload = async (modelId, token, fromLogin = false) => { - const endpoint = `${API_BASE_URL}/api/models/download?model_id=${encodeURIComponent(modelId)}` - const headers = token ? { Authorization: `Bearer ${token}` } : {} - setLoading(true) - setErrorMsg('') + const performDownload = async (modelId, token, fromLogin = false) => { + const endpoint = `${API_BASE_URL}/api/models/download?model_id=${encodeURIComponent( + modelId + )}` + const headers = token ? { Authorization: `Bearer ${token}` } : {} + setLoading(true) + setErrorMsg('') + try { + const res = await fetch(endpoint, { + method: 'GET', + headers, + }) + if (res.status === 403) { + let detailMsg = '' try { - const res = await fetch(endpoint, { - method: 'GET', - headers, - }) - if (res.status === 403) { - let detailMsg = '' - try { - const body = await res.json() - if (body?.error_code === 'MODEL_PRIVATE') { - detailMsg = t('launchModel.error.modelPrivate') - } else if (body?.message) { - detailMsg = body.message - } - } catch { - // ignore and use default message - } + const body = await res.json() + if (body?.error_code === 'MODEL_PRIVATE') { + detailMsg = t('launchModel.error.modelPrivate') + } else if (body?.message) { + detailMsg = body.message + } + } catch { + // ignore and use default message + } - if (fromLogin) { - setErrorMsg(detailMsg || t('launchModel.error.noPermissionAfterLogin')) - return - } else { - setPendingModelId(modelId) - setLoginOpen(true) - return - } - } - if (!res.ok) { - const text = await res.text().catch(() => '') - throw new Error(t('launchModel.error.downloadFailed', { status: res.status, text })) - } - const data = await res.json() - console.log('models/download 响应:', data) - handleClose('add') - } catch (err) { - console.error(err) - setErrorMsg(err.message || t('launchModel.error.requestFailed')) - } finally { - setLoading(false) + if (fromLogin) { + setErrorMsg( + detailMsg || t('launchModel.error.noPermissionAfterLogin') + ) + return + } else { + setPendingModelId(modelId) + setLoginOpen(true) + return } + } + if (!res.ok) { + const text = await res.text().catch(() => '') + throw new Error( + t('launchModel.error.downloadFailed', { status: res.status, text }) + ) + } + const data = await res.json() + console.log('models/download 响应:', data) + handleClose('add') + } catch (err) { + console.error(err) + setErrorMsg(err.message || t('launchModel.error.requestFailed')) + } finally { + setLoading(false) } + } - const handleFormSubmit = async (e) => { - e.preventDefault() - const modelId = extractModelId(url?.trim()) - if (!modelId) { - setErrorMsg(t('launchModel.error.cannotExtractModelId')) - return - } - await performDownload(modelId) + const handleFormSubmit = async (e) => { + e.preventDefault() + const modelId = extractModelId(url?.trim()) + if (!modelId) { + setErrorMsg(t('launchModel.error.cannotExtractModelId')) + return } + await performDownload(modelId) + } - const handleLoginSubmit = async (e) => { - e.preventDefault() - if (!pendingModelId) return - setLoading(true) - setErrorMsg('') - try { - const loginRes = await fetch(`${API_BASE_URL}/api/users/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - usernameOrEmail: usernameOrEmail.trim(), - password: password, - }), - }) - if (!loginRes.ok) { - const text = await loginRes.text().catch(() => '') - throw new Error(t('launchModel.error.loginFailedText', { status: loginRes.status, text })) - } - const loginJson = await loginRes.json() - const token = loginJson.data?.accessToken - if (!token) { - throw new Error(t('launchModel.error.noTokenAfterLogin')) - } - handleClose('login') - await performDownload(pendingModelId, token, true) - } catch (err) { - console.error(err) - setErrorMsg(err.message || t('launchModel.error.requestFailed')) - } finally { - setLoading(false) - } + const handleLoginSubmit = async (e) => { + e.preventDefault() + if (!pendingModelId) return + setLoading(true) + setErrorMsg('') + try { + const loginRes = await fetch(`${API_BASE_URL}/api/users/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + usernameOrEmail: usernameOrEmail.trim(), + password: password, + }), + }) + if (!loginRes.ok) { + const text = await loginRes.text().catch(() => '') + throw new Error( + t('launchModel.error.loginFailedText', { + status: loginRes.status, + text, + }) + ) + } + const loginJson = await loginRes.json() + const token = loginJson.data?.accessToken + if (!token) { + throw new Error(t('launchModel.error.noTokenAfterLogin')) + } + handleClose('login') + await performDownload(pendingModelId, token, true) + } catch (err) { + console.error(err) + setErrorMsg(err.message || t('launchModel.error.requestFailed')) + } finally { + setLoading(false) } + } - return ( - handleClose('add')} width={500}> - {t('launchModel.addModel')} - -
-
- {t('launchModel.addModelDialog.introPrefix')}{' '} - - {t('launchModel.addModelDialog.platformLinkText')} - - {t('launchModel.addModelDialog.introSuffix')} -
-
- {t('launchModel.addModelDialog.example', { - modelName: 'qwen3', - modelUrl: 'https://model.xinference.io/models/detail/250' - })} -
-
- { - setUrl(e.target.value) - }} - disabled={loading} - /> - - {errorMsg &&
{errorMsg}
} -
-
- - - - + return ( + handleClose('add')} width={500}> + {t('launchModel.addModel')} + +
+
+ {t('launchModel.addModelDialog.introPrefix')}{' '} + + {t('launchModel.addModelDialog.platformLinkText')} + + {t('launchModel.addModelDialog.introSuffix')} +
+
+ {t('launchModel.addModelDialog.example', { + modelName: 'qwen3', + modelUrl: 'https://model.xinference.io/models/detail/250', + })} +
+
+ { + setUrl(e.target.value) + }} + disabled={loading} + /> + + {errorMsg &&
{errorMsg}
} +
+
+ + + + - {/* 403 */} - handleClose('login')}> - {t('launchModel.loginDialog.title')} - -
- setUsernameOrEmail(e.target.value)} - disabled={loading} - /> - setPassword(e.target.value)} - disabled={loading} - /> - - {errorMsg &&
{errorMsg}
} -
- - - - -
-
- ) + {/* 403 */} + handleClose('login')}> + {t('launchModel.loginDialog.title')} + +
+ setUsernameOrEmail(e.target.value)} + disabled={loading} + /> + setPassword(e.target.value)} + disabled={loading} + /> + + {errorMsg && ( +
{errorMsg}
+ )} +
+ + + + +
+
+ ) } -export default AddModelDialog \ No newline at end of file +export default AddModelDialog diff --git a/xinference/ui/web/ui/src/scenes/launch_model/index.js b/xinference/ui/web/ui/src/scenes/launch_model/index.js index dda8595cbf..e1cfd1b0e1 100644 --- a/xinference/ui/web/ui/src/scenes/launch_model/index.js +++ b/xinference/ui/web/ui/src/scenes/launch_model/index.js @@ -92,7 +92,11 @@ const LaunchModel = () => { value="/launch_model/custom/llm" /> - From 3f859be262be4a91f5e2cd78fa578bf422b69070 Mon Sep 17 00:00:00 2001 From: yiboyasss <3359595624@qq.com> Date: Mon, 20 Oct 2025 11:08:58 +0800 Subject: [PATCH 03/25] fix: login dialog --- .../launch_model/components/addModelDialog.js | 185 +++++++++--------- 1 file changed, 91 insertions(+), 94 deletions(-) diff --git a/xinference/ui/web/ui/src/scenes/launch_model/components/addModelDialog.js b/xinference/ui/web/ui/src/scenes/launch_model/components/addModelDialog.js index 7f6093e8a7..9f0c58d6f0 100644 --- a/xinference/ui/web/ui/src/scenes/launch_model/components/addModelDialog.js +++ b/xinference/ui/web/ui/src/scenes/launch_model/components/addModelDialog.js @@ -6,7 +6,7 @@ import { DialogTitle, TextField, } from '@mui/material' -import React, { useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' const API_BASE_URL = 'https://model.xinference.io' @@ -15,11 +15,10 @@ const AddModelDialog = ({ open, onClose }) => { const { t } = useTranslation() const [url, setUrl] = useState('') const [loginOpen, setLoginOpen] = useState(false) - const [usernameOrEmail, setUsernameOrEmail] = useState('') - const [password, setPassword] = useState('') const [pendingModelId, setPendingModelId] = useState(null) const [loading, setLoading] = useState(false) const [errorMsg, setErrorMsg] = useState('') + const loginIframeRef = useRef(null) const handleClose = (type) => { setErrorMsg('') @@ -46,11 +45,17 @@ const AddModelDialog = ({ open, onClose }) => { return null } - const performDownload = async (modelId, token, fromLogin = false) => { + // 修改:download 默认从 sessionStorage 读取 token(若传参提供则优先) + // performDownload:收到 token 后直连接口,获取 JSON + const performDownload = async (modelId, tokenFromParam, fromLogin = false) => { const endpoint = `${API_BASE_URL}/api/models/download?model_id=${encodeURIComponent( modelId )}` - const headers = token ? { Authorization: `Bearer ${token}` } : {} + const effectiveToken = + tokenFromParam || + sessionStorage.getItem('model_hub_token') || + localStorage.getItem('io_login_success') + const headers = effectiveToken ? { Authorization: `Bearer ${effectiveToken}` } : {} setLoading(true) setErrorMsg('') try { @@ -58,6 +63,44 @@ const AddModelDialog = ({ open, onClose }) => { method: 'GET', headers, }) + + if (res.status === 401) { + const refreshToken = sessionStorage.getItem('model_hub_refresh_token') + if (!refreshToken) { + sessionStorage.removeItem('model_hub_token') + setPendingModelId(modelId) + setLoginOpen(true) + return + } + try { + const refreshRes = await fetch(`${API_BASE_URL}/api/users/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token: refreshToken }), + }) + if (!refreshRes.ok) { + throw new Error(`refresh failed: ${refreshRes.status}`) + } + const refreshData = await refreshRes.json().catch(() => ({})) + const newToken = refreshData?.data?.accessToken + if (newToken) { + sessionStorage.setItem('model_hub_token', newToken) + await performDownload(modelId, newToken, false) + return + } else { + sessionStorage.removeItem('model_hub_token') + setPendingModelId(modelId) + setLoginOpen(true) + return + } + } catch (e) { + sessionStorage.removeItem('model_hub_token') + setPendingModelId(modelId) + setLoginOpen(true) + return + } + } + if (res.status === 403) { let detailMsg = '' try { @@ -68,13 +111,11 @@ const AddModelDialog = ({ open, onClose }) => { detailMsg = body.message } } catch { - // ignore and use default message + console.log(''); + } - if (fromLogin) { - setErrorMsg( - detailMsg || t('launchModel.error.noPermissionAfterLogin') - ) + setErrorMsg(detailMsg || t('launchModel.error.noPermissionAfterLogin')) return } else { setPendingModelId(modelId) @@ -82,6 +123,7 @@ const AddModelDialog = ({ open, onClose }) => { return } } + if (!res.ok) { const text = await res.text().catch(() => '') throw new Error( @@ -109,43 +151,26 @@ const AddModelDialog = ({ open, onClose }) => { await performDownload(modelId) } - const handleLoginSubmit = async (e) => { - e.preventDefault() - if (!pendingModelId) return - setLoading(true) - setErrorMsg('') - try { - const loginRes = await fetch(`${API_BASE_URL}/api/users/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - usernameOrEmail: usernameOrEmail.trim(), - password: password, - }), - }) - if (!loginRes.ok) { - const text = await loginRes.text().catch(() => '') - throw new Error( - t('launchModel.error.loginFailedText', { - status: loginRes.status, - text, - }) - ) - } - const loginJson = await loginRes.json() - const token = loginJson.data?.accessToken - if (!token) { - throw new Error(t('launchModel.error.noTokenAfterLogin')) + useEffect(() => { + const listener = (event) => { + if (event.origin !== API_BASE_URL) return + const { type, token, refresh_token } = event.data || {} + + if (type === 'io_login_success' && token && refresh_token) { + handleClose('login') + sessionStorage.setItem('model_hub_token', token) + sessionStorage.setItem('model_hub_refresh_token', refresh_token) + if (pendingModelId) { + void performDownload(pendingModelId, token, true) + } } - handleClose('login') - await performDownload(pendingModelId, token, true) - } catch (err) { - console.error(err) - setErrorMsg(err.message || t('launchModel.error.requestFailed')) - } finally { - setLoading(false) } - } + + window.addEventListener('message', listener) + return () => { + window.removeEventListener('message', listener) + } + }, [pendingModelId]) return ( handleClose('add')} width={500}> @@ -163,7 +188,7 @@ const AddModelDialog = ({ open, onClose }) => {
{t('launchModel.addModelDialog.introPrefix')}{' '} { - {/* 403 */} handleClose('login')}> - {t('launchModel.loginDialog.title')} - -
- setUsernameOrEmail(e.target.value)} - disabled={loading} - /> - setPassword(e.target.value)} - disabled={loading} - /> - - {errorMsg && ( -
{errorMsg}
- )} -
- - - - +
+