mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
Fix WhatsApp access settings showing empty grant state (4.9.1-ops.6).
Client normalizeBot dropped accessGrant/groupSessionScope, so pending, contacts, and group cards never appeared. Also record contacts only on DM/@, auto-create group buckets on @, and add one-click grant buttons. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a0c49707a1
commit
93672dfd91
8 changed files with 685 additions and 444 deletions
494
lib/client.js
494
lib/client.js
|
|
@ -571,7 +571,7 @@ var React24 = __toESM(require("react"), 1);
|
|||
// package.json
|
||||
var package_default = {
|
||||
name: "dsh-im-ops",
|
||||
version: "4.9.1-ops.5",
|
||||
version: "4.9.1-ops.6",
|
||||
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 \u2014 access/session policies owned here.",
|
||||
keywords: [
|
||||
"deepseek-harness",
|
||||
|
|
@ -852,6 +852,14 @@ var EN = Object.freeze({
|
|||
"\u673A\u5668\u4EBA\u8BBE\u7F6E\u9875\u7B7E": "Bot settings tabs",
|
||||
"\u6295\u9012\u8BBE\u7F6E": "Delivery settings",
|
||||
"\u8BBF\u95EE\u8BBE\u7F6E": "Access settings",
|
||||
"\u52A0\u672C\u7FA4": "Add to group",
|
||||
"\u5DF2\u5728\u672C\u7FA4": "Already in group",
|
||||
"\u52A0\u79C1\u804A": "Add to DMs",
|
||||
"\u5DF2\u5728\u79C1\u804A": "Already in DMs",
|
||||
"\u5DF2\u52A0\u5165\u672C\u7FA4\u6388\u6743\uFF0C\u8BB0\u5F97\u70B9\u4FDD\u5B58\u3002": "Added to this group. Remember to save.",
|
||||
"\u5DF2\u52A0\u5165\u79C1\u804A\u6388\u6743\uFF0C\u8BB0\u5F97\u70B9\u4FDD\u5B58\u3002": "Added to DM access. Remember to save.",
|
||||
"\u6682\u65E0\u8054\u7CFB\u4EBA\u3002\u6709\u4EBA\u79C1\u804A\u6216 @ \u673A\u5668\u4EBA\u540E\u4F1A\u51FA\u73B0\u5728\u6B64\u3002": "No contacts yet. They appear here after someone DMs or @mentions the bot.",
|
||||
"\u4EC5\u8BB0\u5F55\u79C1\u804A\u673A\u5668\u4EBA\uFF0C\u6216\u5728\u7FA4\u91CC @ \u673A\u5668\u4EBA\u7684\u4EBA\u3002\u7535\u8BDD\u662F\u552F\u4E00\u6388\u6743\u952E\uFF0C\u53EF\u4E00\u952E\u52A0\u5165\u79C1\u804A/\u672C\u7FA4\u6388\u6743\u3002": "Only people who DM the bot or @mention it in a group are recorded. Phone is the sole auth key; use one-click to grant DM or group access.",
|
||||
"WhatsApp \u6309\u7535\u8BDD\u53F7\u7801\u6388\u6743\uFF1A\u5168\u5C40\u7BA1\u7406\u5458\u7BA1\u79C1\u804A\uFF1B\u7FA4\u7BA1\u7406\u5458\u53EA\u6279\u672C\u7FA4\u3002\u6210\u5458\u4E0D\u53EF\u8DE8\u7FA4\u3001\u6709\u7FA4\u6743\u4E5F\u4E0D\u81EA\u52A8\u83B7\u5F97\u79C1\u804A\u6743\u3002": "WhatsApp authorizes by phone number: global admins manage DMs; group admins approve only their group. Members do not cross groups, and group access does not grant DMs.",
|
||||
"\u4FDD\u5B58\u5206\u7EA7\u8BBF\u95EE\u8BBE\u7F6E": "Save graded access settings",
|
||||
"\u4FDD\u5B58\u8BBF\u95EE\u4E0E\u4F1A\u8BDD\u8BBE\u7F6E": "Save access and session settings",
|
||||
|
|
@ -11067,6 +11075,207 @@ function installWeixinStyles() {
|
|||
return () => style.remove();
|
||||
}
|
||||
|
||||
// src/channels/shared/access-grant.mjs
|
||||
var ACCESS_GRANT_VERSION = 1;
|
||||
var ACCESS_PENDING_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
||||
var ACCESS_GRANT_PHONE_MAX_LENGTH = 32;
|
||||
var ACCESS_GRANT_GROUP_JID_PATTERN = /^\d{5,32}@g\.us$/;
|
||||
var CONTROL_CHARACTERS2 = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/;
|
||||
function invalidGrant(message) {
|
||||
const error = new TypeError(message);
|
||||
error.code = "access-grant-invalid";
|
||||
throw error;
|
||||
}
|
||||
function normalizeAccessPhone(value) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) value = String(value);
|
||||
if (typeof value === "bigint") value = String(value);
|
||||
if (typeof value !== "string") return null;
|
||||
let raw = value.trim();
|
||||
if (!raw || CONTROL_CHARACTERS2.test(raw)) return null;
|
||||
const at = raw.indexOf("@");
|
||||
if (at > 0) {
|
||||
const user = raw.slice(0, at).split(":")[0];
|
||||
const server = raw.slice(at + 1).toLowerCase();
|
||||
if (!["s.whatsapp.net", "c.us", "hosted"].includes(server)) return null;
|
||||
raw = user;
|
||||
}
|
||||
const digits = raw.replace(/[^\d]/g, "");
|
||||
if (digits.length < 5 || digits.length > ACCESS_GRANT_PHONE_MAX_LENGTH) return null;
|
||||
return digits;
|
||||
}
|
||||
function validateAccessPhone(value) {
|
||||
const phone = normalizeAccessPhone(value);
|
||||
if (!phone) invalidGrant("\u7535\u8BDD\u53F7\u7801\u65E0\u6548\u3002");
|
||||
return phone;
|
||||
}
|
||||
function validateGroupJid(value) {
|
||||
if (typeof value !== "string" || !ACCESS_GRANT_GROUP_JID_PATTERN.test(value.trim())) {
|
||||
invalidGrant("\u7FA4 JID \u65E0\u6548\u3002");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
function validateMember(input) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||
invalidGrant("\u6210\u5458\u6761\u76EE\u65E0\u6548\u3002");
|
||||
}
|
||||
const phone = validateAccessPhone(input.phone ?? input.id);
|
||||
const canExecuteCommands = input.canExecuteCommands === void 0 ? true : Boolean(input.canExecuteCommands);
|
||||
return Object.freeze({ phone, canExecuteCommands });
|
||||
}
|
||||
function validatePhoneList(input, { emptyMessage, label }) {
|
||||
if (!Array.isArray(input)) invalidGrant(`${label}\u5FC5\u987B\u662F\u6570\u7EC4\u3002`);
|
||||
const phones = input.map((entry) => typeof entry === "string" || typeof entry === "number" || typeof entry === "bigint" ? validateAccessPhone(entry) : validateAccessPhone(entry?.phone ?? entry?.id));
|
||||
if (new Set(phones).size !== phones.length) invalidGrant(`${label}\u4E0D\u80FD\u5305\u542B\u91CD\u590D\u7535\u8BDD\u3002`);
|
||||
if (phones.length === 0 && emptyMessage) invalidGrant(emptyMessage);
|
||||
return Object.freeze(phones);
|
||||
}
|
||||
function validateGroupGrant(input) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||
invalidGrant("\u7FA4\u6388\u6743\u6761\u76EE\u65E0\u6548\u3002");
|
||||
}
|
||||
const admins = validatePhoneList(input.admins ?? [], { label: "\u7FA4\u7BA1\u7406\u5458" });
|
||||
const members = Object.freeze((Array.isArray(input.members) ? input.members : []).map(validateMember));
|
||||
if (new Set(members.map((m) => m.phone)).size !== members.length) {
|
||||
invalidGrant("\u7FA4\u6210\u5458\u4E0D\u80FD\u5305\u542B\u91CD\u590D\u7535\u8BDD\u3002");
|
||||
}
|
||||
const title = typeof input.title === "string" ? input.title.trim().slice(0, 128) : "";
|
||||
return Object.freeze({
|
||||
...title ? { title } : {},
|
||||
admins,
|
||||
members
|
||||
});
|
||||
}
|
||||
function validatePending(input) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||
invalidGrant("\u5F85\u6279\u6761\u76EE\u65E0\u6548\u3002");
|
||||
}
|
||||
const id5 = typeof input.id === "string" ? input.id.trim() : "";
|
||||
if (!/^[A-Za-z0-9_-]{6,64}$/.test(id5)) invalidGrant("\u5F85\u6279\u7F16\u53F7\u65E0\u6548\u3002");
|
||||
const kind = input.kind === "group" ? "group" : input.kind === "direct" ? "direct" : null;
|
||||
if (!kind) invalidGrant("\u5F85\u6279\u573A\u666F\u65E0\u6548\u3002");
|
||||
const phone = input.phone ? validateAccessPhone(input.phone) : "";
|
||||
const lid = typeof input.lid === "string" ? input.lid.trim().slice(0, 128) : "";
|
||||
if (!phone && !lid) invalidGrant("\u5F85\u6279\u7F3A\u5C11\u7535\u8BDD\u6216 LID\u3002");
|
||||
const groupJid = kind === "group" ? validateGroupJid(input.groupJid) : void 0;
|
||||
const pushName = typeof input.pushName === "string" ? input.pushName.trim().slice(0, 128) : "";
|
||||
const requestText = typeof input.requestText === "string" ? input.requestText.trim().slice(0, 500) : "";
|
||||
const createdAt = typeof input.createdAt === "string" && input.createdAt ? input.createdAt : (/* @__PURE__ */ new Date()).toISOString();
|
||||
const unresolved = input.unresolved === true || !phone;
|
||||
const notifyRefs = Array.isArray(input.notifyRefs) ? Object.freeze(input.notifyRefs.map((ref) => Object.freeze({
|
||||
adminPhone: validateAccessPhone(ref.adminPhone),
|
||||
providerMessageId: String(ref.providerMessageId ?? "").trim().slice(0, 128)
|
||||
})).filter((ref) => ref.providerMessageId)) : Object.freeze([]);
|
||||
const resolvedByPhone = input.resolvedByPhone ? validateAccessPhone(input.resolvedByPhone) : void 0;
|
||||
const resolvedAt = typeof input.resolvedAt === "string" ? input.resolvedAt : void 0;
|
||||
const status = ["pending", "approved", "denied", "expired"].includes(input.status) ? input.status : "pending";
|
||||
return Object.freeze({
|
||||
id: id5,
|
||||
kind,
|
||||
...groupJid ? { groupJid } : {},
|
||||
phone: phone || "",
|
||||
...lid ? { lid } : {},
|
||||
...pushName ? { pushName } : {},
|
||||
...requestText ? { requestText } : {},
|
||||
createdAt,
|
||||
unresolved,
|
||||
notifyRefs,
|
||||
status,
|
||||
...resolvedByPhone ? { resolvedByPhone } : {},
|
||||
...resolvedAt ? { resolvedAt } : {}
|
||||
});
|
||||
}
|
||||
function validateContact(input) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||
invalidGrant("\u8054\u7CFB\u4EBA\u6761\u76EE\u65E0\u6548\u3002");
|
||||
}
|
||||
const phone = input.phone ? validateAccessPhone(input.phone) : void 0;
|
||||
const lids = Object.freeze([...Array.isArray(input.lids) ? input.lids : []].map((lid) => String(lid ?? "").trim()).filter((lid) => lid && lid.length <= 128).slice(0, 8));
|
||||
const pushName = typeof input.pushName === "string" ? input.pushName.trim().slice(0, 128) : "";
|
||||
const lastSeenAt = typeof input.lastSeenAt === "string" && input.lastSeenAt ? input.lastSeenAt : (/* @__PURE__ */ new Date()).toISOString();
|
||||
const scenes = Object.freeze([...Array.isArray(input.scenes) ? input.scenes : []].filter((scene) => scene === "direct" || scene === "group").slice(0, 2));
|
||||
const groupJids = Object.freeze([...Array.isArray(input.groupJids) ? input.groupJids : []].map((jid) => String(jid ?? "").trim()).filter((jid) => ACCESS_GRANT_GROUP_JID_PATTERN.test(jid)).slice(0, 32));
|
||||
if (!phone && lids.length === 0) invalidGrant("\u8054\u7CFB\u4EBA\u7F3A\u5C11\u7535\u8BDD\u6216 LID\u3002");
|
||||
return Object.freeze({
|
||||
...phone ? { phone } : {},
|
||||
lids,
|
||||
...pushName ? { pushName } : {},
|
||||
lastSeenAt,
|
||||
scenes: scenes.length > 0 ? scenes : Object.freeze(["direct"]),
|
||||
...groupJids.length > 0 ? { groupJids } : {}
|
||||
});
|
||||
}
|
||||
function validateAccessGrant(input, { requireGlobalAdmin = true } = {}) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||
invalidGrant("\u8BBF\u95EE\u6388\u6743\u6587\u6863\u65E0\u6548\u3002");
|
||||
}
|
||||
const version = input.version === void 0 ? ACCESS_GRANT_VERSION : Number(input.version);
|
||||
if (version !== ACCESS_GRANT_VERSION) invalidGrant("\u8BBF\u95EE\u6388\u6743\u7248\u672C\u4E0D\u652F\u6301\u3002");
|
||||
const globalAdmins = validatePhoneList(input.globalAdmins ?? [], {
|
||||
label: "\u5168\u5C40\u7BA1\u7406\u5458",
|
||||
emptyMessage: requireGlobalAdmin ? "\u81F3\u5C11\u4FDD\u7559\u4E00\u4F4D\u5168\u5C40\u7BA1\u7406\u5458\u3002" : void 0
|
||||
});
|
||||
if (requireGlobalAdmin && globalAdmins.length === 0) {
|
||||
invalidGrant("\u81F3\u5C11\u4FDD\u7559\u4E00\u4F4D\u5168\u5C40\u7BA1\u7406\u5458\u3002");
|
||||
}
|
||||
const directMembers = Object.freeze((Array.isArray(input.directMembers) ? input.directMembers : []).map(validateMember));
|
||||
if (new Set(directMembers.map((m) => m.phone)).size !== directMembers.length) {
|
||||
invalidGrant("\u79C1\u804A\u6388\u6743\u7528\u6237\u4E0D\u80FD\u5305\u542B\u91CD\u590D\u7535\u8BDD\u3002");
|
||||
}
|
||||
const groups = /* @__PURE__ */ Object.create(null);
|
||||
if (input.groups && typeof input.groups === "object" && !Array.isArray(input.groups)) {
|
||||
for (const [groupJid, grant] of Object.entries(input.groups)) {
|
||||
groups[validateGroupJid(groupJid)] = validateGroupGrant(grant);
|
||||
}
|
||||
}
|
||||
const pending = Object.freeze((Array.isArray(input.pending) ? input.pending : []).map(validatePending).slice(0, 200));
|
||||
const contacts = Object.freeze((Array.isArray(input.contacts) ? input.contacts : []).map(validateContact).slice(0, 500));
|
||||
return Object.freeze({
|
||||
version: ACCESS_GRANT_VERSION,
|
||||
globalAdmins,
|
||||
directMembers,
|
||||
groups: Object.freeze(groups),
|
||||
pending,
|
||||
contacts
|
||||
});
|
||||
}
|
||||
function normalizeAccessGrant(input) {
|
||||
try {
|
||||
return validateAccessGrant(input, { requireGlobalAdmin: false });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
var ACCESS_GRANT_COPY = Object.freeze({
|
||||
pendingAckDirect: "\u5DF2\u63D0\u4EA4\u79C1\u804A\u8BBF\u95EE\u7533\u8BF7\uFF0C\u8BF7\u7B49\u5F85\u5168\u5C40\u7BA1\u7406\u5458\u5BA1\u6279\u3002",
|
||||
pendingAckGroup: "\u5DF2\u63D0\u4EA4\u672C\u7FA4\u8BBF\u95EE\u7533\u8BF7\uFF0C\u8BF7\u7B49\u5F85\u7BA1\u7406\u5458\u5BA1\u6279\u3002",
|
||||
pendingUnresolved: "\u5DF2\u8BB0\u5F55\u8BBF\u95EE\u7533\u8BF7\uFF0C\u4F46\u5C1A\u672A\u89E3\u6790\u5230\u7535\u8BDD\u53F7\u7801\uFF1B\u8BF7\u7BA1\u7406\u5458\u5728\u8BBE\u7F6E\u4E2D\u8865\u5168\u540E\u518D\u6279\u51C6\u3002",
|
||||
notifyTitle: "[dsh-im-ops] \u8BBF\u95EE\u7533\u8BF7",
|
||||
approvedDirect: "\u79C1\u804A\u8BBF\u95EE\u5DF2\u6279\u51C6\uFF0C\u73B0\u5728\u53EF\u4EE5\u76F4\u63A5\u5BF9\u8BDD\u3002",
|
||||
approvedGroup: "\u672C\u7FA4\u8BBF\u95EE\u5DF2\u6279\u51C6\uFF0C\u8BF7\u5728\u672C\u7FA4 @ \u673A\u5668\u4EBA\u7EE7\u7EED\u3002",
|
||||
denied: "\u8BBF\u95EE\u7533\u8BF7\u672A\u901A\u8FC7\u3002",
|
||||
adminApproved: "\u5DF2\u6279\u51C6\u8BE5\u8BBF\u95EE\u7533\u8BF7\u3002",
|
||||
adminDenied: "\u5DF2\u62D2\u7EDD\u8BE5\u8BBF\u95EE\u7533\u8BF7\u3002"
|
||||
});
|
||||
|
||||
// src/channels/shared/session-scope.mjs
|
||||
var GROUP_SESSION_SCOPES = Object.freeze(["chat", "user_in_chat"]);
|
||||
var DEFAULT_GROUP_SESSION_SCOPE = "user_in_chat";
|
||||
function normalizeGroupSessionScope(value) {
|
||||
const scope = String(value ?? "").trim().toLowerCase();
|
||||
if (scope === "chat" || scope === "shared" || scope === "shared_chat") return "chat";
|
||||
if (scope === "user_in_chat" || scope === "user" || scope === "per_user" || scope === "member") {
|
||||
return "user_in_chat";
|
||||
}
|
||||
return DEFAULT_GROUP_SESSION_SCOPE;
|
||||
}
|
||||
function validateGroupSessionScope(value) {
|
||||
const scope = String(value ?? "").trim().toLowerCase();
|
||||
if (scope === "chat" || scope === "user_in_chat") return scope;
|
||||
const error = new TypeError(`Invalid group session scope: ${String(value)}`);
|
||||
error.code = "invalid-group-session-scope";
|
||||
throw error;
|
||||
}
|
||||
|
||||
// plugin-src/client/channels/whatsapp/api.js
|
||||
var WHATSAPP_RPC_CHANNEL = "/whatsapp";
|
||||
var WHATSAPP_ENDPOINTS = Object.freeze({
|
||||
|
|
@ -11077,6 +11286,9 @@ var WHATSAPP_ENDPOINTS = Object.freeze({
|
|||
reconnectBot: "bot.reconnect",
|
||||
deleteBot: "bot.delete",
|
||||
setAccessPolicy: "bot.access-policy.set",
|
||||
setAccessGrant: "bot.access-grant.set",
|
||||
resolveAccessPending: "bot.access-pending.resolve",
|
||||
setGroupSessionScope: "bot.group-session-scope.set",
|
||||
setWorkspace: "bot.workspace.set",
|
||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||
setContextEnhancement: "bot.context-enhancement.set"
|
||||
|
|
@ -11147,6 +11359,8 @@ function normalizeBot6(value) {
|
|||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
||||
...Object.hasOwn(value, "accessPolicy") ? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) } : {},
|
||||
...Object.hasOwn(value, "accessGrant") ? { accessGrant: normalizeAccessGrant(value.accessGrant) } : {},
|
||||
...Object.hasOwn(value, "groupSessionScope") ? { groupSessionScope: normalizeGroupSessionScope(value.groupSessionScope) } : {},
|
||||
bot: {
|
||||
name: text5(value.bot?.name, "WhatsApp\u673A\u5668\u4EBA", 100),
|
||||
idMasked: text5(value.bot?.idMasked, "WhatsApp\u8D26\u53F7", 140)
|
||||
|
|
@ -11840,27 +12054,6 @@ var React22 = __toESM(require("react"), 1);
|
|||
|
||||
// plugin-src/client/access-policy-settings.js
|
||||
var React20 = __toESM(require("react"), 1);
|
||||
|
||||
// src/channels/shared/session-scope.mjs
|
||||
var GROUP_SESSION_SCOPES = Object.freeze(["chat", "user_in_chat"]);
|
||||
var DEFAULT_GROUP_SESSION_SCOPE = "user_in_chat";
|
||||
function normalizeGroupSessionScope(value) {
|
||||
const scope = String(value ?? "").trim().toLowerCase();
|
||||
if (scope === "chat" || scope === "shared" || scope === "shared_chat") return "chat";
|
||||
if (scope === "user_in_chat" || scope === "user" || scope === "per_user" || scope === "member") {
|
||||
return "user_in_chat";
|
||||
}
|
||||
return DEFAULT_GROUP_SESSION_SCOPE;
|
||||
}
|
||||
function validateGroupSessionScope(value) {
|
||||
const scope = String(value ?? "").trim().toLowerCase();
|
||||
if (scope === "chat" || scope === "user_in_chat") return scope;
|
||||
const error = new TypeError(`Invalid group session scope: ${String(value)}`);
|
||||
error.code = "invalid-group-session-scope";
|
||||
throw error;
|
||||
}
|
||||
|
||||
// plugin-src/client/access-policy-settings.js
|
||||
var ACCESS_POLICY_ENDPOINT = "bot.access-policy.set";
|
||||
var GROUP_SESSION_SCOPE_ENDPOINT = "bot.group-session-scope.set";
|
||||
var ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
|
||||
|
|
@ -12323,190 +12516,6 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
|
|||
|
||||
// plugin-src/client/access-grant-settings.js
|
||||
var React21 = __toESM(require("react"), 1);
|
||||
|
||||
// src/channels/shared/access-grant.mjs
|
||||
var ACCESS_GRANT_VERSION = 1;
|
||||
var ACCESS_PENDING_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
||||
var ACCESS_GRANT_PHONE_MAX_LENGTH = 32;
|
||||
var ACCESS_GRANT_GROUP_JID_PATTERN = /^\d{5,32}@g\.us$/;
|
||||
var CONTROL_CHARACTERS2 = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/;
|
||||
function invalidGrant(message) {
|
||||
const error = new TypeError(message);
|
||||
error.code = "access-grant-invalid";
|
||||
throw error;
|
||||
}
|
||||
function normalizeAccessPhone(value) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) value = String(value);
|
||||
if (typeof value === "bigint") value = String(value);
|
||||
if (typeof value !== "string") return null;
|
||||
let raw = value.trim();
|
||||
if (!raw || CONTROL_CHARACTERS2.test(raw)) return null;
|
||||
const at = raw.indexOf("@");
|
||||
if (at > 0) {
|
||||
const user = raw.slice(0, at).split(":")[0];
|
||||
const server = raw.slice(at + 1).toLowerCase();
|
||||
if (!["s.whatsapp.net", "c.us", "hosted"].includes(server)) return null;
|
||||
raw = user;
|
||||
}
|
||||
const digits = raw.replace(/[^\d]/g, "");
|
||||
if (digits.length < 5 || digits.length > ACCESS_GRANT_PHONE_MAX_LENGTH) return null;
|
||||
return digits;
|
||||
}
|
||||
function validateAccessPhone(value) {
|
||||
const phone = normalizeAccessPhone(value);
|
||||
if (!phone) invalidGrant("\u7535\u8BDD\u53F7\u7801\u65E0\u6548\u3002");
|
||||
return phone;
|
||||
}
|
||||
function validateGroupJid(value) {
|
||||
if (typeof value !== "string" || !ACCESS_GRANT_GROUP_JID_PATTERN.test(value.trim())) {
|
||||
invalidGrant("\u7FA4 JID \u65E0\u6548\u3002");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
function validateMember(input) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||
invalidGrant("\u6210\u5458\u6761\u76EE\u65E0\u6548\u3002");
|
||||
}
|
||||
const phone = validateAccessPhone(input.phone ?? input.id);
|
||||
const canExecuteCommands = input.canExecuteCommands === void 0 ? true : Boolean(input.canExecuteCommands);
|
||||
return Object.freeze({ phone, canExecuteCommands });
|
||||
}
|
||||
function validatePhoneList(input, { emptyMessage, label }) {
|
||||
if (!Array.isArray(input)) invalidGrant(`${label}\u5FC5\u987B\u662F\u6570\u7EC4\u3002`);
|
||||
const phones = input.map((entry) => typeof entry === "string" || typeof entry === "number" || typeof entry === "bigint" ? validateAccessPhone(entry) : validateAccessPhone(entry?.phone ?? entry?.id));
|
||||
if (new Set(phones).size !== phones.length) invalidGrant(`${label}\u4E0D\u80FD\u5305\u542B\u91CD\u590D\u7535\u8BDD\u3002`);
|
||||
if (phones.length === 0 && emptyMessage) invalidGrant(emptyMessage);
|
||||
return Object.freeze(phones);
|
||||
}
|
||||
function validateGroupGrant(input) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||
invalidGrant("\u7FA4\u6388\u6743\u6761\u76EE\u65E0\u6548\u3002");
|
||||
}
|
||||
const admins = validatePhoneList(input.admins ?? [], { label: "\u7FA4\u7BA1\u7406\u5458" });
|
||||
const members = Object.freeze((Array.isArray(input.members) ? input.members : []).map(validateMember));
|
||||
if (new Set(members.map((m) => m.phone)).size !== members.length) {
|
||||
invalidGrant("\u7FA4\u6210\u5458\u4E0D\u80FD\u5305\u542B\u91CD\u590D\u7535\u8BDD\u3002");
|
||||
}
|
||||
const title = typeof input.title === "string" ? input.title.trim().slice(0, 128) : "";
|
||||
return Object.freeze({
|
||||
...title ? { title } : {},
|
||||
admins,
|
||||
members
|
||||
});
|
||||
}
|
||||
function validatePending(input) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||
invalidGrant("\u5F85\u6279\u6761\u76EE\u65E0\u6548\u3002");
|
||||
}
|
||||
const id5 = typeof input.id === "string" ? input.id.trim() : "";
|
||||
if (!/^[A-Za-z0-9_-]{6,64}$/.test(id5)) invalidGrant("\u5F85\u6279\u7F16\u53F7\u65E0\u6548\u3002");
|
||||
const kind = input.kind === "group" ? "group" : input.kind === "direct" ? "direct" : null;
|
||||
if (!kind) invalidGrant("\u5F85\u6279\u573A\u666F\u65E0\u6548\u3002");
|
||||
const phone = input.phone ? validateAccessPhone(input.phone) : "";
|
||||
const lid = typeof input.lid === "string" ? input.lid.trim().slice(0, 128) : "";
|
||||
if (!phone && !lid) invalidGrant("\u5F85\u6279\u7F3A\u5C11\u7535\u8BDD\u6216 LID\u3002");
|
||||
const groupJid = kind === "group" ? validateGroupJid(input.groupJid) : void 0;
|
||||
const pushName = typeof input.pushName === "string" ? input.pushName.trim().slice(0, 128) : "";
|
||||
const requestText = typeof input.requestText === "string" ? input.requestText.trim().slice(0, 500) : "";
|
||||
const createdAt = typeof input.createdAt === "string" && input.createdAt ? input.createdAt : (/* @__PURE__ */ new Date()).toISOString();
|
||||
const unresolved = input.unresolved === true || !phone;
|
||||
const notifyRefs = Array.isArray(input.notifyRefs) ? Object.freeze(input.notifyRefs.map((ref) => Object.freeze({
|
||||
adminPhone: validateAccessPhone(ref.adminPhone),
|
||||
providerMessageId: String(ref.providerMessageId ?? "").trim().slice(0, 128)
|
||||
})).filter((ref) => ref.providerMessageId)) : Object.freeze([]);
|
||||
const resolvedByPhone = input.resolvedByPhone ? validateAccessPhone(input.resolvedByPhone) : void 0;
|
||||
const resolvedAt = typeof input.resolvedAt === "string" ? input.resolvedAt : void 0;
|
||||
const status = ["pending", "approved", "denied", "expired"].includes(input.status) ? input.status : "pending";
|
||||
return Object.freeze({
|
||||
id: id5,
|
||||
kind,
|
||||
...groupJid ? { groupJid } : {},
|
||||
phone: phone || "",
|
||||
...lid ? { lid } : {},
|
||||
...pushName ? { pushName } : {},
|
||||
...requestText ? { requestText } : {},
|
||||
createdAt,
|
||||
unresolved,
|
||||
notifyRefs,
|
||||
status,
|
||||
...resolvedByPhone ? { resolvedByPhone } : {},
|
||||
...resolvedAt ? { resolvedAt } : {}
|
||||
});
|
||||
}
|
||||
function validateContact(input) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||
invalidGrant("\u8054\u7CFB\u4EBA\u6761\u76EE\u65E0\u6548\u3002");
|
||||
}
|
||||
const phone = input.phone ? validateAccessPhone(input.phone) : void 0;
|
||||
const lids = Object.freeze([...Array.isArray(input.lids) ? input.lids : []].map((lid) => String(lid ?? "").trim()).filter((lid) => lid && lid.length <= 128).slice(0, 8));
|
||||
const pushName = typeof input.pushName === "string" ? input.pushName.trim().slice(0, 128) : "";
|
||||
const lastSeenAt = typeof input.lastSeenAt === "string" && input.lastSeenAt ? input.lastSeenAt : (/* @__PURE__ */ new Date()).toISOString();
|
||||
const scenes = Object.freeze([...Array.isArray(input.scenes) ? input.scenes : []].filter((scene) => scene === "direct" || scene === "group").slice(0, 2));
|
||||
const groupJids = Object.freeze([...Array.isArray(input.groupJids) ? input.groupJids : []].map((jid) => String(jid ?? "").trim()).filter((jid) => ACCESS_GRANT_GROUP_JID_PATTERN.test(jid)).slice(0, 32));
|
||||
if (!phone && lids.length === 0) invalidGrant("\u8054\u7CFB\u4EBA\u7F3A\u5C11\u7535\u8BDD\u6216 LID\u3002");
|
||||
return Object.freeze({
|
||||
...phone ? { phone } : {},
|
||||
lids,
|
||||
...pushName ? { pushName } : {},
|
||||
lastSeenAt,
|
||||
scenes: scenes.length > 0 ? scenes : Object.freeze(["direct"]),
|
||||
...groupJids.length > 0 ? { groupJids } : {}
|
||||
});
|
||||
}
|
||||
function validateAccessGrant(input, { requireGlobalAdmin = true } = {}) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||
invalidGrant("\u8BBF\u95EE\u6388\u6743\u6587\u6863\u65E0\u6548\u3002");
|
||||
}
|
||||
const version = input.version === void 0 ? ACCESS_GRANT_VERSION : Number(input.version);
|
||||
if (version !== ACCESS_GRANT_VERSION) invalidGrant("\u8BBF\u95EE\u6388\u6743\u7248\u672C\u4E0D\u652F\u6301\u3002");
|
||||
const globalAdmins = validatePhoneList(input.globalAdmins ?? [], {
|
||||
label: "\u5168\u5C40\u7BA1\u7406\u5458",
|
||||
emptyMessage: requireGlobalAdmin ? "\u81F3\u5C11\u4FDD\u7559\u4E00\u4F4D\u5168\u5C40\u7BA1\u7406\u5458\u3002" : void 0
|
||||
});
|
||||
if (requireGlobalAdmin && globalAdmins.length === 0) {
|
||||
invalidGrant("\u81F3\u5C11\u4FDD\u7559\u4E00\u4F4D\u5168\u5C40\u7BA1\u7406\u5458\u3002");
|
||||
}
|
||||
const directMembers = Object.freeze((Array.isArray(input.directMembers) ? input.directMembers : []).map(validateMember));
|
||||
if (new Set(directMembers.map((m) => m.phone)).size !== directMembers.length) {
|
||||
invalidGrant("\u79C1\u804A\u6388\u6743\u7528\u6237\u4E0D\u80FD\u5305\u542B\u91CD\u590D\u7535\u8BDD\u3002");
|
||||
}
|
||||
const groups = /* @__PURE__ */ Object.create(null);
|
||||
if (input.groups && typeof input.groups === "object" && !Array.isArray(input.groups)) {
|
||||
for (const [groupJid, grant] of Object.entries(input.groups)) {
|
||||
groups[validateGroupJid(groupJid)] = validateGroupGrant(grant);
|
||||
}
|
||||
}
|
||||
const pending = Object.freeze((Array.isArray(input.pending) ? input.pending : []).map(validatePending).slice(0, 200));
|
||||
const contacts = Object.freeze((Array.isArray(input.contacts) ? input.contacts : []).map(validateContact).slice(0, 500));
|
||||
return Object.freeze({
|
||||
version: ACCESS_GRANT_VERSION,
|
||||
globalAdmins,
|
||||
directMembers,
|
||||
groups: Object.freeze(groups),
|
||||
pending,
|
||||
contacts
|
||||
});
|
||||
}
|
||||
function normalizeAccessGrant(input) {
|
||||
try {
|
||||
return validateAccessGrant(input, { requireGlobalAdmin: false });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
var ACCESS_GRANT_COPY = Object.freeze({
|
||||
pendingAckDirect: "\u5DF2\u63D0\u4EA4\u79C1\u804A\u8BBF\u95EE\u7533\u8BF7\uFF0C\u8BF7\u7B49\u5F85\u5168\u5C40\u7BA1\u7406\u5458\u5BA1\u6279\u3002",
|
||||
pendingAckGroup: "\u5DF2\u63D0\u4EA4\u672C\u7FA4\u8BBF\u95EE\u7533\u8BF7\uFF0C\u8BF7\u7B49\u5F85\u7BA1\u7406\u5458\u5BA1\u6279\u3002",
|
||||
pendingUnresolved: "\u5DF2\u8BB0\u5F55\u8BBF\u95EE\u7533\u8BF7\uFF0C\u4F46\u5C1A\u672A\u89E3\u6790\u5230\u7535\u8BDD\u53F7\u7801\uFF1B\u8BF7\u7BA1\u7406\u5458\u5728\u8BBE\u7F6E\u4E2D\u8865\u5168\u540E\u518D\u6279\u51C6\u3002",
|
||||
notifyTitle: "[dsh-im-ops] \u8BBF\u95EE\u7533\u8BF7",
|
||||
approvedDirect: "\u79C1\u804A\u8BBF\u95EE\u5DF2\u6279\u51C6\uFF0C\u73B0\u5728\u53EF\u4EE5\u76F4\u63A5\u5BF9\u8BDD\u3002",
|
||||
approvedGroup: "\u672C\u7FA4\u8BBF\u95EE\u5DF2\u6279\u51C6\uFF0C\u8BF7\u5728\u672C\u7FA4 @ \u673A\u5668\u4EBA\u7EE7\u7EED\u3002",
|
||||
denied: "\u8BBF\u95EE\u7533\u8BF7\u672A\u901A\u8FC7\u3002",
|
||||
adminApproved: "\u5DF2\u6279\u51C6\u8BE5\u8BBF\u95EE\u7533\u8BF7\u3002",
|
||||
adminDenied: "\u5DF2\u62D2\u7EDD\u8BE5\u8BBF\u95EE\u7533\u8BF7\u3002"
|
||||
});
|
||||
|
||||
// plugin-src/client/access-grant-settings.js
|
||||
var ACCESS_GRANT_ENDPOINT = "bot.access-grant.set";
|
||||
var ACCESS_PENDING_RESOLVE_ENDPOINT = "bot.access-pending.resolve";
|
||||
var GROUP_SESSION_SCOPE_ENDPOINT2 = "bot.group-session-scope.set";
|
||||
|
|
@ -12978,16 +12987,81 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
|||
"fieldset",
|
||||
{ className: "dim-accessScene", disabled: saving },
|
||||
h2("legend", null, "\u6700\u8FD1\u8054\u7CFB\u4EBA\uFF08\u81EA\u52A8\u6C89\u6DC0\uFF09"),
|
||||
contacts.length === 0 ? h2("div", { className: "dim-accessUsersEmpty" }, "\u6682\u65E0\u8054\u7CFB\u4EBA\u3002\u79C1\u804A\u6216\u7FA4\u5185\u89E6\u53D1\u540E\u4F1A\u51FA\u73B0\u5728\u6B64\uFF0C\u4FBF\u4E8E\u8865\u9F50\u7535\u8BDD\u4E0E\u6635\u79F0\u3002") : h2("ul", { className: "dim-accessUserList" }, contacts.slice(0, 30).map((contact) => h2(
|
||||
h2(
|
||||
"p",
|
||||
{ className: "dim-accessUsersEmpty" },
|
||||
"\u4EC5\u8BB0\u5F55\u79C1\u804A\u673A\u5668\u4EBA\uFF0C\u6216\u5728\u7FA4\u91CC @ \u673A\u5668\u4EBA\u7684\u4EBA\u3002\u7535\u8BDD\u662F\u552F\u4E00\u6388\u6743\u952E\uFF0C\u53EF\u4E00\u952E\u52A0\u5165\u79C1\u804A/\u672C\u7FA4\u6388\u6743\u3002"
|
||||
),
|
||||
contacts.length === 0 ? h2("div", { className: "dim-accessUsersEmpty" }, "\u6682\u65E0\u8054\u7CFB\u4EBA\u3002\u6709\u4EBA\u79C1\u804A\u6216 @ \u673A\u5668\u4EBA\u540E\u4F1A\u51FA\u73B0\u5728\u6B64\u3002") : h2("ul", { className: "dim-accessUserList" }, contacts.slice(0, 30).map((contact) => {
|
||||
const phone = contact.phone || "";
|
||||
const alreadyDirect = phone && draft.directMembers.some((m) => m.phone === phone);
|
||||
const groupTargets = (contact.groupJids ?? []).filter((jid) => knownGroupJids.includes(jid));
|
||||
return h2(
|
||||
"li",
|
||||
{
|
||||
key: `${contact.phone ?? ""}-${(contact.lids ?? []).join(",")}`,
|
||||
className: "dim-accessUserRow"
|
||||
},
|
||||
h2("span", null, contactLabel(contact)),
|
||||
h2(
|
||||
"div",
|
||||
{ className: "dim-accessField" },
|
||||
h2("strong", null, contactLabel(contact)),
|
||||
h2("span", null, (contact.scenes ?? []).join(" / ")),
|
||||
contact.phone ? null : h2("span", null, "\u5F85\u8865\u7535\u8BDD")
|
||||
)))
|
||||
phone ? null : h2("span", null, "\u5F85\u8865\u7535\u8BDD")
|
||||
),
|
||||
phone ? h2("button", {
|
||||
type: "button",
|
||||
className: "dim-deliveryButton",
|
||||
"data-kind": "primary",
|
||||
disabled: saving || alreadyDirect,
|
||||
onClick: () => {
|
||||
setDraft((current) => {
|
||||
if (current.directMembers.some((m) => m.phone === phone)) return current;
|
||||
return {
|
||||
...current,
|
||||
directMembers: [
|
||||
...current.directMembers,
|
||||
{ phone, canExecuteCommands: true }
|
||||
]
|
||||
};
|
||||
});
|
||||
setFeedback({ tone: "success", message: "\u5DF2\u52A0\u5165\u79C1\u804A\u6388\u6743\uFF0C\u8BB0\u5F97\u70B9\u4FDD\u5B58\u3002" });
|
||||
}
|
||||
}, alreadyDirect ? "\u5DF2\u5728\u79C1\u804A" : "\u52A0\u79C1\u804A") : null,
|
||||
...groupTargets.map((groupJid) => {
|
||||
const group = draft.groups[groupJid] ?? { admins: [], members: [] };
|
||||
const already = group.admins.includes(phone) || group.members.some((m) => m.phone === phone);
|
||||
return h2("button", {
|
||||
key: `add-${groupJid}`,
|
||||
type: "button",
|
||||
className: "dim-deliveryButton",
|
||||
disabled: saving || !phone || already,
|
||||
onClick: () => {
|
||||
setDraft((current) => {
|
||||
const existing = current.groups[groupJid] ?? { title: "", admins: [], members: [] };
|
||||
if (existing.admins.includes(phone) || existing.members.some((m) => m.phone === phone)) {
|
||||
return current;
|
||||
}
|
||||
return {
|
||||
...current,
|
||||
groups: {
|
||||
...current.groups,
|
||||
[groupJid]: {
|
||||
...existing,
|
||||
members: [
|
||||
...existing.members,
|
||||
{ phone, canExecuteCommands: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
setFeedback({ tone: "success", message: "\u5DF2\u52A0\u5165\u672C\u7FA4\u6388\u6743\uFF0C\u8BB0\u5F97\u70B9\u4FDD\u5B58\u3002" });
|
||||
}
|
||||
}, already ? "\u5DF2\u5728\u672C\u7FA4" : "\u52A0\u672C\u7FA4");
|
||||
})
|
||||
);
|
||||
}))
|
||||
),
|
||||
h2(
|
||||
"fieldset",
|
||||
|
|
|
|||
418
lib/index.js
418
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "dsh-im-ops",
|
||||
"version": "4.9.1-ops.5",
|
||||
"version": "4.9.1-ops.6",
|
||||
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 — access/session policies owned here.",
|
||||
"keywords": [
|
||||
"deepseek-harness",
|
||||
|
|
|
|||
|
|
@ -457,16 +457,76 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
|
|||
}, '添加群'))),
|
||||
h('fieldset', { className: 'dim-accessScene', disabled: saving },
|
||||
h('legend', null, '最近联系人(自动沉淀)'),
|
||||
h('p', { className: 'dim-accessUsersEmpty' },
|
||||
'仅记录私聊机器人,或在群里 @ 机器人的人。电话是唯一授权键,可一键加入私聊/本群授权。'),
|
||||
contacts.length === 0
|
||||
? h('div', { className: 'dim-accessUsersEmpty' }, '暂无联系人。私聊或群内触发后会出现在此,便于补齐电话与昵称。')
|
||||
: h('ul', { className: 'dim-accessUserList' }, contacts.slice(0, 30).map((contact) =>
|
||||
h('li', {
|
||||
? h('div', { className: 'dim-accessUsersEmpty' }, '暂无联系人。有人私聊或 @ 机器人后会出现在此。')
|
||||
: h('ul', { className: 'dim-accessUserList' }, contacts.slice(0, 30).map((contact) => {
|
||||
const phone = contact.phone || '';
|
||||
const alreadyDirect = phone && draft.directMembers.some((m) => m.phone === phone);
|
||||
const groupTargets = (contact.groupJids ?? []).filter((jid) => knownGroupJids.includes(jid));
|
||||
return h('li', {
|
||||
key: `${contact.phone ?? ''}-${(contact.lids ?? []).join(',')}`,
|
||||
className: 'dim-accessUserRow',
|
||||
},
|
||||
h('span', null, contactLabel(contact)),
|
||||
h('div', { className: 'dim-accessField' },
|
||||
h('strong', null, contactLabel(contact)),
|
||||
h('span', null, (contact.scenes ?? []).join(' / ')),
|
||||
contact.phone ? null : h('span', null, '待补电话'))))),
|
||||
phone ? null : h('span', null, '待补电话')),
|
||||
phone ? h('button', {
|
||||
type: 'button',
|
||||
className: 'dim-deliveryButton',
|
||||
'data-kind': 'primary',
|
||||
disabled: saving || alreadyDirect,
|
||||
onClick: () => {
|
||||
setDraft((current) => {
|
||||
if (current.directMembers.some((m) => m.phone === phone)) return current;
|
||||
return {
|
||||
...current,
|
||||
directMembers: [
|
||||
...current.directMembers,
|
||||
{ phone, canExecuteCommands: true },
|
||||
],
|
||||
};
|
||||
});
|
||||
setFeedback({ tone: 'success', message: '已加入私聊授权,记得点保存。' });
|
||||
},
|
||||
}, alreadyDirect ? '已在私聊' : '加私聊') : null,
|
||||
...groupTargets.map((groupJid) => {
|
||||
const group = draft.groups[groupJid] ?? { admins: [], members: [] };
|
||||
const already = group.admins.includes(phone)
|
||||
|| group.members.some((m) => m.phone === phone);
|
||||
return h('button', {
|
||||
key: `add-${groupJid}`,
|
||||
type: 'button',
|
||||
className: 'dim-deliveryButton',
|
||||
disabled: saving || !phone || already,
|
||||
onClick: () => {
|
||||
setDraft((current) => {
|
||||
const existing = current.groups[groupJid] ?? { title: '', admins: [], members: [] };
|
||||
if (existing.admins.includes(phone)
|
||||
|| existing.members.some((m) => m.phone === phone)) {
|
||||
return current;
|
||||
}
|
||||
return {
|
||||
...current,
|
||||
groups: {
|
||||
...current.groups,
|
||||
[groupJid]: {
|
||||
...existing,
|
||||
members: [
|
||||
...existing.members,
|
||||
{ phone, canExecuteCommands: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
setFeedback({ tone: 'success', message: '已加入本群授权,记得点保存。' });
|
||||
},
|
||||
}, already ? '已在本群' : '加本群');
|
||||
}));
|
||||
}))),
|
||||
h('fieldset', { className: 'dim-accessScene', disabled: saving },
|
||||
h('legend', null, '群会话策略'),
|
||||
h('label', { className: 'dim-accessField' },
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
||||
import { normalizeLastMessageError } from '../../last-message-error.js';
|
||||
import { normalizeAccessGrant } from '../../../../src/channels/shared/access-grant.mjs';
|
||||
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
||||
import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
|
||||
import { normalizeGroupSessionScope } from '../../../../src/channels/shared/session-scope.mjs';
|
||||
|
||||
export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
|
||||
|
||||
|
|
@ -13,6 +15,9 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|||
reconnectBot: 'bot.reconnect',
|
||||
deleteBot: 'bot.delete',
|
||||
setAccessPolicy: 'bot.access-policy.set',
|
||||
setAccessGrant: 'bot.access-grant.set',
|
||||
resolveAccessPending: 'bot.access-pending.resolve',
|
||||
setGroupSessionScope: 'bot.group-session-scope.set',
|
||||
setWorkspace: 'bot.workspace.set',
|
||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||
setContextEnhancement: 'bot.context-enhancement.set',
|
||||
|
|
@ -95,6 +100,12 @@ function normalizeBot(value) {
|
|||
...(Object.hasOwn(value, 'accessPolicy')
|
||||
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
||||
: {}),
|
||||
...(Object.hasOwn(value, 'accessGrant')
|
||||
? { accessGrant: normalizeAccessGrant(value.accessGrant) }
|
||||
: {}),
|
||||
...(Object.hasOwn(value, 'groupSessionScope')
|
||||
? { groupSessionScope: normalizeGroupSessionScope(value.groupSessionScope) }
|
||||
: {}),
|
||||
bot: {
|
||||
name: text(value.bot?.name, 'WhatsApp机器人', 100),
|
||||
idMasked: text(value.bot?.idMasked, 'WhatsApp账号', 140),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,22 @@ const EN = Object.freeze({
|
|||
'机器人设置页签': 'Bot settings tabs',
|
||||
'投递设置': 'Delivery settings',
|
||||
'访问设置': 'Access settings',
|
||||
"加本群": "Add to group",
|
||||
|
||||
"已在本群": "Already in group",
|
||||
|
||||
"加私聊": "Add to DMs",
|
||||
|
||||
"已在私聊": "Already in DMs",
|
||||
|
||||
"已加入本群授权,记得点保存。": "Added to this group. Remember to save.",
|
||||
|
||||
"已加入私聊授权,记得点保存。": "Added to DM access. Remember to save.",
|
||||
|
||||
"暂无联系人。有人私聊或 @ 机器人后会出现在此。": "No contacts yet. They appear here after someone DMs or @mentions the bot.",
|
||||
|
||||
"仅记录私聊机器人,或在群里 @ 机器人的人。电话是唯一授权键,可一键加入私聊/本群授权。": "Only people who DM the bot or @mention it in a group are recorded. Phone is the sole auth key; use one-click to grant DM or group access.",
|
||||
|
||||
"WhatsApp 按电话号码授权:全局管理员管私聊;群管理员只批本群。成员不可跨群、有群权也不自动获得私聊权。": "WhatsApp authorizes by phone number: global admins manage DMs; group admins approve only their group. Members do not cross groups, and group access does not grant DMs.",
|
||||
"保存分级访问设置": "Save graded access settings",
|
||||
"保存访问与会话设置": "Save access and session settings",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
attachPendingNotifyRefs,
|
||||
emptyAccessGrant,
|
||||
enqueueAccessPending,
|
||||
ensureGroupBucket,
|
||||
ensureOwnerGlobalAdmin,
|
||||
evaluateAccessGrant,
|
||||
findPendingByNotifyMessageId,
|
||||
|
|
@ -96,7 +97,9 @@ export async function loadWhatsappAccessGrant({
|
|||
* Persist contact sighting and return updated grant.
|
||||
*/
|
||||
export async function rememberWhatsappContact(workspaces, botId, grant, message, { phone, lid }) {
|
||||
if (!phone && !lid) return grant;
|
||||
const pushName = message.contextSource?.()?.senderName;
|
||||
try {
|
||||
const next = upsertAccessContact(grant, {
|
||||
phone,
|
||||
lid,
|
||||
|
|
@ -106,6 +109,10 @@ export async function rememberWhatsappContact(workspaces, botId, grant, message,
|
|||
});
|
||||
await workspaces.setAccessGrant(botId, next);
|
||||
return next;
|
||||
} catch {
|
||||
// Contact directory is best-effort and must not block chat.
|
||||
return grant;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -189,9 +196,25 @@ export async function gateWhatsappInbound({
|
|||
accountJid,
|
||||
}) {
|
||||
const { phone, lid } = resolveInboundPhone(message);
|
||||
let current = await rememberWhatsappContact(workspaces, botId, grant, message, { phone, lid });
|
||||
|
||||
const scene = message.kind === 'group' ? 'group' : 'direct';
|
||||
const addressed = scene === 'direct' || message.addressed === true;
|
||||
let current = grant;
|
||||
|
||||
// Only people who DM the bot or @ it in a group enter the contact directory.
|
||||
if (addressed) {
|
||||
current = await rememberWhatsappContact(workspaces, botId, current, message, { phone, lid });
|
||||
}
|
||||
if (scene === 'group' && addressed && message.conversationId) {
|
||||
const title = message.contextSource?.()?.conversationTitle;
|
||||
const withGroup = ensureGroupBucket(current, message.conversationId, {
|
||||
...(title ? { title } : {}),
|
||||
});
|
||||
if (withGroup !== current) {
|
||||
current = withGroup;
|
||||
await workspaces.setAccessGrant(botId, current);
|
||||
}
|
||||
}
|
||||
|
||||
const isCommand = isSharedLocalCommand(message.content ?? '', {
|
||||
hasImages: Array.isArray(message.images) && message.images.length > 0,
|
||||
hasFiles: Array.isArray(message.files) && message.files.length > 0,
|
||||
|
|
@ -228,7 +251,7 @@ export async function gateWhatsappInbound({
|
|||
}
|
||||
|
||||
// Unaddressed group spam: do not create pending.
|
||||
if (scene === 'group' && message.addressed !== true) {
|
||||
if (!addressed) {
|
||||
return { allowed: false, reason: 'group-unaddressed', grant: current };
|
||||
}
|
||||
|
||||
|
|
|
|||
57
test/channels/whatsapp/access-grant-client.test.mjs
Normal file
57
test/channels/whatsapp/access-grant-client.test.mjs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { normalizeSnapshot } from '../../../plugin-src/client/channels/whatsapp/api.js';
|
||||
|
||||
test('WhatsApp normalizeBot keeps accessGrant, groupSessionScope, and contacts', () => {
|
||||
const snapshot = normalizeSnapshot({
|
||||
revision: 1,
|
||||
bots: [{
|
||||
botId: 'wa-1',
|
||||
connected: true,
|
||||
state: 'connected',
|
||||
workspace: '/tmp/ws',
|
||||
groupSessionScope: 'user_in_chat',
|
||||
accessGrant: {
|
||||
version: 1,
|
||||
globalAdmins: ['8618111111111'],
|
||||
directMembers: [{ phone: '8618222222222', canExecuteCommands: true }],
|
||||
groups: {
|
||||
'120363111111111111@g.us': {
|
||||
title: 'Ops',
|
||||
admins: [],
|
||||
members: [{ phone: '8618333333333', canExecuteCommands: true }],
|
||||
},
|
||||
},
|
||||
pending: [{
|
||||
id: 'p_test01',
|
||||
kind: 'group',
|
||||
groupJid: '120363111111111111@g.us',
|
||||
phone: '8618444444444',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
status: 'pending',
|
||||
unresolved: false,
|
||||
notifyRefs: [],
|
||||
}],
|
||||
contacts: [{
|
||||
phone: '8618444444444',
|
||||
lids: [],
|
||||
pushName: 'Alice',
|
||||
lastSeenAt: '2026-01-01T00:00:00.000Z',
|
||||
scenes: ['group'],
|
||||
groupJids: ['120363111111111111@g.us'],
|
||||
}],
|
||||
},
|
||||
bot: { name: 'Bot', idMasked: '+86***' },
|
||||
health: { summary: 'ok' },
|
||||
}],
|
||||
});
|
||||
|
||||
const bot = snapshot.bots[0];
|
||||
assert.equal(bot.groupSessionScope, 'user_in_chat');
|
||||
assert.deepEqual(bot.accessGrant.globalAdmins, ['8618111111111']);
|
||||
assert.equal(bot.accessGrant.directMembers[0].phone, '8618222222222');
|
||||
assert.ok(bot.accessGrant.groups['120363111111111111@g.us']);
|
||||
assert.equal(bot.accessGrant.contacts[0].phone, '8618444444444');
|
||||
assert.equal(bot.accessGrant.pending[0].id, 'p_test01');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue