feat: add Feishu export credential config

This commit is contained in:
wxs 2026-05-08 16:27:18 +08:00
parent 07ff72eafa
commit 10f4ef45d3
2 changed files with 188 additions and 0 deletions

View File

@ -826,6 +826,24 @@
let cachedFields = [];
return {
getFeishuCredentials() {
return resolveFeishuCredentials(settings);
},
saveFeishuCredentials(credentials) {
const appId = String((credentials && credentials.appId) || "").trim();
const appSecret = String((credentials && credentials.appSecret) || "").trim();
if (!appId || !appSecret) {
throw new Error("请填写飞书应用 app_id 和 app_secret。");
}
saveLocal(FEISHU_APP_ID_STORAGE_KEY, appId);
saveLocal(FEISHU_APP_SECRET_STORAGE_KEY, appSecret);
return {
appId,
appSecret,
};
},
async preview(rawInput, onProgress) {
const ids = await parseCreatorInputs(rawInput);
if (!ids.length) {
@ -1164,6 +1182,50 @@
color: #2e211a;
}
.xhs-export-config {
display: grid;
gap: 8px;
}
.xhs-export-config-panel {
display: none;
gap: 10px;
padding: 12px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(123, 83, 52, 0.12);
}
.xhs-export-config.is-open .xhs-export-config-panel {
display: grid;
}
.xhs-export-config-grid {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
.xhs-export-config-field {
display: grid;
gap: 6px;
}
.xhs-export-config-label {
font-size: 12px;
font-weight: 800;
color: #5e412f;
}
.xhs-export-config-input {
border: 1px solid rgba(141, 88, 51, 0.2);
border-radius: 12px;
padding: 9px 10px;
font-size: 12px;
background: rgba(255, 255, 255, 0.85);
color: #2e211a;
}
.xhs-export-actions,
.xhs-export-mini-actions {
display: flex;
@ -1577,6 +1639,21 @@
</div>
<div class="xhs-export-body">
<textarea class="xhs-export-input" placeholder="每行一个达人主页链接或达人 ID支持批量。"></textarea>
<div class="xhs-export-config">
<button class="xhs-export-select-trigger" type="button" data-action="toggle-feishu-config" aria-expanded="false">飞书导出配置</button>
<div class="xhs-export-config-panel">
<div class="xhs-export-config-grid">
<label class="xhs-export-config-field">
<span class="xhs-export-config-label">飞书 app_id</span>
<input class="xhs-export-config-input" data-config="feishu-app-id" type="text" autocomplete="off" placeholder="cli_xxx">
</label>
<label class="xhs-export-config-field">
<span class="xhs-export-config-label">飞书 app_secret</span>
<input class="xhs-export-config-input" data-config="feishu-app-secret" type="password" autocomplete="off" placeholder="请输入 app_secret">
</label>
</div>
</div>
</div>
<div class="xhs-export-status"></div>
<div class="xhs-export-progress is-hidden">
<div class="xhs-export-progress-meta">
@ -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);

View File

@ -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 = [];