diff --git a/public/index.html b/public/index.html index 9286084..d37c1d1 100644 --- a/public/index.html +++ b/public/index.html @@ -39,6 +39,7 @@

🔐 OpenList Token 获取工具

+ @@ -84,6 +85,65 @@

🔐 OpenList Token 获取工具

+ +
@@ -173,6 +233,7 @@

隐私政策和使用条款

+ diff --git a/public/static/event.js b/public/static/event.js index 1ab51c0..2178534 100644 --- a/public/static/event.js +++ b/public/static/event.js @@ -31,6 +31,40 @@ function onSelect() { const appSecretContainer = client_key_input.closest('.mb-3'); const serverUseContainer = server_use_input.closest('.mb-3'); const callbackContainer = direct_url_input.closest('.mb-3'); + const clientIdLabel = clientIdContainer.querySelector('label'); + const appSecretLabel = appSecretContainer.querySelector('label'); + const callbackLabel = callbackContainer.querySelector('label'); + const pdsViews = document.getElementById('pds-views'); + const accessTokenInput = document.getElementById('access-token'); + const refreshTokenInput = document.getElementById('refresh-token'); + clientIdLabel.textContent = '客户端ID(ClientID/AppID)'; + appSecretLabel.textContent = '应用秘钥 (AppKey/Secret)'; + callbackLabel.textContent = '回调地址(Callback URL)'; + pdsViews.hidden = true; + accessTokenInput.readOnly = true; + refreshTokenInput.readOnly = true; + accessTokenInput.setAttribute('onclick', 'autoCopy(this)'); + refreshTokenInput.setAttribute('onclick', 'autoCopy(this)'); + if (driver_txt_input.value === "pds_go") { + clientIdContainer.style.display = 'block'; + appSecretContainer.style.display = 'block'; + serverUseContainer.style.display = 'none'; + secret_key_views.hidden = true; + callbackContainer.style.display = 'block'; + clientIdLabel.textContent = 'PDS Domain ID'; + appSecretLabel.textContent = 'Client ID'; + callbackLabel.textContent = '授权链接'; + direct_url_input.value = ''; + pdsViews.hidden = false; + accessTokenInput.readOnly = false; + refreshTokenInput.readOnly = false; + accessTokenInput.removeAttribute('onclick'); + refreshTokenInput.removeAttribute('onclick'); + shared_all_views.hidden = true; + shared_btn_views.classList.remove('d-grid'); + if (typeof initPDSDefaults === 'function') initPDSDefaults(); + return; + } // 阿里云盘扫码登录v2不需要客户端ID、应用机密和回调地址 ================ if (driver_txt_input.value === "alicloud_cs" || driver_txt_input.value === "alicloud_tv" diff --git a/public/static/login.js b/public/static/login.js index 2e2b087..6363675 100644 --- a/public/static/login.js +++ b/public/static/login.js @@ -15,6 +15,11 @@ async function getLogin(refresh = false) { const qrModalTitle = document.getElementById('qr-modal-title'); let driver_pre = driver_txt.split("_")[0] let check_flag = true; + if (driver_txt === "pds_go") { + if (refresh) await refreshPdsToken(); + else await startPdsLogin(); + return; + } // 阿里云盘扫码v2直接调用专用API,不需要构建传统的requests路径 if (driver_txt === "alicloud_cs" && !refresh) { qrModalTitle.textContent = '阿里云盘扫码登录v2'; diff --git a/public/static/pds.js b/public/static/pds.js new file mode 100644 index 0000000..3eb9a02 --- /dev/null +++ b/public/static/pds.js @@ -0,0 +1,274 @@ +const PDS_DEFAULT_CLIENT_ID = "lMNVp25Sd1MfqZDQ"; +const PDS_DEFAULT_DEVICE_NAME = "OpenList PDS"; + +let pdsDeviceCode = ""; +let pdsPollTimer = null; +let pdsPolling = false; +let pdsPollExpiresAt = 0; +let pdsDrives = []; + +function pdsElement(id) { + return document.getElementById(id); +} + +function pdsValue(id) { + const element = pdsElement(id); + return element ? element.value.trim() : ""; +} + +function setPdsValue(id, value) { + const element = pdsElement(id); + if (element) element.value = value || ""; +} + +function pdsMessage(data) { + if (!data) return "请求失败"; + return data.text || data.message || data.error_description || data.error || data.code || "请求失败"; +} + +function pdsConfigBase() { + return { + domain_id: pdsValue("client-uid-input"), + client_id: pdsValue("client-key-input") || PDS_DEFAULT_CLIENT_ID, + }; +} + +async function pdsPost(path, body) { + const response = await fetch(path, { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(body), + }); + const text = await response.text(); + let data = {}; + if (text) { + try { + data = JSON.parse(text); + } catch { + data = {text}; + } + } + return { + ok: response.ok, + status: response.status, + data, + }; +} + +function initPDSDefaults() { + if (!pdsValue("client-key-input")) setPdsValue("client-key-input", PDS_DEFAULT_CLIENT_ID); + if (!pdsValue("pds-device-name-input")) setPdsValue("pds-device-name-input", PDS_DEFAULT_DEVICE_NAME); + if (!pdsValue("pds-token-type-input")) setPdsValue("pds-token-type-input", "Bearer"); + if (!pdsValue("pds-root-folder-id-input")) setPdsValue("pds-root-folder-id-input", "root"); + if (!pdsValue("pds-expires-at-input")) setPdsValue("pds-expires-at-input", "0"); +} + +function setPdsStatus(text) { + setPdsValue("pds-status-output", text); +} + +function stopPdsPolling() { + if (pdsPollTimer) { + clearInterval(pdsPollTimer); + pdsPollTimer = null; + } + pdsPolling = false; +} + +function fillPdsToken(data) { + if (data.access_token) setPdsValue("access-token", data.access_token); + if (data.refresh_token) setPdsValue("refresh-token", data.refresh_token); + if (data.token_type) setPdsValue("pds-token-type-input", data.token_type); + setPdsValue("pds-expires-at-input", "0"); +} + +async function startPdsLogin() { + initPDSDefaults(); + const domainId = pdsValue("client-uid-input"); + if (!domainId) { + await showErrorMessage("获取授权链接", "请先填写 PDS Domain ID"); + return; + } + stopPdsPolling(); + setPdsStatus("正在获取授权链接..."); + const result = await pdsPost("/pds/device_authorization", { + ...pdsConfigBase(), + device_name: pdsValue("pds-device-name-input") || PDS_DEFAULT_DEVICE_NAME, + }); + if (!result.ok) { + setPdsStatus("获取授权链接失败"); + await showErrorMessage("获取授权链接", pdsMessage(result.data), result.status); + return; + } + + pdsDeviceCode = result.data.device_code || ""; + pdsPollExpiresAt = Date.now() + Number(result.data.expires_in || 0) * 1000; + const authUrl = result.data.verification_uri_complete || + (result.data.verification_uri && result.data.user_code + ? `${result.data.verification_uri}?user_code=${encodeURIComponent(result.data.user_code)}` + : result.data.verification_uri); + setPdsValue("direct-url-input", authUrl || ""); + setPdsValue("pds-user-code-output", result.data.user_code || ""); + const openButton = pdsElement("pds-open-auth-button"); + if (openButton) openButton.disabled = !authUrl; + setPdsStatus("等待授权确认"); + + const intervalSeconds = Math.max(3, Number(result.data.interval || 5)); + pdsPollTimer = setInterval(pollPdsToken, intervalSeconds * 1000); + await pollPdsToken(); +} + +async function pollPdsToken() { + if (!pdsDeviceCode || pdsPolling) return; + if (pdsPollExpiresAt > 0 && Date.now() > pdsPollExpiresAt) { + stopPdsPolling(); + setPdsStatus("授权码已过期,请重新获取"); + return; + } + pdsPolling = true; + try { + const result = await pdsPost("/pds/device_token", { + ...pdsConfigBase(), + device_code: pdsDeviceCode, + }); + if (result.status === 202 || result.data.status === "pending") { + setPdsStatus("等待授权确认..."); + return; + } + if (!result.ok) { + stopPdsPolling(); + setPdsStatus("授权失败"); + await showErrorMessage("授权", pdsMessage(result.data), result.status); + return; + } + fillPdsToken(result.data); + stopPdsPolling(); + setPdsStatus("授权成功"); + await Swal.fire({ + icon: "success", + title: "授权成功", + showConfirmButton: true, + timer: 1000, + }); + await loadPdsDrives(); + } finally { + pdsPolling = false; + } +} + +function openPdsAuthURL() { + const url = pdsValue("direct-url-input"); + if (url) window.open(url, "_blank", "noopener,noreferrer"); +} + +async function refreshPdsToken() { + initPDSDefaults(); + const refreshToken = pdsValue("refresh-token"); + if (!refreshToken) { + await showErrorMessage("刷新 Token", "请先填写 Refresh Token"); + return; + } + setPdsStatus("正在刷新 Token..."); + const result = await pdsPost("/pds/refresh", { + ...pdsConfigBase(), + refresh_token: refreshToken, + }); + if (!result.ok) { + setPdsStatus("刷新 Token 失败"); + await showErrorMessage("刷新 Token", pdsMessage(result.data), result.status); + return; + } + fillPdsToken(result.data); + setPdsStatus("Token 已刷新"); + await Swal.fire({ + icon: "success", + title: "刷新 Token 成功", + showConfirmButton: true, + timer: 1000, + }); +} + +function formatPdsSize(value) { + const size = Number(value || 0); + if (!size) return ""; + const units = ["B", "KB", "MB", "GB", "TB", "PB"]; + let index = 0; + let current = size; + while (current >= 1024 && index < units.length - 1) { + current /= 1024; + index += 1; + } + return `${current.toFixed(index === 0 ? 0 : 2)} ${units[index]}`; +} + +function updateSelectedDrive() { + setPdsValue("pds-drive-id-input", pdsValue("pds-drive-select")); +} + +function fillPdsDrives(drives) { + pdsDrives = Array.isArray(drives) ? drives : []; + const select = pdsElement("pds-drive-select"); + if (!select) return; + select.innerHTML = ""; + for (const drive of pdsDrives) { + const option = document.createElement("option"); + option.value = drive.drive_id || ""; + const ownerType = drive.owner_type ? ` / ${drive.owner_type}` : ""; + const sizeText = drive.total_size ? ` / ${formatPdsSize(drive.used_size)} / ${formatPdsSize(drive.total_size)}` : ""; + option.textContent = `${drive.drive_name || drive.drive_id}${ownerType}${sizeText}`; + select.appendChild(option); + } + updateSelectedDrive(); +} + +async function loadPdsDrives() { + initPDSDefaults(); + const accessToken = pdsValue("access-token"); + if (!accessToken) { + await showErrorMessage("列出 Drive", "请先获取或填写 Access Token"); + return; + } + setPdsStatus("正在列出 Drive..."); + const result = await pdsPost("/pds/drives", { + ...pdsConfigBase(), + access_token: accessToken, + token_type: pdsValue("pds-token-type-input") || "Bearer", + }); + if (!result.ok) { + setPdsStatus("列出 Drive 失败"); + await showErrorMessage("列出 Drive", pdsMessage(result.data), result.status); + return; + } + fillPdsDrives(result.data.drives); + setPdsStatus(`已找到 ${pdsDrives.length} 个 Drive`); + if (pdsDrives.length === 0) { + await Swal.fire({ + icon: "warning", + title: "未找到 Drive", + showConfirmButton: true, + }); + } +} + +function buildPDSConfig() { + initPDSDefaults(); + const config = { + root_folder_id: pdsValue("pds-root-folder-id-input") || "root", + domain_id: pdsValue("client-uid-input"), + drive_id: pdsValue("pds-drive-id-input"), + client_id: pdsValue("client-key-input") || PDS_DEFAULT_CLIENT_ID, + access_token: pdsValue("access-token"), + refresh_token: pdsValue("refresh-token"), + token_type: pdsValue("pds-token-type-input") || "Bearer", + expires_at: 0, + }; + setPdsValue("pds-config-output", JSON.stringify(config, null, 2)); +} + +function initPDSPage() { + const driveSelect = pdsElement("pds-drive-select"); + if (driveSelect) driveSelect.addEventListener("change", updateSelectedDrive); + if (pdsValue("driver-txt-input") === "pds_go") initPDSDefaults(); +} + +document.addEventListener("DOMContentLoaded", initPDSPage); diff --git a/src/driver/pds.ts b/src/driver/pds.ts new file mode 100644 index 0000000..0318f95 --- /dev/null +++ b/src/driver/pds.ts @@ -0,0 +1,230 @@ +import {Context} from "hono"; +import {Requests} from "../shares/request"; + +const DEFAULT_CLIENT_ID = "lMNVp25Sd1MfqZDQ"; +const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"; + +type JsonMap = Record; + +function isRecord(value: unknown): value is JsonMap { + return typeof value === "object" && value !== null; +} + +async function getJsonBody(c: Context): Promise { + try { + const body = await c.req.json(); + return isRecord(body) ? body : {}; + } catch { + return {}; + } +} + +function getString(body: JsonMap, key: string, fallback = ""): string { + const value = body[key]; + return typeof value === "string" ? value.trim() : fallback; +} + +function normalizeClientID(clientID: string): string { + return clientID || DEFAULT_CLIENT_ID; +} + +function domainIDError(domainID: string): string { + if (!domainID) return "domain_id 不能为空"; + if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/.test(domainID)) + return "domain_id 格式不正确"; + return ""; +} + +function apiEndpoint(domainID: string): string { + return `https://${domainID}.api.aliyunfile.com`; +} + +function authEndpoint(domainID: string): string { + return `https://${domainID}.auth.aliyunfile.com`; +} + +function upstreamMessage(data: JsonMap): string { + return String( + data.message || + data.error_description || + data.error || + data.code || + data.text || + "PDS 请求失败" + ); +} + +function isPendingAuthorization(data: JsonMap): boolean { + const message = upstreamMessage(data).toLowerCase(); + return message.includes("authorizationpending") || + message.includes("authorization_pending") || + message.includes("authorization is pending") || + message.includes("authorization pending"); +} + +function toForm(params: JsonMap): Record { + return Object.fromEntries( + Object.entries(params) + .filter(([, value]) => value !== undefined && value !== null) + .map(([key, value]) => [key, String(value)]) + ); +} + +async function postForm(c: Context, url: string, params: JsonMap): Promise { + return await Requests(c, toForm(params), url, "POST", false, { + "Content-Type": "application/x-www-form-urlencoded", + }); +} + +async function postJson(c: Context, domainID: string, path: string, tokenType: string, + accessToken: string, params: JsonMap): Promise { + return await Requests(c, params as Record, `${apiEndpoint(domainID)}${path}`, + "POST", false, { + "Authorization": `${tokenType || "Bearer"} ${accessToken}`, + "Content-Type": "application/json", + }); +} + +function pickItems(data: JsonMap): JsonMap[] { + const items = data.items; + if (!Array.isArray(items)) return []; + return items.filter(isRecord); +} + +function appendDrive(map: Map, drive: JsonMap, ownerType = "") { + const driveID = getString(drive, "drive_id"); + if (!driveID || map.has(driveID)) return; + map.set(driveID, { + drive_id: driveID, + drive_name: getString(drive, "drive_name") || getString(drive, "name") || driveID, + owner_type: getString(drive, "owner_type", ownerType), + total_size: drive.total_size, + used_size: drive.used_size, + }); +} + +async function listGroupDrives(c: Context, domainID: string, tokenType: string, + accessToken: string): Promise { + const paths = [ + "/v2/drive/list_my_group_drives", + "/v2/drive/list_my_group_drive", + "/v2/drive/list_all_my_group_drives", + "/v2/group/list_my_group_drives", + ]; + for (const path of paths) { + const data = await postJson(c, domainID, path, tokenType, accessToken, { + limit: 100, + marker: "", + }); + if (Array.isArray(data.items) || isRecord(data.root_group_drive)) return data; + } + return {}; +} + +export async function page(c: Context) { + return c.redirect("/"); +} + +export async function deviceAuthorization(c: Context) { + const body = await getJsonBody(c); + const domainID = getString(body, "domain_id"); + const clientID = normalizeClientID(getString(body, "client_id")); + const deviceName = getString(body, "device_name", "OpenList PDS"); + const error = domainIDError(domainID); + if (error) return c.json({text: error}, 400); + + const result = await postForm(c, `${apiEndpoint(domainID)}/v2/oauth/device_authorization`, { + client_id: clientID, + device_name: deviceName, + device_info: deviceName, + login_type: "default", + }); + if (!result.device_code) return c.json({text: upstreamMessage(result), raw: result}, 500); + return c.json({ + ...result, + client_id: clientID, + }, 200); +} + +export async function deviceToken(c: Context) { + const body = await getJsonBody(c); + const domainID = getString(body, "domain_id"); + const clientID = normalizeClientID(getString(body, "client_id")); + const deviceCode = getString(body, "device_code"); + const error = domainIDError(domainID); + if (error) return c.json({text: error}, 400); + if (!deviceCode) return c.json({text: "device_code 不能为空"}, 400); + + const result = await postForm(c, `${apiEndpoint(domainID)}/v2/oauth/token`, { + grant_type: DEVICE_GRANT_TYPE, + device_code: deviceCode, + client_id: clientID, + }); + if (result.access_token) { + return c.json({ + ...result, + expires_at: 0, + }, 200); + } + if (isPendingAuthorization(result)) + return c.json({status: "pending", text: "等待授权确认", raw: result}, 202); + return c.json({text: upstreamMessage(result), raw: result}, 500); +} + +export async function refreshToken(c: Context) { + const body = await getJsonBody(c); + const domainID = getString(body, "domain_id"); + const clientID = normalizeClientID(getString(body, "client_id")); + const refreshToken = getString(body, "refresh_token"); + const error = domainIDError(domainID); + if (error) return c.json({text: error}, 400); + if (!refreshToken) return c.json({text: "refresh_token 不能为空"}, 400); + + const result = await postForm(c, `${authEndpoint(domainID)}/v2/oauth/token`, { + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: clientID, + }); + if (!result.access_token) return c.json({text: upstreamMessage(result), raw: result}, 500); + return c.json({ + ...result, + refresh_token: result.refresh_token || refreshToken, + expires_at: 0, + }, 200); +} + +export async function drives(c: Context) { + const body = await getJsonBody(c); + const domainID = getString(body, "domain_id"); + const accessToken = getString(body, "access_token"); + const tokenType = getString(body, "token_type", "Bearer"); + const error = domainIDError(domainID); + if (error) return c.json({text: error}, 400); + if (!accessToken) return c.json({text: "access_token 不能为空"}, 400); + + const domain = await postJson(c, domainID, "/v2/domain/get", tokenType, accessToken, { + fields: "*", + }); + if (domain.code || domain.error || domain.message) + return c.json({text: upstreamMessage(domain), raw: domain}, 500); + + const mine = await postJson(c, domainID, "/v2/drive/list_my_drives", tokenType, accessToken, { + limit: 100, + marker: "", + }); + if (!Array.isArray(mine.items)) + return c.json({text: upstreamMessage(mine), raw: mine}, 500); + + const group = await listGroupDrives(c, domainID, tokenType, accessToken); + const driveMap = new Map(); + for (const item of pickItems(mine)) appendDrive(driveMap, item, "user"); + if (isRecord(group.root_group_drive)) appendDrive(driveMap, group.root_group_drive, "group"); + for (const item of pickItems(group)) appendDrive(driveMap, item, "group"); + + return c.json({ + domain, + drives: Array.from(driveMap.values()), + my_drives: mine, + group_drives: group, + }, 200); +} diff --git a/src/index.ts b/src/index.ts index fd27884..7d9db82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import * as goapi from './driver/googleui_oa'; import * as yanui from './driver/yandexui_oa'; import * as drops from './driver/dropboxs_oa'; import * as quark from './driver/quarkpan_oa'; +import * as pds from './driver/pds'; export type Bindings = { // 基本配置 ================================ @@ -34,6 +35,26 @@ app.get('/app', async (c) => { return c.redirect('/index.html'); }) +app.get('/pds', async (c: Context) => { + return pds.page(c); +}) + +app.post('/pds/device_authorization', async (c: Context) => { + return pds.deviceAuthorization(c); +}) + +app.post('/pds/device_token', async (c: Context) => { + return pds.deviceToken(c); +}) + +app.post('/pds/refresh', async (c: Context) => { + return pds.refreshToken(c); +}) + +app.post('/pds/drives', async (c: Context) => { + return pds.drives(c); +}) + // 登录申请 ############################################################################## app.get('/dropboxs/requests', async (c) => { return drops.getLogin(c);