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 @@
@@ -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);