@@ -1622,6 +1699,10 @@
toggle,
panel,
input: panel.querySelector(".xhs-export-input"),
+ feishuConfig: panel.querySelector(".xhs-export-config"),
+ feishuConfigTrigger: panel.querySelector('[data-action="toggle-feishu-config"]'),
+ feishuAppIdInput: panel.querySelector('[data-config="feishu-app-id"]'),
+ feishuAppSecretInput: panel.querySelector('[data-config="feishu-app-secret"]'),
localExportButton: panel.querySelector('[data-action="export-local"]'),
feishuExportButton: panel.querySelector('[data-action="export-feishu"]'),
status: panel.querySelector(".xhs-export-status"),
@@ -1750,6 +1831,26 @@
.filter(Boolean);
}
+ function loadFeishuConfigForm(controller, refs) {
+ if (!controller || !refs) {
+ return;
+ }
+ const credentials = controller.getFeishuCredentials();
+ if (refs.feishuAppIdInput) {
+ refs.feishuAppIdInput.value = credentials.appId || "";
+ }
+ if (refs.feishuAppSecretInput) {
+ refs.feishuAppSecretInput.value = credentials.appSecret || "";
+ }
+ }
+
+ function saveFeishuConfigForm(controller, refs) {
+ return controller.saveFeishuCredentials({
+ appId: refs.feishuAppIdInput ? refs.feishuAppIdInput.value : "",
+ appSecret: refs.feishuAppSecretInput ? refs.feishuAppSecretInput.value : "",
+ });
+ }
+
function setStatus(node, message, isError) {
node.textContent = message;
node.classList.toggle("is-error", Boolean(isError));
@@ -1871,6 +1972,7 @@
const defaultSelectedFields = SELECTABLE_FIELD_PATHS.slice();
refs.input.value = typeof persistedInput === "string" ? persistedInput : "";
+ loadFeishuConfigForm(controller, refs);
renderFields(
refs.fields,
staticFields,
@@ -1884,6 +1986,14 @@
refs.panel.classList.toggle("is-open");
});
+ if (refs.feishuConfigTrigger && refs.feishuConfig) {
+ refs.feishuConfigTrigger.addEventListener("click", () => {
+ const open = !refs.feishuConfig.classList.contains("is-open");
+ refs.feishuConfig.classList.toggle("is-open", open);
+ refs.feishuConfigTrigger.setAttribute("aria-expanded", open ? "true" : "false");
+ });
+ }
+
refs.localExportButton.addEventListener("click", async () => {
try {
const checkedFields = getCheckedFields(refs.fields);
@@ -1934,6 +2044,7 @@
setExportButtonsDisabled(refs, true);
hideProgress(refs);
setProgress(refs, 0, "准备导出到飞书...", false);
+ saveFeishuConfigForm(controller, refs);
setStatus(refs.status, "正在读取达人数据,请稍候...", false);
await prepareExportData(controller, refs, checkedFields, 40);
diff --git a/pugongying/xhs-pgy-export.user.test.js b/pugongying/xhs-pgy-export.user.test.js
index b4523ee..e917fbe 100644
--- a/pugongying/xhs-pgy-export.user.test.js
+++ b/pugongying/xhs-pgy-export.user.test.js
@@ -218,6 +218,83 @@ test("controller reads Feishu credentials from localStorage instead of bundled s
}
});
+test("controller saves Feishu credentials for later exports", async () => {
+ const previousLocalStorage = global.localStorage;
+ const previousGm = global.GM_xmlhttpRequest;
+ const storage = new Map();
+ const sentAuthBodies = [];
+
+ global.GM_xmlhttpRequest = undefined;
+ global.localStorage = {
+ getItem(key) {
+ return storage.has(key) ? storage.get(key) : null;
+ },
+ setItem(key, value) {
+ storage.set(key, value);
+ },
+ };
+
+ async function fetchImpl(url, options) {
+ const body = options && options.body ? JSON.parse(options.body) : null;
+ if (String(url).startsWith(api.API_BASE)) {
+ return okJson({
+ code: 0,
+ data: {
+ id: "60379f3c000000000101e53f",
+ userId: "60379f3c000000000101e53f",
+ name: "达人 A",
+ },
+ });
+ }
+ if (String(url).includes("/auth/v3/tenant_access_token/internal")) {
+ sentAuthBodies.push(body);
+ return okJson({ code: 0, tenant_access_token: "tenant-token" });
+ }
+ if (String(url).includes("/sheets/v3/spreadsheets")) {
+ return okJson({
+ code: 0,
+ data: {
+ spreadsheet: {
+ spreadsheet_token: "spreadsheet-token",
+ },
+ },
+ });
+ }
+ if (String(url).includes("/metainfo")) {
+ return okJson({ code: 0, data: { sheets: [{ sheetId: "sheet-a" }] } });
+ }
+ if (String(url).includes("/values_batch_update")) {
+ return okJson({ code: 0, data: {} });
+ }
+ throw new Error(`unexpected url: ${url}`);
+ }
+
+ try {
+ const controller = api.createExportController({ fetchImpl });
+ controller.saveFeishuCredentials({
+ appId: " cli_saved ",
+ appSecret: " secret_saved ",
+ });
+ await controller.preview("60379f3c000000000101e53f");
+ await controller.exportFeishuSpreadsheet(["userId", "name"]);
+
+ assert.equal(storage.get("xhs-pgy-export:feishu-app-id"), JSON.stringify("cli_saved"));
+ assert.equal(
+ storage.get("xhs-pgy-export:feishu-app-secret"),
+ JSON.stringify("secret_saved"),
+ );
+ assert.deepEqual(sentAuthBodies, [
+ {
+ app_id: "cli_saved",
+ app_secret: "secret_saved",
+ },
+ ]);
+ } finally {
+ global.localStorage = previousLocalStorage;
+ global.GM_xmlhttpRequest = previousGm;
+ }
+});
+
test("controller uses GM_xmlhttpRequest for Feishu even when page fetch is available", async () => {
const gmUrls = [];
const pageFetchUrls = [];