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
506
lib/client.js
506
lib/client.js
|
|
@ -571,7 +571,7 @@ var React24 = __toESM(require("react"), 1);
|
||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "dsh-im-ops",
|
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.",
|
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 \u2014 access/session policies owned here.",
|
||||||
keywords: [
|
keywords: [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
@ -852,6 +852,14 @@ var EN = Object.freeze({
|
||||||
"\u673A\u5668\u4EBA\u8BBE\u7F6E\u9875\u7B7E": "Bot settings tabs",
|
"\u673A\u5668\u4EBA\u8BBE\u7F6E\u9875\u7B7E": "Bot settings tabs",
|
||||||
"\u6295\u9012\u8BBE\u7F6E": "Delivery settings",
|
"\u6295\u9012\u8BBE\u7F6E": "Delivery settings",
|
||||||
"\u8BBF\u95EE\u8BBE\u7F6E": "Access 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.",
|
"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\u5206\u7EA7\u8BBF\u95EE\u8BBE\u7F6E": "Save graded access settings",
|
||||||
"\u4FDD\u5B58\u8BBF\u95EE\u4E0E\u4F1A\u8BDD\u8BBE\u7F6E": "Save access and session settings",
|
"\u4FDD\u5B58\u8BBF\u95EE\u4E0E\u4F1A\u8BDD\u8BBE\u7F6E": "Save access and session settings",
|
||||||
|
|
@ -11067,6 +11075,207 @@ function installWeixinStyles() {
|
||||||
return () => style.remove();
|
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
|
// plugin-src/client/channels/whatsapp/api.js
|
||||||
var WHATSAPP_RPC_CHANNEL = "/whatsapp";
|
var WHATSAPP_RPC_CHANNEL = "/whatsapp";
|
||||||
var WHATSAPP_ENDPOINTS = Object.freeze({
|
var WHATSAPP_ENDPOINTS = Object.freeze({
|
||||||
|
|
@ -11077,6 +11286,9 @@ var WHATSAPP_ENDPOINTS = Object.freeze({
|
||||||
reconnectBot: "bot.reconnect",
|
reconnectBot: "bot.reconnect",
|
||||||
deleteBot: "bot.delete",
|
deleteBot: "bot.delete",
|
||||||
setAccessPolicy: "bot.access-policy.set",
|
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",
|
setWorkspace: "bot.workspace.set",
|
||||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||||
setContextEnhancement: "bot.context-enhancement.set"
|
setContextEnhancement: "bot.context-enhancement.set"
|
||||||
|
|
@ -11147,6 +11359,8 @@ function normalizeBot6(value) {
|
||||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||||
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
||||||
...Object.hasOwn(value, "accessPolicy") ? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) } : {},
|
...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: {
|
bot: {
|
||||||
name: text5(value.bot?.name, "WhatsApp\u673A\u5668\u4EBA", 100),
|
name: text5(value.bot?.name, "WhatsApp\u673A\u5668\u4EBA", 100),
|
||||||
idMasked: text5(value.bot?.idMasked, "WhatsApp\u8D26\u53F7", 140)
|
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
|
// plugin-src/client/access-policy-settings.js
|
||||||
var React20 = __toESM(require("react"), 1);
|
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 ACCESS_POLICY_ENDPOINT = "bot.access-policy.set";
|
||||||
var GROUP_SESSION_SCOPE_ENDPOINT = "bot.group-session-scope.set";
|
var GROUP_SESSION_SCOPE_ENDPOINT = "bot.group-session-scope.set";
|
||||||
var ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
|
var ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
|
||||||
|
|
@ -12323,190 +12516,6 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
|
||||||
|
|
||||||
// plugin-src/client/access-grant-settings.js
|
// plugin-src/client/access-grant-settings.js
|
||||||
var React21 = __toESM(require("react"), 1);
|
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_GRANT_ENDPOINT = "bot.access-grant.set";
|
||||||
var ACCESS_PENDING_RESOLVE_ENDPOINT = "bot.access-pending.resolve";
|
var ACCESS_PENDING_RESOLVE_ENDPOINT = "bot.access-pending.resolve";
|
||||||
var GROUP_SESSION_SCOPE_ENDPOINT2 = "bot.group-session-scope.set";
|
var GROUP_SESSION_SCOPE_ENDPOINT2 = "bot.group-session-scope.set";
|
||||||
|
|
@ -12978,16 +12987,81 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
||||||
"fieldset",
|
"fieldset",
|
||||||
{ className: "dim-accessScene", disabled: saving },
|
{ className: "dim-accessScene", disabled: saving },
|
||||||
h2("legend", null, "\u6700\u8FD1\u8054\u7CFB\u4EBA\uFF08\u81EA\u52A8\u6C89\u6DC0\uFF09"),
|
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(
|
||||||
"li",
|
"p",
|
||||||
{
|
{ className: "dim-accessUsersEmpty" },
|
||||||
key: `${contact.phone ?? ""}-${(contact.lids ?? []).join(",")}`,
|
"\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"
|
||||||
className: "dim-accessUserRow"
|
),
|
||||||
},
|
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) => {
|
||||||
h2("span", null, contactLabel(contact)),
|
const phone = contact.phone || "";
|
||||||
h2("span", null, (contact.scenes ?? []).join("/")),
|
const alreadyDirect = phone && draft.directMembers.some((m) => m.phone === phone);
|
||||||
contact.phone ? null : h2("span", null, "\u5F85\u8865\u7535\u8BDD")
|
const groupTargets = (contact.groupJids ?? []).filter((jid) => knownGroupJids.includes(jid));
|
||||||
)))
|
return h2(
|
||||||
|
"li",
|
||||||
|
{
|
||||||
|
key: `${contact.phone ?? ""}-${(contact.lids ?? []).join(",")}`,
|
||||||
|
className: "dim-accessUserRow"
|
||||||
|
},
|
||||||
|
h2(
|
||||||
|
"div",
|
||||||
|
{ className: "dim-accessField" },
|
||||||
|
h2("strong", null, contactLabel(contact)),
|
||||||
|
h2("span", null, (contact.scenes ?? []).join(" / ")),
|
||||||
|
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(
|
h2(
|
||||||
"fieldset",
|
"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",
|
"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.",
|
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 — access/session policies owned here.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
|
||||||
|
|
@ -457,16 +457,76 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
|
||||||
}, '添加群'))),
|
}, '添加群'))),
|
||||||
h('fieldset', { className: 'dim-accessScene', disabled: saving },
|
h('fieldset', { className: 'dim-accessScene', disabled: saving },
|
||||||
h('legend', null, '最近联系人(自动沉淀)'),
|
h('legend', null, '最近联系人(自动沉淀)'),
|
||||||
|
h('p', { className: 'dim-accessUsersEmpty' },
|
||||||
|
'仅记录私聊机器人,或在群里 @ 机器人的人。电话是唯一授权键,可一键加入私聊/本群授权。'),
|
||||||
contacts.length === 0
|
contacts.length === 0
|
||||||
? h('div', { className: 'dim-accessUsersEmpty' }, '暂无联系人。私聊或群内触发后会出现在此,便于补齐电话与昵称。')
|
? h('div', { className: 'dim-accessUsersEmpty' }, '暂无联系人。有人私聊或 @ 机器人后会出现在此。')
|
||||||
: h('ul', { className: 'dim-accessUserList' }, contacts.slice(0, 30).map((contact) =>
|
: h('ul', { className: 'dim-accessUserList' }, contacts.slice(0, 30).map((contact) => {
|
||||||
h('li', {
|
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(',')}`,
|
key: `${contact.phone ?? ''}-${(contact.lids ?? []).join(',')}`,
|
||||||
className: 'dim-accessUserRow',
|
className: 'dim-accessUserRow',
|
||||||
},
|
},
|
||||||
h('span', null, contactLabel(contact)),
|
h('div', { className: 'dim-accessField' },
|
||||||
h('span', null, (contact.scenes ?? []).join('/')),
|
h('strong', null, contactLabel(contact)),
|
||||||
contact.phone ? null : h('span', null, '待补电话'))))),
|
h('span', null, (contact.scenes ?? []).join(' / ')),
|
||||||
|
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('fieldset', { className: 'dim-accessScene', disabled: saving },
|
||||||
h('legend', null, '群会话策略'),
|
h('legend', null, '群会话策略'),
|
||||||
h('label', { className: 'dim-accessField' },
|
h('label', { className: 'dim-accessField' },
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
||||||
import { normalizeLastMessageError } from '../../last-message-error.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 { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
||||||
import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.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';
|
export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
|
||||||
|
|
||||||
|
|
@ -13,6 +15,9 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
||||||
reconnectBot: 'bot.reconnect',
|
reconnectBot: 'bot.reconnect',
|
||||||
deleteBot: 'bot.delete',
|
deleteBot: 'bot.delete',
|
||||||
setAccessPolicy: 'bot.access-policy.set',
|
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',
|
setWorkspace: 'bot.workspace.set',
|
||||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||||
setContextEnhancement: 'bot.context-enhancement.set',
|
setContextEnhancement: 'bot.context-enhancement.set',
|
||||||
|
|
@ -95,6 +100,12 @@ function normalizeBot(value) {
|
||||||
...(Object.hasOwn(value, 'accessPolicy')
|
...(Object.hasOwn(value, 'accessPolicy')
|
||||||
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
||||||
: {}),
|
: {}),
|
||||||
|
...(Object.hasOwn(value, 'accessGrant')
|
||||||
|
? { accessGrant: normalizeAccessGrant(value.accessGrant) }
|
||||||
|
: {}),
|
||||||
|
...(Object.hasOwn(value, 'groupSessionScope')
|
||||||
|
? { groupSessionScope: normalizeGroupSessionScope(value.groupSessionScope) }
|
||||||
|
: {}),
|
||||||
bot: {
|
bot: {
|
||||||
name: text(value.bot?.name, 'WhatsApp机器人', 100),
|
name: text(value.bot?.name, 'WhatsApp机器人', 100),
|
||||||
idMasked: text(value.bot?.idMasked, 'WhatsApp账号', 140),
|
idMasked: text(value.bot?.idMasked, 'WhatsApp账号', 140),
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,22 @@ const EN = Object.freeze({
|
||||||
'机器人设置页签': 'Bot settings tabs',
|
'机器人设置页签': 'Bot settings tabs',
|
||||||
'投递设置': 'Delivery settings',
|
'投递设置': 'Delivery settings',
|
||||||
'访问设置': 'Access 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.",
|
"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 graded access settings",
|
||||||
"保存访问与会话设置": "Save access and session settings",
|
"保存访问与会话设置": "Save access and session settings",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
attachPendingNotifyRefs,
|
attachPendingNotifyRefs,
|
||||||
emptyAccessGrant,
|
emptyAccessGrant,
|
||||||
enqueueAccessPending,
|
enqueueAccessPending,
|
||||||
|
ensureGroupBucket,
|
||||||
ensureOwnerGlobalAdmin,
|
ensureOwnerGlobalAdmin,
|
||||||
evaluateAccessGrant,
|
evaluateAccessGrant,
|
||||||
findPendingByNotifyMessageId,
|
findPendingByNotifyMessageId,
|
||||||
|
|
@ -96,16 +97,22 @@ export async function loadWhatsappAccessGrant({
|
||||||
* Persist contact sighting and return updated grant.
|
* Persist contact sighting and return updated grant.
|
||||||
*/
|
*/
|
||||||
export async function rememberWhatsappContact(workspaces, botId, grant, message, { phone, lid }) {
|
export async function rememberWhatsappContact(workspaces, botId, grant, message, { phone, lid }) {
|
||||||
|
if (!phone && !lid) return grant;
|
||||||
const pushName = message.contextSource?.()?.senderName;
|
const pushName = message.contextSource?.()?.senderName;
|
||||||
const next = upsertAccessContact(grant, {
|
try {
|
||||||
phone,
|
const next = upsertAccessContact(grant, {
|
||||||
lid,
|
phone,
|
||||||
pushName,
|
lid,
|
||||||
scene: message.kind === 'group' ? 'group' : 'direct',
|
pushName,
|
||||||
groupJid: message.kind === 'group' ? message.conversationId : undefined,
|
scene: message.kind === 'group' ? 'group' : 'direct',
|
||||||
});
|
groupJid: message.kind === 'group' ? message.conversationId : undefined,
|
||||||
await workspaces.setAccessGrant(botId, next);
|
});
|
||||||
return next;
|
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,
|
accountJid,
|
||||||
}) {
|
}) {
|
||||||
const { phone, lid } = resolveInboundPhone(message);
|
const { phone, lid } = resolveInboundPhone(message);
|
||||||
let current = await rememberWhatsappContact(workspaces, botId, grant, message, { phone, lid });
|
|
||||||
|
|
||||||
const scene = message.kind === 'group' ? 'group' : 'direct';
|
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 ?? '', {
|
const isCommand = isSharedLocalCommand(message.content ?? '', {
|
||||||
hasImages: Array.isArray(message.images) && message.images.length > 0,
|
hasImages: Array.isArray(message.images) && message.images.length > 0,
|
||||||
hasFiles: Array.isArray(message.files) && message.files.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.
|
// Unaddressed group spam: do not create pending.
|
||||||
if (scene === 'group' && message.addressed !== true) {
|
if (!addressed) {
|
||||||
return { allowed: false, reason: 'group-unaddressed', grant: current };
|
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