feat(whatsapp): add access modes

This commit is contained in:
xmanrui 2026-08-23 21:28:50 +08:00
parent b0e2c266c5
commit 9b65dd3d6b
15 changed files with 860 additions and 162 deletions

View file

@ -550,6 +550,24 @@ var EN = Object.freeze({
"\u6B63\u5728\u5EFA\u7ACB\u5B89\u5168\u7684\u5173\u8054\u8BBE\u5907\u4F1A\u8BDD\u3002": "Creating a secure linked-device session.",
"\u5173\u8054\u8BBE\u5907\u6B63\u5728\u63A5\u5165 DeepSeek Harness\u3002": "Linking the device to DeepSeek Harness.",
"WhatsApp Web \u5173\u8054\u8BBE\u5907\u8FD0\u884C\u6B63\u5E38": "WhatsApp linked device is healthy",
"\u67E5\u770B WhatsApp \u8BBF\u95EE\u6A21\u5F0F\u8BF4\u660E": "View WhatsApp access mode details",
"WhatsApp \u8BBF\u95EE\u6A21\u5F0F": "WhatsApp access mode",
"\u4EC5\u81EA\u5DF1\u6A21\u5F0F": "Only me",
"\u6307\u5B9A\u8054\u7CFB\u4EBA\u6A21\u5F0F": "Selected contacts",
"\u5F00\u653E\u54CD\u5E94\u6A21\u5F0F": "Open responses",
"\u4EC5\u81EA\u5DF1\u6A21\u5F0F\uFF08\u9ED8\u8BA4\uFF09": "Only me (default)",
"\u5DF2\u751F\u6548\uFF1A": "Active: ",
"\u53EA\u54CD\u5E94\u5DF2\u7ED1\u5B9A WhatsApp \u8D26\u53F7\u7684\u81EA\u804A\u6D88\u606F\u3002": "Only respond to self-chat messages from the linked WhatsApp account.",
"\u54CD\u5E94\u81EA\u804A\u548C\u767D\u540D\u5355\u8054\u7CFB\u4EBA\u7684\u79C1\u804A\uFF0C\u5FFD\u7565\u7FA4\u804A\u3002": "Respond to self-chat and allowlisted direct messages; ignore group messages.",
"\u54CD\u5E94\u6240\u6709\u79C1\u804A\uFF0C\u4EE5\u53CA\u7FA4\u804A\u4E2D\u7684\u63D0\u53CA\u6216\u56DE\u590D\u3002": "Respond to all direct messages and to group mentions or replies.",
"\u5141\u8BB8\u79C1\u804A\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801": "WhatsApp phone numbers allowed to send direct messages",
"\u6BCF\u884C\u4E00\u4E2A\u542B\u56FD\u5BB6\u6216\u5730\u533A\u4EE3\u7801\u7684\u53F7\u7801": "One number with country or region code per line",
"\u53EF\u4EE5\u5305\u542B\u5F00\u5934\u7684 +\uFF0C\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u79FB\u9664\u3002": "A leading + is allowed and removed when saved.",
"\u4EC5\u6307\u5B9A\u8054\u7CFB\u4EBA\u6A21\u5F0F\u4F7F\u7528\u767D\u540D\u5355\uFF0C\u5207\u6362\u6A21\u5F0F\u65F6\u4F1A\u4FDD\u7559\u3002": "Only Selected contacts uses the allowlist; it is retained when modes change.",
"\u767D\u540D\u5355\u4E3A\u7A7A\uFF1B\u4FDD\u5B58\u540E\u5C06\u53EA\u63A5\u53D7\u81EA\u804A\u6D88\u606F\u3002": "The allowlist is empty; only self-chat messages will be accepted after saving.",
"\u7535\u8BDD\u53F7\u7801\u5FC5\u987B\u5305\u542B\u56FD\u5BB6\u6216\u5730\u533A\u4EE3\u7801\uFF0C\u6BCF\u884C\u4E00\u4E2A\u3002": "Each phone number must include a country or region code on its own line.",
"WhatsApp \u8BBF\u95EE\u8BBE\u7F6E\u6682\u4E0D\u53EF\u7528\u3002": "WhatsApp access settings are currently unavailable.",
"WhatsApp \u8BBF\u95EE\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25\u3002": "Could not save WhatsApp access settings.",
"Bot API \u957F\u8F6E\u8BE2": "Bot API long polling",
" Gateway \u957F\u8FDE\u63A5": " Gateway persistent connection",
"Gateway \u957F\u8FDE\u63A5": "Gateway persistent connection",
@ -9075,6 +9093,7 @@ var WHATSAPP_ENDPOINTS = Object.freeze({
cancelProvisioning: "provision.cancel",
reconnectBot: "bot.reconnect",
deleteBot: "bot.delete",
setAccessPolicy: "bot.access-policy.set",
setWorkspace: "bot.workspace.set",
setAgentPreset: SET_AGENT_PRESET_ENDPOINT
});
@ -9142,6 +9161,12 @@ function normalizeBot6(value) {
state: connected ? "connected" : state,
workspace: text4(value.workspace, "", 4096),
agentPreset: normalizeAgentPresetId(value.agentPreset),
accessPolicy: {
accessMode: ["self-only", "private-allowlist", "open"].includes(
value.accessPolicy?.accessMode
) ? value.accessPolicy.accessMode : "self-only",
allowedNumbers: Array.isArray(value.accessPolicy?.allowedNumbers) ? [...new Set(value.accessPolicy.allowedNumbers.filter((entry) => typeof entry === "string" && /^[1-9]\d{4,14}$/.test(entry)))] : []
},
bot: {
name: text4(value.bot?.name, "WhatsApp\u673A\u5668\u4EBA", 100),
idMasked: text4(value.bot?.idMasked, "WhatsApp\u8D26\u53F7", 140)
@ -9190,6 +9215,31 @@ var CSS10 = String.raw`
.dwa-page { --ddt-accent: #25d366; --ddt-accent-deep: #128c7e; --ddt-accent-wash: #eafbf0; }
.dwa-avatar { color: #fff; background: #25d366; }
.dwa-avatar svg { display: block; }
.dwa-access { display: grid; gap: 10px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 10px; background: var(--dsw-alias-bg-layer-2, #f7f8fa); }
.dwa-accessHeading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.dwa-accessHeading > strong { font-size: 13px; }
.dwa-accessStatus { min-width: 0; display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; }
.dwa-accessBadge { flex: none; padding: 3px 8px; border-radius: 999px; color: #08785f; background: #eafbf0; font-size: 11px; font-weight: 700; }
.dwa-accessBadge[data-mode="private-allowlist"] { color: #0f6f8f; background: #eaf7fd; }
.dwa-accessBadge[data-mode="open"] { color: #a15c00; background: #fff3d6; }
.dwa-accessHelp { position: relative; display: inline-flex; flex: none; }
.dwa-accessHelpButton { width: 20px; height: 20px; display: grid; place-items: center; padding: 0; border: 1px solid color-mix(in srgb, #25d366 34%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 50%; color: #128c7e; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; line-height: 1; font-weight: 750; cursor: help; }
.dwa-accessTooltip { position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; width: 270px; max-width: min(290px, calc(100vw - 48px)); display: grid; gap: 8px; padding: 10px 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 9px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 10px 28px rgb(31 35 41 / 16%); opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
.dwa-accessTooltipItem { display: grid; gap: 2px; }
.dwa-accessTooltipItem + .dwa-accessTooltipItem { padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l2, #eef0f3); }
.dwa-accessTooltipItem strong { font-size: 12px; line-height: 17px; }
.dwa-accessTooltipItem > span { color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 16px; }
.dwa-accessHelp:hover .dwa-accessTooltip, .dwa-accessHelp:focus-within .dwa-accessTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
.dwa-accessField { display: grid; gap: 5px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; font-weight: 600; }
.dwa-accessField select, .dwa-accessField textarea { width: 100%; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1, #c9cdd4); border-radius: 7px; color: inherit; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-weight: 400; }
.dwa-accessField select { height: 34px; padding: 0 9px; }
.dwa-accessField textarea { min-height: 68px; padding: 8px 9px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.dwa-accessField textarea:disabled { color: var(--dsw-alias-label-tertiary, #8f959e); background: var(--dsw-alias-bg-module-platform, #f2f3f5); cursor: not-allowed; resize: none; opacity: 1; }
.dwa-accessField small { color: var(--dsw-alias-label-secondary, #646a73); font-weight: 400; }
.dwa-accessWarning, .dwa-accessError { margin: 0; font-size: 12px; line-height: 1.5; }
.dwa-accessWarning { color: #a15c00; }
.dwa-accessError { color: var(--dsw-alias-state-error-primary, #d83931); }
.dwa-accessActions { display: flex; justify-content: flex-end; }
`;
function installWhatsappStyles() {
if (typeof document === "undefined") return () => {
@ -9207,6 +9257,156 @@ function installWhatsappStyles() {
// plugin-src/client/channels/whatsapp/index.js
var ACTIVE_STATES3 = /* @__PURE__ */ new Set(["pending", "connecting"]);
function accessPolicyFor(account) {
const accessMode = ["self-only", "private-allowlist", "open"].includes(
account?.accessPolicy?.accessMode
) ? account.accessPolicy.accessMode : "self-only";
return {
accessMode,
allowedNumbers: Array.isArray(account?.accessPolicy?.allowedNumbers) ? account.accessPolicy.allowedNumbers : []
};
}
function allowedNumbersFromText(value) {
const entries = value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
const normalized = entries.map((entry) => entry.replace(/^\+/, ""));
if (normalized.some((entry) => !/^[1-9]\d{4,14}$/.test(entry))) {
throw new TypeError("\u7535\u8BDD\u53F7\u7801\u5FC5\u987B\u5305\u542B\u56FD\u5BB6\u6216\u5730\u533A\u4EE3\u7801\uFF0C\u6BCF\u884C\u4E00\u4E2A\u3002");
}
return [...new Set(normalized)];
}
function WhatsappAccessSettings({ account, busy = false, onSave }) {
const policy = accessPolicyFor(account);
const sourceNumbers = policy.allowedNumbers.join("\n");
const helpId = React19.useId();
const [accessMode, setAccessMode] = React19.useState(policy.accessMode);
const [allowedNumbers, setAllowedNumbers] = React19.useState(sourceNumbers);
const [error, setError] = React19.useState(null);
React19.useEffect(() => {
setAccessMode(policy.accessMode);
setAllowedNumbers(sourceNumbers);
setError(null);
}, [policy.accessMode, sourceNumbers]);
const save = async (event) => {
event.preventDefault();
setError(null);
try {
const normalized = allowedNumbersFromText(allowedNumbers);
if (typeof onSave !== "function") throw new Error("WhatsApp \u8BBF\u95EE\u8BBE\u7F6E\u6682\u4E0D\u53EF\u7528\u3002");
await onSave({ accessMode, allowedNumbers: normalized });
} catch (caught) {
setError(caught?.message ?? "WhatsApp \u8BBF\u95EE\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25\u3002");
}
};
const allowlistEnabled = accessMode === "private-allowlist";
const labels = {
"self-only": "\u4EC5\u81EA\u5DF1\u6A21\u5F0F",
"private-allowlist": "\u6307\u5B9A\u8054\u7CFB\u4EBA\u6A21\u5F0F",
open: "\u5F00\u653E\u54CD\u5E94\u6A21\u5F0F"
};
return h2(
"form",
{ className: "dwa-access", onSubmit: save },
h2(
"div",
{ className: "dwa-accessHeading" },
h2("strong", null, "\u8BBF\u95EE\u8BBE\u7F6E"),
h2(
"span",
{ className: "dwa-accessStatus" },
h2(
"span",
{ className: "dwa-accessBadge", "data-mode": policy.accessMode },
["\u5DF2\u751F\u6548\uFF1A", labels[policy.accessMode]]
),
h2(
"span",
{ className: "dwa-accessHelp" },
h2("button", {
type: "button",
className: "dwa-accessHelpButton",
"aria-label": "\u67E5\u770B WhatsApp \u8BBF\u95EE\u6A21\u5F0F\u8BF4\u660E",
"aria-describedby": helpId
}, h2("span", { "aria-hidden": "true" }, "?")),
h2(
"span",
{ id: helpId, className: "dwa-accessTooltip", role: "tooltip" },
h2(
"span",
{ className: "dwa-accessTooltipItem" },
h2("strong", null, "\u4EC5\u81EA\u5DF1\u6A21\u5F0F"),
h2("span", null, "\u53EA\u54CD\u5E94\u5DF2\u7ED1\u5B9A WhatsApp \u8D26\u53F7\u7684\u81EA\u804A\u6D88\u606F\u3002")
),
h2(
"span",
{ className: "dwa-accessTooltipItem" },
h2("strong", null, "\u6307\u5B9A\u8054\u7CFB\u4EBA\u6A21\u5F0F"),
h2("span", null, "\u54CD\u5E94\u81EA\u804A\u548C\u767D\u540D\u5355\u8054\u7CFB\u4EBA\u7684\u79C1\u804A\uFF0C\u5FFD\u7565\u7FA4\u804A\u3002")
),
h2(
"span",
{ className: "dwa-accessTooltipItem" },
h2("strong", null, "\u5F00\u653E\u54CD\u5E94\u6A21\u5F0F"),
h2("span", null, "\u54CD\u5E94\u6240\u6709\u79C1\u804A\uFF0C\u4EE5\u53CA\u7FA4\u804A\u4E2D\u7684\u63D0\u53CA\u6216\u56DE\u590D\u3002")
)
)
)
)
),
h2(
"label",
{ className: "dwa-accessField" },
h2("span", null, "\u6A21\u5F0F"),
h2(
"select",
{
value: accessMode,
disabled: busy,
"aria-label": "WhatsApp \u8BBF\u95EE\u6A21\u5F0F",
onChange: (event) => {
setAccessMode(event.target.value);
setError(null);
}
},
h2("option", { value: "self-only" }, "\u4EC5\u81EA\u5DF1\u6A21\u5F0F\uFF08\u9ED8\u8BA4\uFF09"),
h2("option", { value: "private-allowlist" }, "\u6307\u5B9A\u8054\u7CFB\u4EBA\u6A21\u5F0F"),
h2("option", { value: "open" }, "\u5F00\u653E\u54CD\u5E94\u6A21\u5F0F")
)
),
allowlistEnabled ? h2(
"label",
{ className: "dwa-accessField" },
h2("span", null, "\u5141\u8BB8\u79C1\u804A\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801"),
h2("textarea", {
value: allowedNumbers,
disabled: busy,
rows: 3,
placeholder: "\u6BCF\u884C\u4E00\u4E2A\u542B\u56FD\u5BB6\u6216\u5730\u533A\u4EE3\u7801\u7684\u53F7\u7801",
"aria-label": "\u5141\u8BB8\u79C1\u804A\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801",
onChange: (event) => {
setAllowedNumbers(event.target.value);
setError(null);
}
}),
h2("small", null, "\u53EF\u4EE5\u5305\u542B\u5F00\u5934\u7684 +\uFF0C\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u79FB\u9664\u3002")
) : null,
allowlistEnabled && allowedNumbers.trim() === "" ? h2(
"p",
{ className: "dwa-accessWarning", role: "status" },
"\u767D\u540D\u5355\u4E3A\u7A7A\uFF1B\u4FDD\u5B58\u540E\u5C06\u53EA\u63A5\u53D7\u81EA\u804A\u6D88\u606F\u3002"
) : null,
error ? h2("p", { className: "dwa-accessError", role: "alert" }, error) : null,
h2(
"div",
{ className: "dwa-accessActions" },
h2("button", {
type: "submit",
className: "ddt-button",
"data-kind": "secondary",
disabled: busy
}, busy ? "\u6B63\u5728\u4FDD\u5B58\u2026" : "\u4FDD\u5B58\u8BBF\u95EE\u8BBE\u7F6E")
)
);
}
var Button14 = React19.forwardRef(function Button15({ children, kind = "secondary", className = "", ...props }, ref) {
return h2("button", {
...props,
@ -9430,6 +9630,7 @@ function WhatsappAccountCard({
onReconnect,
onWorkspaceSave,
onAgentPresetSave,
onAccessPolicySave,
onRequestRemove,
onConfirmRemove,
onCancelRemove
@ -9480,6 +9681,11 @@ function WhatsappAccountCard({
disabled: Boolean(busy),
onSave: onAgentPresetSave
}),
h2(WhatsappAccessSettings, {
account,
busy: Boolean(busy),
onSave: onAccessPolicySave
}),
h2(
"div",
{ className: "ddt-accountFooter dim-cardFooter" },
@ -9747,6 +9953,12 @@ function WhatsappSettingsTab({ rpcCall }) {
WHATSAPP_ENDPOINTS.setAgentPreset,
{ botId: account.botId, agentPreset }
),
onAccessPolicySave: (accessPolicy) => botAction(
account,
"access",
WHATSAPP_ENDPOINTS.setAccessPolicy,
{ botId: account.botId, ...accessPolicy }
),
onRequestRemove: () => setRemoveTarget(account.botId),
onCancelRemove: () => setRemoveTarget(null),
onConfirmRemove: async () => {