fix(feishu): add one-click card callback repair

This commit is contained in:
xmanrui 2026-08-21 13:43:02 +08:00
parent 0e62c69382
commit c1a3b0cf97
24 changed files with 3601 additions and 248 deletions

View file

@ -587,6 +587,27 @@ var EN = Object.freeze({
"\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u65B0\u673A\u5668\u4EBA\u7684\u957F\u8FDE\u63A5\u5C31\u7EEA": "Keep this page open until the bot connection is ready",
"\u5728\u98DE\u4E66\u4E2D\u6253\u5F00": "Open in Feishu",
"\u53D6\u6D88\u6DFB\u52A0": "Cancel",
"\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u4FEE\u590D\u5361\u7247\u6309\u94AE": "Scan with Feishu to repair card buttons",
"\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u53EA\u589E\u91CF\u8865\u5145\u5361\u7247\u6309\u94AE\u56DE\u8C03\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u540E\u6B64\u673A\u5668\u4EBA\u4F1A\u77ED\u6682\u91CD\u8FDE\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002": "Scanning updates the existing Feishu app with only the card-button callback. It does not create a new app. This bot reconnects briefly after confirmation; other bots are not affected.",
"\u6838\u5BF9\u73B0\u6709\u5E94\u7528\u540D\u79F0\uFF0C\u5E76\u786E\u8BA4\u53EA\u65B0\u589E\u5361\u7247\u56DE\u8C03": "Review the existing app name and confirm that only the card callback is added",
"\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u5361\u7247\u6309\u94AE\u4FEE\u590D\u5B8C\u6210": "Keep this page open until card-button repair finishes",
"\u53D6\u6D88\u4FEE\u590D": "Cancel repair",
"\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u5B8C\u6210\u5361\u7247\u6309\u94AE\u4FEE\u590D": "Confirmed. Finishing card-button repair",
"\u6B63\u5728\u51C6\u5907\u4FEE\u590D\u4E8C\u7EF4\u7801": "Preparing the repair QR code",
"\u914D\u7F6E\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u9A8C\u8BC1\u5361\u7247\u6309\u94AE\u56DE\u8C03\u5E76\u91CD\u8FDE\u6B64\u673A\u5668\u4EBA\uFF1B\u6B64\u9636\u6BB5\u65E0\u6CD5\u53D6\u6D88\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002": "The update was submitted. Verifying the card callback and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.",
"\u6B63\u5728\u4E3A\u73B0\u6709\u98DE\u4E66\u5E94\u7528\u7533\u8BF7\u4E00\u6B21\u6027\u66F4\u65B0\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002": "Requesting a one-time update QR code for the existing Feishu app\u2026",
"\u5361\u7247\u6309\u94AE\u6CA1\u6709\u4FEE\u590D\u5B8C\u6210": "Card-button repair did not finish",
"\u4FEE\u590D\u5361\u7247\u6309\u94AE": "Repair card buttons",
"\u7B49\u5F85\u626B\u7801\u2026": "Waiting for scan\u2026",
"\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u5361\u7247\u4FEE\u590D\u4E8C\u7EF4\u7801": "Feishu returned a repair QR code for a different bot",
"\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u7684\u4FEE\u590D\u4FE1\u606F\u7F3A\u5C11 botId": "Feishu repair status is missing the bot ID",
"\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u6CE8\u518C\u8FDB\u5EA6": "Feishu returned registration progress for a different operation",
"\u6B64\u673A\u5668\u4EBA": "this bot",
'${botName ?? "\u673A\u5668\u4EBA"}\u7684\u4FEE\u590D\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u3002': 'Repair QR code generated for ${botName ?? "bot"}. Scan it with Feishu.',
"${targetBot.bot.name}\u5DF2\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u5728\u98DE\u4E66\u4E2D\u5F00\u59CB\u804A\u5929\u3002": "${targetBot.bot.name} is connected and ready to chat in Feishu.",
"\u5DF2\u53D6\u6D88\u5361\u7247\u6309\u94AE\u4FEE\u590D\u3002": "Card-button repair was cancelled.",
"\u5361\u7247\u6309\u94AE\u5DF2\u66F4\u65B0\uFF0C\u4F46\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u673A\u5668\u4EBA\u8FDE\u63A5\u72B6\u6001": "The card callback was updated, but the bot connection could not be confirmed yet",
"\u98DE\u4E66\u5361\u7247\u6309\u94AE\u4FEE\u590D\u5931\u8D25": "Could not repair the Feishu card buttons",
"\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u8FDE\u63A5\u65B0\u673A\u5668\u4EBA": "Confirmed. Connecting the new bot",
"\u6B63\u5728\u5B89\u5168\u4FDD\u5B58\u51ED\u636E\u5E76\u68C0\u67E5\u65B0\u673A\u5668\u4EBA\u7684\u6D88\u606F\u901A\u9053\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002": "Saving credentials and checking the new bot connection. Existing bots will not be interrupted.",
"\u6B63\u5728\u5411\u98DE\u4E66\u7533\u8BF7\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002": "Requesting a one-time authorization QR code from Feishu\u2026",
@ -865,6 +886,16 @@ function translateDynamic(text5) {
if (match) return `Remove \u201C${match[1]}\u201D from DeepSeek Harness?`;
match = /^从 DeepSeek Harness 移除(.+)$/.exec(text5);
if (match) return `Remove ${match[1]} from DeepSeek Harness`;
match = /^用于修复(.+)卡片按钮的一次性授权二维码$/.exec(text5);
if (match) return `One-time QR code for repairing card buttons for ${match[1]}`;
match = /^正在修复「(.+)」$/.exec(text5);
if (match) return `Repairing \u201C${match[1]}\u201D`;
match = /^修复(.+)的卡片按钮$/.exec(text5);
if (match) return `Repair card buttons for ${match[1]}`;
match = /^(.+)的修复二维码已生成,请使用飞书扫码。$/.exec(text5);
if (match) return `Repair QR code generated for ${match[1]}. Scan it with Feishu.`;
match = /^(.+)的卡片按钮已修复。$/.exec(text5);
if (match) return `Card buttons repaired for ${match[1]}.`;
match = /^(检查连接|重试连接)(.+)$/.exec(text5);
if (match) return `${localizeText(match[1])} ${match[2]}`;
match = /^移除(.+)$/.exec(text5);
@ -3039,6 +3070,7 @@ var FEISHU_RPC_CHANNEL = "/feishu";
var FEISHU_ENDPOINTS = Object.freeze({
status: "connection.status",
beginProvisioning: "provision.begin",
beginCallbackRepair: "bot.callback-repair.begin",
pollProvisioning: "provision.poll",
cancelProvisioning: "provision.cancel",
bindCredentials: "bot.bind-credentials",
@ -3050,6 +3082,10 @@ var FEISHU_ENDPOINTS = Object.freeze({
testConnection: "connection.test",
disconnect: "connection.disconnect"
});
var FEISHU_REGISTRATION_OPERATIONS = Object.freeze({
PROVISION: "provision",
CALLBACK_REPAIR: "callback_repair"
});
var CONNECTION_STATES = /* @__PURE__ */ new Set([
"disconnected",
"offline",
@ -3084,6 +3120,9 @@ function optionalTimestamp(value) {
function clamp2(value, min, max, fallback) {
return typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
}
function normalizeRegistrationOperation(value) {
return value === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR ? FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR : FEISHU_REGISTRATION_OPERATIONS.PROVISION;
}
function unwrapRpcResult3(result) {
if (!isRecord3(result) || typeof result.ok !== "boolean") {
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u54CD\u5E94");
@ -3102,15 +3141,24 @@ function normalizeProvisioning2(value, now = Date.now()) {
const attemptId = optionalString2(source.attemptId) ?? optionalString2(source.provisioningId);
const verificationUrl = optionalString2(source.verificationUrl);
const qrCodeDataUrl = optionalString2(source.qrCodeDataUrl);
if (!attemptId || !verificationUrl && !qrCodeDataUrl) {
const submitted = source.submitted === true;
if (!attemptId || !verificationUrl && !qrCodeDataUrl && !submitted) {
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u7684\u4E8C\u7EF4\u7801\u4FE1\u606F\u4E0D\u5B8C\u6574");
}
const explicitExpiry = optionalTimestamp(source.expiresAt);
const expireIn = clamp2(source.expireIn, 1, 60 * 60, 5 * 60);
const operation = normalizeRegistrationOperation(source.operation);
const botId = optionalString2(source.botId);
if (operation === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR && !botId) {
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u7684\u4FEE\u590D\u4FE1\u606F\u7F3A\u5C11 botId");
}
return {
attemptId,
operation,
botId,
verificationUrl,
qrCodeDataUrl,
submitted,
expiresAt: explicitExpiry ?? now + expireIn * 1e3,
pollIntervalMs: clamp2(source.pollIntervalMs, 800, 1e4, 1800)
};
@ -3228,6 +3276,7 @@ function normalizePollResult(value) {
if (!status) throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u672A\u77E5\u7684\u521B\u5EFA\u72B6\u6001");
const normalized = {
status,
operation: normalizeRegistrationOperation(value.operation),
botId: optionalString2(value.botId),
message: optionalString2(value.error?.message) ?? optionalString2(value.message),
connection: void 0,
@ -3746,6 +3795,8 @@ var CSS3 = String.raw`
.bxf-healthSummary[data-error="true"] { color: var(--bxf-error); }
.bxf-botActions { flex: none; flex-wrap: nowrap; gap: 8px; margin-top: 0; justify-content: flex-end; }
.bxf-botActions .bxf-button { flex: none; white-space: nowrap; }
.bxf-botActions .bxf-repairButton { color: var(--bxf-accent); border-color: color-mix(in srgb, var(--bxf-accent) 35%, var(--dsw-alias-border-l2, #dee0e3)); }
.bxf-botActions .bxf-repairButton:hover:not(:disabled) { background: color-mix(in srgb, var(--bxf-accent) 7%, transparent); }
.bxf-confirm {
border-top: 1px solid var(--dsw-alias-border-l2, #dee0e3);
@ -3837,6 +3888,10 @@ function installFeishuStyles() {
}
// plugin-src/client/channels/feishu/index.js
var CALLBACK_REPAIR_OPERATION = FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
function isCallbackRepair(value) {
return value?.operation === CALLBACK_REPAIR_OPERATION;
}
function SvgIcon({ children, size = 18, className, viewBox = "0 0 24 24" }) {
return h2("svg", {
width: size,
@ -3996,7 +4051,12 @@ function safeVerificationHref(value) {
if (!value) return void 0;
try {
const url = new URL(value);
return url.protocol === "https:" ? url.toString() : void 0;
return url.protocol === "https:" && [
"accounts.feishu.cn",
"accounts.larksuite.com",
"open.feishu.cn",
"open.larksuite.com"
].includes(url.hostname) && !url.port && !url.username && !url.password ? url.toString() : void 0;
} catch {
return void 0;
}
@ -4012,6 +4072,8 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
const remaining = Math.max(0, provision.expiresAt - now);
const expired = provision.expired === true || remaining === 0;
const progress = Math.min(1, remaining / Math.max(1, provision.durationMs ?? remaining));
const repairing = isCallbackRepair(provision);
const botName = provision.botName ?? "\u6B64\u673A\u5668\u4EBA";
React10.useEffect(() => setImageFailed(false), [qrSource]);
return h2(
"div",
@ -4027,7 +4089,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
{ className: "bxf-qrFrame dim-qrFrame" },
qrSource && !imageFailed ? h2("img", {
src: qrSource,
alt: "\u7528\u4E8E\u65B0\u589E DeepSeek Harness \u98DE\u4E66\u673A\u5668\u4EBA\u7684\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801",
alt: repairing ? `\u7528\u4E8E\u4FEE\u590D${botName}\u5361\u7247\u6309\u94AE\u7684\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801` : "\u7528\u4E8E\u65B0\u589E DeepSeek Harness \u98DE\u4E66\u673A\u5668\u4EBA\u7684\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801",
onError: () => setImageFailed(true)
}) : h2(
"div",
@ -4066,16 +4128,16 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
"div",
{ className: "bxf-stateLabel dim-stateLabel" },
h2("span", { className: "bxf-dot dim-stateDot", "data-tone": "warning" }),
h2("span", null, "\u6B63\u5728\u6DFB\u52A0\u65B0\u673A\u5668\u4EBA")
h2("span", null, repairing ? `\u6B63\u5728\u4FEE\u590D\u300C${botName}\u300D` : "\u6B63\u5728\u6DFB\u52A0\u65B0\u673A\u5668\u4EBA")
),
h2("h3", null, expired ? "\u5237\u65B0\u4E8C\u7EF4\u7801\u540E\u7EE7\u7EED" : "\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u521B\u5EFA\u673A\u5668\u4EBA"),
h2("p", null, "\u626B\u7801\u53EA\u4F1A\u65B0\u589E\u4E00\u4E2A\u673A\u5668\u4EBA\uFF0C\u5DF2\u63A5\u5165\u7684\u673A\u5668\u4EBA\u4F1A\u7EE7\u7EED\u6B63\u5E38\u6536\u53D1\u6D88\u606F\u3002"),
h2("h3", null, expired ? "\u5237\u65B0\u4E8C\u7EF4\u7801\u540E\u7EE7\u7EED" : repairing ? "\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u4FEE\u590D\u5361\u7247\u6309\u94AE" : "\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u521B\u5EFA\u673A\u5668\u4EBA"),
h2("p", null, repairing ? "\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u53EA\u589E\u91CF\u8865\u5145\u5361\u7247\u6309\u94AE\u56DE\u8C03\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u540E\u6B64\u673A\u5668\u4EBA\u4F1A\u77ED\u6682\u91CD\u8FDE\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002" : "\u626B\u7801\u53EA\u4F1A\u65B0\u589E\u4E00\u4E2A\u673A\u5668\u4EBA\uFF0C\u5DF2\u63A5\u5165\u7684\u673A\u5668\u4EBA\u4F1A\u7EE7\u7EED\u6B63\u5E38\u6536\u53D1\u6D88\u606F\u3002"),
h2(
"ol",
{ className: "bxf-steps dim-steps" },
h2("li", null, "\u6253\u5F00\u98DE\u4E66\u79FB\u52A8\u7AEF\uFF0C\u4F7F\u7528\u626B\u4E00\u626B\u8BFB\u53D6\u4E8C\u7EF4\u7801"),
h2("li", null, "\u6838\u5BF9\u5E94\u7528\u540D\u79F0\u4E0E\u6743\u9650\u8303\u56F4\uFF0C\u5E76\u786E\u8BA4\u521B\u5EFA"),
h2("li", null, "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u65B0\u673A\u5668\u4EBA\u7684\u957F\u8FDE\u63A5\u5C31\u7EEA")
h2("li", null, repairing ? "\u6838\u5BF9\u73B0\u6709\u5E94\u7528\u540D\u79F0\uFF0C\u5E76\u786E\u8BA4\u53EA\u65B0\u589E\u5361\u7247\u56DE\u8C03" : "\u6838\u5BF9\u5E94\u7528\u540D\u79F0\u4E0E\u6743\u9650\u8303\u56F4\uFF0C\u5E76\u786E\u8BA4\u521B\u5EFA"),
h2("li", null, repairing ? "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u5361\u7247\u6309\u94AE\u4FEE\u590D\u5B8C\u6210" : "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u65B0\u673A\u5668\u4EBA\u7684\u957F\u8FDE\u63A5\u5C31\u7EEA")
),
h2(
"div",
@ -4092,14 +4154,15 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
rel: "noopener noreferrer"
}, h2("span", null, "\u5728\u98DE\u4E66\u4E2D\u6253\u5F00")) : null,
!expired ? h2(Button5, { onClick: onRefresh, disabled: busy }, "\u6362\u4E00\u4E2A\u4E8C\u7EF4\u7801") : null,
h2(Button5, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88\u6DFB\u52A0")
h2(Button5, { onClick: onCancel, disabled: busy }, repairing ? "\u53D6\u6D88\u4FEE\u590D" : "\u53D6\u6D88\u6DFB\u52A0")
)
)
)
);
}
function ProvisionProgress({ phase, onCancel, busy }) {
function ProvisionProgress({ phase, provision, onCancel, busy }) {
const connecting = phase === "connecting";
const repairing = isCallbackRepair(provision);
return h2(
"div",
{
@ -4107,16 +4170,17 @@ function ProvisionProgress({ phase, onCancel, busy }) {
"aria-busy": "true"
},
h2("div", { className: "dim-spinner", "aria-hidden": "true" }),
h2("h3", null, connecting ? "\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u8FDE\u63A5\u65B0\u673A\u5668\u4EBA" : "\u6B63\u5728\u51C6\u5907\u6388\u6743\u4E8C\u7EF4\u7801"),
h2("p", null, connecting ? "\u6B63\u5728\u5B89\u5168\u4FDD\u5B58\u51ED\u636E\u5E76\u68C0\u67E5\u65B0\u673A\u5668\u4EBA\u7684\u6D88\u606F\u901A\u9053\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002" : "\u6B63\u5728\u5411\u98DE\u4E66\u7533\u8BF7\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002"),
connecting ? h2(
h2("h3", null, connecting ? repairing ? "\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u5B8C\u6210\u5361\u7247\u6309\u94AE\u4FEE\u590D" : "\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u8FDE\u63A5\u65B0\u673A\u5668\u4EBA" : repairing ? "\u6B63\u5728\u51C6\u5907\u4FEE\u590D\u4E8C\u7EF4\u7801" : "\u6B63\u5728\u51C6\u5907\u6388\u6743\u4E8C\u7EF4\u7801"),
h2("p", null, connecting ? repairing ? "\u914D\u7F6E\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u9A8C\u8BC1\u5361\u7247\u6309\u94AE\u56DE\u8C03\u5E76\u91CD\u8FDE\u6B64\u673A\u5668\u4EBA\uFF1B\u6B64\u9636\u6BB5\u65E0\u6CD5\u53D6\u6D88\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002" : "\u6B63\u5728\u5B89\u5168\u4FDD\u5B58\u51ED\u636E\u5E76\u68C0\u67E5\u65B0\u673A\u5668\u4EBA\u7684\u6D88\u606F\u901A\u9053\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002" : repairing ? "\u6B63\u5728\u4E3A\u73B0\u6709\u98DE\u4E66\u5E94\u7528\u7533\u8BF7\u4E00\u6B21\u6027\u66F4\u65B0\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002" : "\u6B63\u5728\u5411\u98DE\u4E66\u7533\u8BF7\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002"),
connecting && onCancel ? h2(
"div",
{ className: "bxf-actions dim-viewActions", style: { justifyContent: "center" } },
h2(Button5, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88\u6DFB\u52A0")
h2(Button5, { onClick: onCancel, disabled: busy }, repairing ? "\u53D6\u6D88\u4FEE\u590D" : "\u53D6\u6D88\u6DFB\u52A0")
) : null
);
}
function ProvisionError2({ error, onRetry, onCancel, busy }) {
function ProvisionError2({ error, provision, onRetry, onCancel, busy }) {
const repairing = isCallbackRepair(provision);
return h2(
"div",
{ className: "bxf-card bxf-provisionCard dim-surfaceCard" },
@ -4126,7 +4190,7 @@ function ProvisionError2({ error, onRetry, onCancel, busy }) {
h2(
"div",
null,
h2("h3", null, "\u65B0\u673A\u5668\u4EBA\u6CA1\u6709\u6DFB\u52A0\u5B8C\u6210"),
h2("h3", null, repairing ? "\u5361\u7247\u6309\u94AE\u6CA1\u6709\u4FEE\u590D\u5B8C\u6210" : "\u65B0\u673A\u5668\u4EBA\u6CA1\u6709\u6DFB\u52A0\u5B8C\u6210"),
h2("p", null, error.message),
error.code ? h2("span", { className: "bxf-errorCode" }, error.code) : null,
h2(
@ -4211,10 +4275,12 @@ function RemoveConfirmation2({ bot, busy, onConfirm, onCancel }) {
function BotCard({
connection,
busy,
repairDisabled,
actionError,
testNotice,
removing,
onReconnect,
onRepairCallback,
onWorkspaceSave,
onRequestRemove,
onConfirmRemove,
@ -4307,6 +4373,13 @@ function BotCard({
"aria-busy": busy === "reconnect" ? "true" : void 0,
"aria-label": `${connected ? "\u68C0\u67E5\u8FDE\u63A5" : "\u91CD\u8BD5\u8FDE\u63A5"}${bot.name}`
}, busy === "reconnect" ? connected ? "\u68C0\u67E5\u4E2D\u2026" : "\u6B63\u5728\u8FDE\u63A5\u2026" : connected ? "\u68C0\u67E5\u8FDE\u63A5" : "\u91CD\u8BD5\u8FDE\u63A5"),
h2(Button5, {
className: "bxf-repairButton dim-cardAction",
onClick: onRepairCallback,
disabled: Boolean(busy) || repairDisabled,
"aria-busy": busy === "callback-repair" ? "true" : void 0,
"aria-label": `\u4FEE\u590D${bot.name}\u7684\u5361\u7247\u6309\u94AE`
}, busy === "callback-repair" ? "\u7B49\u5F85\u626B\u7801\u2026" : "\u4FEE\u590D\u5361\u7247\u6309\u94AE"),
h2(Button5, {
className: "dim-cardAction",
kind: "danger",
@ -4343,11 +4416,13 @@ function BotList(props) {
{ key: bot.botId },
h2(BotCard, {
connection: bot,
busy: props.busyByBot[bot.botId],
busy: props.busyByBot[bot.botId] ?? (isCallbackRepair(props.provisioning) && props.provisioning.botId === bot.botId ? "callback-repair" : void 0),
repairDisabled: Boolean(props.provisioning),
actionError: props.errorsByBot[bot.botId],
testNotice: props.testNoticesByBot[bot.botId],
removing: props.removeTargetId === bot.botId,
onReconnect: () => props.onReconnect(bot),
onRepairCallback: () => props.onRepairCallback(bot),
onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
onRequestRemove: () => props.onRequestRemove(bot),
onConfirmRemove: () => props.onConfirmRemove(bot),
@ -4390,11 +4465,12 @@ function mergeFeishuSnapshotState(current, snapshot, { restoreProvisioning = fal
if (snapshot.revision > 0 && current.revision > snapshot.revision) return current;
let provisioning = current.provisioning;
if (!provisioning && restoreProvisioning && snapshot.provisioning) {
const submitted = snapshot.provisioning.submitted === true;
provisioning = {
phase: snapshot.state === "connecting" ? "connecting" : "qr",
phase: submitted || snapshot.state === "connecting" ? "connecting" : "qr",
...snapshot.provisioning,
durationMs: Math.max(1, snapshot.provisioning.expiresAt - now),
expired: snapshot.provisioning.expiresAt <= now
expired: !submitted && snapshot.provisioning.expiresAt <= now
};
}
return {
@ -4509,7 +4585,15 @@ function FeishuSettingsTab({ rpcCall }) {
node.focus({ preventScroll: true });
setFocusBotId(null);
}, [focusBotId, model.bots]);
const startProvisioning = React10.useCallback(async ({ replace = false } = {}) => {
const startProvisioning = React10.useCallback(async ({
replace = false,
operation = FEISHU_REGISTRATION_OPERATIONS.PROVISION,
bot
} = {}) => {
const repairing = operation === CALLBACK_REPAIR_OPERATION;
const botId = repairing ? bot?.botId ?? model.provisioning?.botId : void 0;
const botName = repairing ? bot?.bot?.name ?? model.provisioning?.botName : void 0;
if (repairing && !botId) return;
setCredentialOpen(false);
setCredentialError(null);
setProvisionBusy(true);
@ -4518,16 +4602,27 @@ function FeishuSettingsTab({ rpcCall }) {
setModel((current) => ({
...current,
phase: current.phase === "loading" ? "ready" : current.phase,
provisioning: { phase: "creating" }
provisioning: {
phase: "creating",
operation,
...botId ? { botId } : {},
...botName ? { botName } : {}
}
}));
try {
if (replace && previousAttemptId) {
await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId: previousAttemptId });
try {
await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId: previousAttemptId });
} catch {
}
}
const provision2 = normalizeProvisioning2(await invoke(
FEISHU_ENDPOINTS.beginProvisioning,
{ locale: "zh-CN" }
repairing ? FEISHU_ENDPOINTS.beginCallbackRepair : FEISHU_ENDPOINTS.beginProvisioning,
repairing ? { botId } : { locale: "zh-CN" }
));
if (repairing && (provision2.operation !== CALLBACK_REPAIR_OPERATION || provision2.botId !== botId)) {
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u5361\u7247\u4FEE\u590D\u4E8C\u7EF4\u7801");
}
const timestamp7 = Date.now();
setNow(timestamp7);
setModel((current) => ({
@ -4535,20 +4630,34 @@ function FeishuSettingsTab({ rpcCall }) {
provisioning: {
phase: "qr",
...provision2,
...botName ? { botName } : {},
durationMs: Math.max(1, provision2.expiresAt - timestamp7),
expired: false
}
}));
announce("\u6388\u6743\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u3002");
announce(repairing ? `${botName ?? "\u673A\u5668\u4EBA"}\u7684\u4FEE\u590D\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u3002` : "\u6388\u6743\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u3002");
} catch (error) {
setModel((current) => ({
...current,
provisioning: { phase: "error", error: presentError3(error) }
provisioning: {
phase: "error",
operation,
...botId ? { botId } : {},
...botName ? { botName } : {},
...replace && previousAttemptId ? { attemptId: previousAttemptId } : {},
error: presentError3(error)
}
}));
} finally {
setProvisionBusy(false);
}
}, [announce, invoke, model.provisioning?.attemptId]);
}, [
announce,
invoke,
model.provisioning?.attemptId,
model.provisioning?.botId,
model.provisioning?.botName
]);
const bindCredentials = React10.useCallback(async ({ identity, secret }) => {
const snapshotVersion = workspaceFence.beginMutation();
setCredentialBusy(true);
@ -4572,23 +4681,64 @@ function FeishuSettingsTab({ rpcCall }) {
}
}, [announce, invoke, loadStatus, mergeSnapshot, workspaceFence]);
const cancelProvisioning = React10.useCallback(async () => {
const attemptId = model.provisioning?.attemptId;
const activeProvision = model.provisioning;
const attemptId = activeProvision?.attemptId;
const repairing = isCallbackRepair(activeProvision);
const targetBot = repairing ? model.bots.find((bot) => bot.botId === activeProvision?.botId) : void 0;
setProvisionBusy(true);
try {
if (attemptId) await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId });
const result = attemptId ? normalizePollResult(await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId })) : null;
if (repairing && result) {
if (result.operation !== CALLBACK_REPAIR_OPERATION || result.botId !== activeProvision.botId) {
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u6CE8\u518C\u8FDB\u5EA6");
}
if (result.status === "connecting") {
setModel((current) => current.provisioning?.attemptId === attemptId ? {
...current,
provisioning: {
...current.provisioning,
...result.provisioning ?? {},
phase: "connecting",
submitted: true,
expired: false
}
} : current);
announce("\u914D\u7F6E\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u9A8C\u8BC1\u5361\u7247\u6309\u94AE\u56DE\u8C03\u5E76\u91CD\u8FDE\u6B64\u673A\u5668\u4EBA\uFF1B\u6B64\u9636\u6BB5\u65E0\u6CD5\u53D6\u6D88\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002");
return;
}
if (result.status === "connected") {
const targetBotName = targetBot?.bot.name ?? activeProvision.botName ?? "\u673A\u5668\u4EBA";
setModel((current) => ({ ...current, provisioning: null }));
announce(`${targetBotName}\u7684\u5361\u7247\u6309\u94AE\u5DF2\u4FEE\u590D\u3002`);
if (activeProvision.botId) setFocusBotId(activeProvision.botId);
await loadStatus({ silent: true, restoreProvisioning: false });
return;
}
}
setModel((current) => ({ ...current, provisioning: null }));
announce("\u5DF2\u53D6\u6D88\u6DFB\u52A0\u673A\u5668\u4EBA\u3002");
announce(repairing ? "\u5DF2\u53D6\u6D88\u5361\u7247\u6309\u94AE\u4FEE\u590D\u3002" : "\u5DF2\u53D6\u6D88\u6DFB\u52A0\u673A\u5668\u4EBA\u3002");
await loadStatus({ silent: true, restoreProvisioning: false });
scheduleAnimationFrame(() => addButtonRef.current?.focus(), "focus");
scheduleAnimationFrame(() => {
if (repairing && activeProvision.botId) {
cardRefs.current.get(activeProvision.botId)?.focus();
} else {
addButtonRef.current?.focus();
}
}, "focus");
} catch (error) {
setModel((current) => ({
...current,
provisioning: { phase: "error", attemptId, error: presentError3(error) }
provisioning: {
...activeProvision,
phase: "error",
attemptId,
error: presentError3(error)
}
}));
} finally {
setProvisionBusy(false);
}
}, [announce, invoke, loadStatus, model.provisioning?.attemptId, scheduleAnimationFrame]);
}, [announce, invoke, loadStatus, model.bots, model.provisioning, scheduleAnimationFrame]);
const countdownAttemptId = model.provisioning?.attemptId;
const countdownPhase = model.provisioning?.phase;
const countdownExpiresAt = model.provisioning?.expiresAt;
@ -4617,23 +4767,26 @@ function FeishuSettingsTab({ rpcCall }) {
{ attemptId: provision2.attemptId },
controller.signal
));
if (result.operation !== provision2.operation || isCallbackRepair(provision2) && result.botId !== provision2.botId) {
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u6CE8\u518C\u8FDB\u5EA6");
}
if (result.status === "connected") {
const snapshot = await loadStatus({ signal: controller.signal, silent: true, restoreProvisioning: false });
const newBot = snapshot?.bots.find((bot) => bot.botId === result.botId);
const targetBot = snapshot?.bots.find((bot) => bot.botId === result.botId);
if (!snapshot) {
throw new Error("\u673A\u5668\u4EBA\u5DF2\u7ECF\u521B\u5EFA\uFF0C\u4F46\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u8FDE\u63A5\u72B6\u6001");
throw new Error(isCallbackRepair(provision2) ? "\u5361\u7247\u6309\u94AE\u5DF2\u66F4\u65B0\uFF0C\u4F46\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u673A\u5668\u4EBA\u8FDE\u63A5\u72B6\u6001" : "\u673A\u5668\u4EBA\u5DF2\u7ECF\u521B\u5EFA\uFF0C\u4F46\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u8FDE\u63A5\u72B6\u6001");
}
if (!newBot?.connected) {
if (!targetBot?.connected) {
setModel((current) => current.provisioning?.attemptId === provision2.attemptId ? { ...current, provisioning: { ...current.provisioning, phase: "connecting" } } : current);
return;
}
setModel((current) => ({ ...current, provisioning: null }));
announce(newBot ? `${newBot.bot.name}\u5DF2\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u5728\u98DE\u4E66\u4E2D\u5F00\u59CB\u804A\u5929\u3002` : "\u65B0\u98DE\u4E66\u673A\u5668\u4EBA\u5DF2\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u5F00\u59CB\u804A\u5929\u3002");
announce(isCallbackRepair(provision2) ? `${targetBot.bot.name}\u7684\u5361\u7247\u6309\u94AE\u5DF2\u4FEE\u590D\u3002` : targetBot ? `${targetBot.bot.name}\u5DF2\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u5728\u98DE\u4E66\u4E2D\u5F00\u59CB\u804A\u5929\u3002` : "\u65B0\u98DE\u4E66\u673A\u5668\u4EBA\u5DF2\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u5F00\u59CB\u804A\u5929\u3002");
if (result.botId) setFocusBotId(result.botId);
return;
}
if (result.status === "failed") {
const error = new Error(result.message ?? "\u98DE\u4E66\u5E94\u7528\u521B\u5EFA\u5931\u8D25");
const error = new Error(result.message ?? (isCallbackRepair(provision2) ? "\u98DE\u4E66\u5361\u7247\u6309\u94AE\u4FEE\u590D\u5931\u8D25" : "\u98DE\u4E66\u5E94\u7528\u521B\u5EFA\u5931\u8D25"));
error.code = "FEISHU_PROVISION_FAILED";
throw error;
}
@ -4658,6 +4811,7 @@ function FeishuSettingsTab({ rpcCall }) {
setModel((current) => current.provisioning?.attemptId === provision2.attemptId ? {
...current,
provisioning: {
...current.provisioning,
phase: "error",
attemptId: provision2.attemptId,
error: presentError3(error)
@ -4686,6 +4840,20 @@ function FeishuSettingsTab({ rpcCall }) {
return next;
});
}, []);
const repairCallback = React10.useCallback((connection) => {
if (model.provisioning) return;
setRemoveTargetId(null);
setBotError(connection.botId, null);
setTestNoticesByBot((current) => {
const next = { ...current };
delete next[connection.botId];
return next;
});
void startProvisioning({
operation: CALLBACK_REPAIR_OPERATION,
bot: connection
});
}, [model.provisioning, setBotError, startProvisioning]);
const reconnectOneBot = React10.useCallback(async (connection) => {
const { botId, bot } = connection;
const snapshotVersion = workspaceFence.beginMutation();
@ -4779,28 +4947,48 @@ function FeishuSettingsTab({ rpcCall }) {
}
}, [announce, invoke, loadStatus, mergeSnapshot, scheduleAnimationFrame, setBotBusy, setBotError, workspaceFence]);
const provision = model.provisioning;
const provisionBot = provision?.botId ? model.bots.find((bot) => bot.botId === provision.botId) ?? { botId: provision.botId, bot: { name: provision.botName ?? "\u6B64\u673A\u5668\u4EBA" } } : void 0;
const restartProvisioning = ({ replace = false } = {}) => startProvisioning({
replace,
operation: provision?.operation ?? FEISHU_REGISTRATION_OPERATIONS.PROVISION,
bot: provisionBot
});
let provisionContent = null;
if (provision?.phase === "creating") {
provisionContent = h2(ProvisionProgress, { phase: "creating", busy: provisionBusy });
provisionContent = h2(ProvisionProgress, {
phase: "creating",
provision,
busy: provisionBusy
});
} else if (provision?.phase === "qr") {
provisionContent = h2(QrPane, {
provision,
now,
onRefresh: () => void startProvisioning({ replace: true }),
onRefresh: () => void restartProvisioning({ replace: true }),
onCancel: () => void cancelProvisioning(),
busy: provisionBusy || model.phase !== "ready"
});
} else if (provision?.phase === "connecting") {
provisionContent = h2(ProvisionProgress, {
phase: "connecting",
onCancel: () => void cancelProvisioning(),
provision,
onCancel: isCallbackRepair(provision) ? void 0 : () => void cancelProvisioning(),
busy: provisionBusy
});
} else if (provision?.phase === "error") {
provisionContent = h2(ProvisionError2, {
error: provision.error,
onRetry: () => void startProvisioning({ replace: Boolean(provision.attemptId) }),
onCancel: () => void cancelProvisioning(),
provision,
onRetry: () => void restartProvisioning({ replace: Boolean(provision.attemptId) }),
onCancel: () => {
const targetBotId = provision.botId;
setModel((current) => ({ ...current, provisioning: null }));
void loadStatus({ silent: true, restoreProvisioning: false });
scheduleAnimationFrame(() => {
if (targetBotId) cardRefs.current.get(targetBotId)?.focus();
else addButtonRef.current?.focus();
}, "focus");
},
busy: provisionBusy
});
}
@ -4870,7 +5058,9 @@ function FeishuSettingsTab({ rpcCall }) {
errorsByBot,
testNoticesByBot,
removeTargetId,
provisioning: provision,
onReconnect: (bot) => void reconnectOneBot(bot),
onRepairCallback: repairCallback,
onWorkspaceSave: saveWorkspace,
onRequestRemove: requestRemove,
onConfirmRemove: (bot) => void confirmRemove(bot),