fix(client): recover loopback RPC 403 via localhost

This commit is contained in:
xmanrui 2026-08-25 00:50:27 +08:00
parent 9e39333ff6
commit d999cb511f
6 changed files with 425 additions and 18 deletions

View file

@ -35,6 +35,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
var index_exports = {};
__export(index_exports, {
IMSettingsTab: () => IMSettingsTab,
LoopbackRecoveryNotice: () => LoopbackRecoveryNotice,
apply: () => apply,
inject: () => inject,
name: () => name
@ -245,6 +246,10 @@ var EN = Object.freeze({
"\u672C\u673A\u6682\u65F6\u65E0\u6CD5\u8BBF\u95EE AI Office\u3002": "AI Office cannot currently be reached from this machine.",
"AI Office \u8FDE\u63A5\u5DF2\u4E2D\u65AD\u3002": "The AI Office connection was interrupted.",
"\u5E2E\u52A9\u4E0E\u53CD\u9988 \xB7 \u524D\u5F80 GitHub": "Help & feedback \xB7 Open GitHub",
"\u8BF7\u6539\u7528 localhost \u91CD\u65B0\u6253\u5F00": "Reopen with localhost",
"\u9875\u9762\u4F1A\u5728\u5F53\u524D\u7AEF\u53E3\u91CD\u65B0\u6253\u5F00\uFF0C\u673A\u5668\u4EBA\u914D\u7F6E\u4E0D\u4F1A\u6539\u53D8\u3002": "The page will reopen on the current port. Your bot configuration will not change.",
"\u4F7F\u7528 localhost \u91CD\u65B0\u6253\u5F00": "Reopen with localhost",
"\u5F53\u524D\u5730\u5740\u4E0E\u6D4F\u89C8\u5668\u7684\u672C\u673A\u8BF7\u6C42\u6821\u9A8C\u4E0D\u517C\u5BB9\u3002\u8BF7\u4F7F\u7528\u4E0A\u65B9\u6309\u94AE\u6539\u7528 localhost \u91CD\u65B0\u6253\u5F00\u3002": "This address is incompatible with the browser\u2019s local-request checks. Use the button above to reopen with localhost.",
"\u5FAE\u4FE1": "WeChat",
"\u98DE\u4E66": "Feishu",
"\u9489\u9489": "DingTalk",
@ -10014,6 +10019,61 @@ function WhatsappSettingsTab({ rpcCall }) {
));
}
// plugin-src/client/loopback-recovery.js
var TRANSPORT_FORBIDDEN = /^transport failure for \/[A-Za-z0-9._~-]+\/[A-Za-z0-9_$./~-]+: HTTP 403$/;
var LOOPBACK_RECOVERY_ERROR_CODE = "loopback-recovery-required";
var LOOPBACK_RECOVERY_ERROR_MESSAGE = "\u5F53\u524D\u5730\u5740\u4E0E\u6D4F\u89C8\u5668\u7684\u672C\u673A\u8BF7\u6C42\u6821\u9A8C\u4E0D\u517C\u5BB9\u3002\u8BF7\u4F7F\u7528\u4E0A\u65B9\u6309\u94AE\u6539\u7528 localhost \u91CD\u65B0\u6253\u5F00\u3002";
function isIpv4Loopback(hostname) {
const parts = hostname.split(".");
return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
}
function createLoopbackRecovery(error, location) {
if (!TRANSPORT_FORBIDDEN.test(error?.message ?? "")) return null;
if (typeof location?.href !== "string") return null;
try {
const current = new URL(location.href);
if (current.protocol !== "http:" || !isIpv4Loopback(current.hostname)) return null;
current.hostname = "localhost";
return Object.freeze({
url: current.href,
origin: current.origin
});
} catch {
return null;
}
}
function createLoopbackAwareRpcCall(rpcCall, {
location,
onRecovery
} = {}) {
if (typeof rpcCall !== "function") throw new TypeError("rpcCall must be a function");
return async (...args) => {
try {
return await rpcCall(...args);
} catch (error) {
const recovery = createLoopbackRecovery(error, location);
if (!recovery) throw error;
onRecovery?.(recovery);
const presented = new Error(LOOPBACK_RECOVERY_ERROR_MESSAGE);
presented.code = LOOPBACK_RECOVERY_ERROR_CODE;
presented.cause = error;
presented.recoveryUrl = recovery.url;
throw presented;
}
};
}
function createLoopbackAwareRpcCalls(rpcCalls, options) {
return Object.freeze(Object.fromEntries(
Object.entries(rpcCalls).map(([name2, rpcCall]) => [
name2,
typeof rpcCall === "function" ? createLoopbackAwareRpcCall(rpcCall, options) : rpcCall
])
));
}
function replacePageLocation(url, location = globalThis.location) {
location?.replace?.(url);
}
// plugin-src/client/styles.js
var IM_STYLE_ID = "xmanrui-dsh-im-settings";
var CSS11 = String.raw`
@ -10073,6 +10133,14 @@ var CSS11 = String.raw`
.dim-channelNote { overflow: hidden; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 10px; line-height: 13px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
.dim-divider { width: 1px; min-height: 520px; background: var(--dsw-alias-border-l1, #eef0f3); }
.dim-panel { min-width: 0; container-type: inline-size; }
.dim-loopbackRecovery { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 14px; padding: 14px 16px; border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #d97706) 30%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 12px; color: var(--dsw-alias-label-primary, #1f2329); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #d97706) 8%, var(--dsw-alias-bg-layer-1, #fff)); }
.dim-loopbackRecoveryCopy { min-width: 0; }
.dim-loopbackRecoveryCopy strong { display: block; font-size: 14px; line-height: 20px; font-weight: 650; }
.dim-loopbackRecoveryCopy p { margin: 3px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
.dim-loopbackRecoveryCopy code { display: block; overflow: hidden; margin-top: 5px; color: var(--dsw-alias-label-secondary, #646a73); font: 11px/16px ui-monospace, SFMono-Regular, Menlo, monospace; text-overflow: ellipsis; white-space: nowrap; }
.dim-loopbackRecoveryAction { flex: none; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; padding: 0 12px; border: 1px solid #1677ff; border-radius: 8px; color: #fff; background: #1677ff; font: inherit; font-size: 13px; font-weight: 560; white-space: nowrap; cursor: pointer; }
.dim-loopbackRecoveryAction:hover { border-color: #0958d9; background: #0958d9; }
.dim-loopbackRecoveryAction:focus-visible { outline: 2px solid color-mix(in srgb, #1677ff 62%, white); outline-offset: 2px; }
.dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page, .dim-panel .dqq-page, .dim-panel .dwecom-page, .dim-panel .dsl-page, .dim-panel .dwa-page { width: 100%; max-width: none; padding: 0 0 24px; }
.dim-panel .bxf-heading, .dim-panel .dxw-heading, .dim-panel .ddt-heading { justify-content: flex-end; }
.dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; justify-content: stretch; gap: 8px; }
@ -10282,6 +10350,8 @@ var CSS11 = String.raw`
.dim-title p { white-space: normal; }
.dim-githubTooltip { right: auto; left: 0; }
.dim-rail { grid-template-columns: minmax(0, 1fr); }
.dim-loopbackRecovery { align-items: stretch; flex-direction: column; gap: 12px; }
.dim-loopbackRecoveryAction { width: 100%; }
.dim-directoryPickerBackdrop { padding: 10px; }
.dim-directoryPicker { height: calc(100vh - 20px); min-height: 0; border-radius: 14px; }
.dim-directoryPickerHeader { padding: 18px 17px 14px; }
@ -10398,6 +10468,27 @@ function ChannelLogo({ channel: channel4 }) {
if (channel4 === "whatsapp") return h2(WhatsappLogo);
return h2(OfficeLogo);
}
function LoopbackRecoveryNotice({ recovery, onNavigate = replacePageLocation }) {
return h2(
"div",
{
className: "dim-loopbackRecovery",
role: "alert"
},
h2(
"div",
{ className: "dim-loopbackRecoveryCopy" },
h2("strong", null, "\u8BF7\u6539\u7528 localhost \u91CD\u65B0\u6253\u5F00"),
h2("p", null, "\u9875\u9762\u4F1A\u5728\u5F53\u524D\u7AEF\u53E3\u91CD\u65B0\u6253\u5F00\uFF0C\u673A\u5668\u4EBA\u914D\u7F6E\u4E0D\u4F1A\u6539\u53D8\u3002"),
h2("code", null, recovery.origin)
),
h2("button", {
type: "button",
className: "dim-loopbackRecoveryAction",
onClick: () => onNavigate(recovery.url)
}, "\u4F7F\u7528 localhost \u91CD\u65B0\u6253\u5F00")
);
}
function IMSettingsTab({
dingtalkRpcCall,
discordRpcCall,
@ -10409,11 +10500,45 @@ function IMSettingsTab({
weixinRpcCall,
whatsappRpcCall,
officeRpcCall,
workspaceDirectoryPicker
workspaceDirectoryPicker,
browserLocation = globalThis.location,
navigateToRecoveryUrl = replacePageLocation
}) {
const [selected, setSelected] = React20.useState("weixin");
const [loopbackRecovery, setLoopbackRecovery] = React20.useState(null);
const githubTooltipId = React20.useId();
const active = CHANNELS.find((channel4) => channel4.id === selected) ?? CHANNELS[0];
const reportLoopbackRecovery = React20.useCallback((recovery) => {
setLoopbackRecovery((current) => current?.url === recovery.url ? current : recovery);
}, []);
const rpcCalls = React20.useMemo(() => createLoopbackAwareRpcCalls({
dingtalkRpcCall,
discordRpcCall,
feishuRpcCall,
qqRpcCall,
slackRpcCall,
telegramRpcCall,
wecomRpcCall,
weixinRpcCall,
whatsappRpcCall,
officeRpcCall
}, {
location: browserLocation,
onRecovery: reportLoopbackRecovery
}), [
browserLocation,
dingtalkRpcCall,
discordRpcCall,
feishuRpcCall,
officeRpcCall,
qqRpcCall,
reportLoopbackRecovery,
slackRpcCall,
telegramRpcCall,
wecomRpcCall,
weixinRpcCall,
whatsappRpcCall
]);
return h2(
WorkspaceDirectoryPickerContext.Provider,
{ value: workspaceDirectoryPicker },
@ -10480,12 +10605,20 @@ function IMSettingsTab({
))
),
h2("div", { className: "dim-divider", "aria-hidden": "true" }),
h2("main", {
className: "dim-panel",
role: "tabpanel",
id: `dim-panel-${active.id}`,
"aria-labelledby": `dim-tab-${active.id}`
}, active.id === "weixin" ? h2(WeixinSettingsTab, { rpcCall: weixinRpcCall }) : active.id === "feishu" ? h2(FeishuSettingsTab, { rpcCall: feishuRpcCall }) : active.id === "dingtalk" ? h2(DingtalkSettingsTab, { rpcCall: dingtalkRpcCall }) : active.id === "wecom" ? h2(WecomSettingsTab, { rpcCall: wecomRpcCall }) : active.id === "qq" ? h2(QqSettingsTab, { rpcCall: qqRpcCall }) : active.id === "slack" ? h2(SlackSettingsTab, { rpcCall: slackRpcCall }) : active.id === "telegram" ? h2(TelegramSettingsTab, { rpcCall: telegramRpcCall }) : active.id === "discord" ? h2(DiscordSettingsTab, { rpcCall: discordRpcCall }) : active.id === "whatsapp" ? h2(WhatsappSettingsTab, { rpcCall: whatsappRpcCall }) : h2(OfficeSettingsTab, { rpcCall: officeRpcCall }))
h2(
"main",
{
className: "dim-panel",
role: "tabpanel",
id: `dim-panel-${active.id}`,
"aria-labelledby": `dim-tab-${active.id}`
},
loopbackRecovery ? h2(LoopbackRecoveryNotice, {
recovery: loopbackRecovery,
onNavigate: navigateToRecoveryUrl
}) : null,
active.id === "weixin" ? h2(WeixinSettingsTab, { rpcCall: rpcCalls.weixinRpcCall }) : active.id === "feishu" ? h2(FeishuSettingsTab, { rpcCall: rpcCalls.feishuRpcCall }) : active.id === "dingtalk" ? h2(DingtalkSettingsTab, { rpcCall: rpcCalls.dingtalkRpcCall }) : active.id === "wecom" ? h2(WecomSettingsTab, { rpcCall: rpcCalls.wecomRpcCall }) : active.id === "qq" ? h2(QqSettingsTab, { rpcCall: rpcCalls.qqRpcCall }) : active.id === "slack" ? h2(SlackSettingsTab, { rpcCall: rpcCalls.slackRpcCall }) : active.id === "telegram" ? h2(TelegramSettingsTab, { rpcCall: rpcCalls.telegramRpcCall }) : active.id === "discord" ? h2(DiscordSettingsTab, { rpcCall: rpcCalls.discordRpcCall }) : active.id === "whatsapp" ? h2(WhatsappSettingsTab, { rpcCall: rpcCalls.whatsappRpcCall }) : h2(OfficeSettingsTab, { rpcCall: rpcCalls.officeRpcCall })
)
)
)
);