mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +08:00
fix(client): recover loopback RPC 403 via localhost
This commit is contained in:
parent
9e39333ff6
commit
d999cb511f
6 changed files with 425 additions and 18 deletions
147
lib/client.js
147
lib/client.js
|
|
@ -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 })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue