mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
Sync WhatsApp group subjects into access UI (4.9.1-ops.10).
Refresh missing/forced group titles via groupMetadata and participating catalog so cards show real names instead of bare JIDs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a169b164a8
commit
f96157da1e
13 changed files with 568 additions and 260 deletions
|
|
@ -571,7 +571,7 @@ var React24 = __toESM(require("react"), 1);
|
|||
// package.json
|
||||
var package_default = {
|
||||
name: "dsh-im-ops",
|
||||
version: "4.9.1-ops.9",
|
||||
version: "4.9.1-ops.10",
|
||||
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,12 @@ 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",
|
||||
"\u8BF7\u63D0\u4EA4\u6709\u6548\u7684\u7FA4\u540D\u540C\u6B65\u8BF7\u6C42\u3002": "Submit a valid group-name sync request.",
|
||||
"\u540C\u6B65\u7FA4\u540D\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002": "Could not sync the group name. Try again later.",
|
||||
"\u672A\u80FD\u4ECE WhatsApp \u8BFB\u53D6\u7FA4\u540D\uFF0C\u8BF7\u786E\u8BA4\u673A\u5668\u4EBA\u5728\u7EBF\u4E14\u4ECD\u5728\u8BE5\u7FA4\u3002": "Could not read the group name from WhatsApp. Confirm the bot is online and still in the group.",
|
||||
"\u5DF2\u540C\u6B65\u7FA4\u540D\uFF1A": "Synced group name: ",
|
||||
"\u540C\u6B65\u7FA4\u540D": "Sync group name",
|
||||
"\u672A\u547D\u540D\u7FA4": "Unnamed group",
|
||||
"\u6682\u65E0\u6635\u79F0\uFF08\u79C1\u804A/@ \u540E\u4F1A\u81EA\u52A8\u8865\u9F50\uFF09": "No nickname yet (filled after DM or @mention)",
|
||||
"\u7FA4\u804A \xB7 ": "Group \xB7 ",
|
||||
"\u3002\u8BB0\u5F97\u70B9\u4FDD\u5B58\u3002": ". Remember to save.",
|
||||
|
|
@ -11309,6 +11315,7 @@ var WHATSAPP_ENDPOINTS = Object.freeze({
|
|||
setAccessPolicy: "bot.access-policy.set",
|
||||
setAccessGrant: "bot.access-grant.set",
|
||||
resolveAccessPending: "bot.access-pending.resolve",
|
||||
refreshAccessGroupTitles: "bot.access-group-titles.refresh",
|
||||
setGroupSessionScope: "bot.group-session-scope.set",
|
||||
setWorkspace: "bot.workspace.set",
|
||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||
|
|
@ -12541,6 +12548,7 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
|
|||
var React21 = __toESM(require("react"), 1);
|
||||
var ACCESS_GRANT_ENDPOINT = "bot.access-grant.set";
|
||||
var ACCESS_PENDING_RESOLVE_ENDPOINT = "bot.access-pending.resolve";
|
||||
var ACCESS_GROUP_TITLES_REFRESH_ENDPOINT = "bot.access-group-titles.refresh";
|
||||
var GROUP_SESSION_SCOPE_ENDPOINT2 = "bot.group-session-scope.set";
|
||||
function unwrapRpcResult11(result) {
|
||||
if (result?.ok === true) return result.value;
|
||||
|
|
@ -12604,10 +12612,7 @@ function NicknameHint({ contacts, phone }) {
|
|||
function groupDisplayName(groupJid, groups) {
|
||||
const title = groups?.[groupJid]?.title;
|
||||
if (typeof title === "string" && title.trim()) return title.trim();
|
||||
const raw = String(groupJid ?? "");
|
||||
const id5 = raw.replace(/@g\.us$/i, "");
|
||||
if (id5.length > 14) return `${id5.slice(0, 10)}\u2026@g.us`;
|
||||
return raw || "\u672A\u77E5\u7FA4";
|
||||
return "\u672A\u547D\u540D\u7FA4";
|
||||
}
|
||||
function digitsOnly(value) {
|
||||
return String(value ?? "").replace(/[^\d]/g, "");
|
||||
|
|
@ -12828,7 +12833,7 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
|||
const value = unwrapRpcResult11(await rpcCall("connection.status", {}));
|
||||
if (cancelled) return;
|
||||
const bot = Array.isArray(value?.bots) ? value.bots.find((entry) => entry?.botId === account.botId) : null;
|
||||
const grant = normalizeAccessGrant(bot?.accessGrant);
|
||||
let grant = normalizeAccessGrant(bot?.accessGrant);
|
||||
if (grant) {
|
||||
setDraft(cloneGrant(grant, ownerPhone));
|
||||
onSaved?.(grant);
|
||||
|
|
@ -12836,6 +12841,24 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
|||
if (bot?.groupSessionScope) {
|
||||
setGroupSessionScope(normalizeGroupSessionScope(bot.groupSessionScope));
|
||||
}
|
||||
const missingTitles = Object.entries(grant?.groups ?? {}).filter(([, group]) => !(typeof group?.title === "string" && group.title.trim())).map(([jid]) => jid);
|
||||
const pendingGroupJids = (grant?.pending ?? []).map((entry) => entry.groupJid).filter((jid) => typeof jid === "string" && jid.endsWith("@g.us"));
|
||||
const toSync = [.../* @__PURE__ */ new Set([...missingTitles, ...pendingGroupJids])];
|
||||
if (toSync.length > 0) {
|
||||
try {
|
||||
const refreshed = unwrapRpcResult11(await rpcCall(ACCESS_GROUP_TITLES_REFRESH_ENDPOINT, {
|
||||
botId: account.botId,
|
||||
groupJids: toSync
|
||||
}));
|
||||
if (cancelled) return;
|
||||
const next = grantFromSnapshot(refreshed, account.botId);
|
||||
if (next) {
|
||||
setDraft(cloneGrant(next, ownerPhone));
|
||||
onSaved?.(next);
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
setFeedback({
|
||||
|
|
@ -13018,6 +13041,9 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
|||
h2("h3", null, groupDisplayName(groupJid, draft.groups)),
|
||||
h2("code", { className: "dim-accessGroupJid" }, groupJid)
|
||||
),
|
||||
h2(
|
||||
"div",
|
||||
{ className: "dim-accessGroupTitleRow" },
|
||||
h2(
|
||||
"label",
|
||||
{ className: "dim-accessField" },
|
||||
|
|
@ -13035,6 +13061,38 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
|||
}))
|
||||
})
|
||||
),
|
||||
h2("button", {
|
||||
type: "button",
|
||||
className: "dim-deliveryButton",
|
||||
disabled: saving,
|
||||
onClick: () => void (async () => {
|
||||
setSaving(true);
|
||||
setFeedback(null);
|
||||
try {
|
||||
const refreshed = unwrapRpcResult11(await rpcCall(
|
||||
ACCESS_GROUP_TITLES_REFRESH_ENDPOINT,
|
||||
{ botId: account.botId, groupJids: [groupJid] }
|
||||
));
|
||||
const next = grantFromSnapshot(refreshed, account.botId);
|
||||
if (!next) throw new Error("\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u5DF2\u4FDD\u5B58\u7684\u8BBF\u95EE\u6388\u6743\u3002");
|
||||
setDraft(cloneGrant(next, ownerPhone));
|
||||
onSaved?.(next);
|
||||
const title = next.groups?.[groupJid]?.title;
|
||||
setFeedback({
|
||||
tone: title ? "success" : "error",
|
||||
message: title ? ["\u5DF2\u540C\u6B65\u7FA4\u540D\uFF1A", title].join("") : "\u672A\u80FD\u4ECE WhatsApp \u8BFB\u53D6\u7FA4\u540D\uFF0C\u8BF7\u786E\u8BA4\u673A\u5668\u4EBA\u5728\u7EBF\u4E14\u4ECD\u5728\u8BE5\u7FA4\u3002"
|
||||
});
|
||||
} catch (error) {
|
||||
setFeedback({
|
||||
tone: "error",
|
||||
message: error?.message || "\u540C\u6B65\u7FA4\u540D\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
})
|
||||
}, "\u540C\u6B65\u7FA4\u540D")
|
||||
),
|
||||
h2(AdminPhones, {
|
||||
title: "\u672C\u7FA4\u7BA1\u7406\u5458",
|
||||
help: "\u672A\u914D\u7F6E\u65F6\uFF0C\u672C\u7FA4\u7533\u8BF7\u56DE\u843D\u7ED9\u5168\u5C40\u7BA1\u7406\u5458\u5BA1\u6279\uFF08\u4ECD\u53EA\u6388\u4E88\u672C\u7FA4\u6743\uFF09\u3002",
|
||||
|
|
@ -14610,6 +14668,7 @@ var CSS11 = String.raw`
|
|||
.dim-accessGroupHeading { min-width: 0; display: grid; gap: 2px; }
|
||||
.dim-accessGroupHeading h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 21px; font-weight: 650; }
|
||||
.dim-accessGroupJid { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 16px; overflow-wrap: anywhere; }
|
||||
.dim-accessGroupTitleRow { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; gap: 10px; align-items: end; }
|
||||
.dim-accessNickname { display: block; margin-top: 4px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; font-weight: 560; }
|
||||
.dim-accessNicknameEmpty { color: var(--dsw-alias-label-tertiary, #8f959e); font-weight: 400; }
|
||||
.dim-accessUserRow { position: relative; z-index: 0; overflow: visible; }
|
||||
|
|
|
|||
422
lib/index.js
422
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.9",
|
||||
"version": "4.9.1-ops.10",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { h, localizeText } from './i18n.js';
|
|||
|
||||
export const ACCESS_GRANT_ENDPOINT = 'bot.access-grant.set';
|
||||
export const ACCESS_PENDING_RESOLVE_ENDPOINT = 'bot.access-pending.resolve';
|
||||
export const ACCESS_GROUP_TITLES_REFRESH_ENDPOINT = 'bot.access-group-titles.refresh';
|
||||
export const GROUP_SESSION_SCOPE_ENDPOINT = 'bot.group-session-scope.set';
|
||||
|
||||
function unwrapRpcResult(result) {
|
||||
|
|
@ -88,10 +89,7 @@ function NicknameHint({ contacts, phone }) {
|
|||
function groupDisplayName(groupJid, groups) {
|
||||
const title = groups?.[groupJid]?.title;
|
||||
if (typeof title === 'string' && title.trim()) return title.trim();
|
||||
const raw = String(groupJid ?? '');
|
||||
const id = raw.replace(/@g\.us$/i, '');
|
||||
if (id.length > 14) return `${id.slice(0, 10)}…@g.us`;
|
||||
return raw || '未知群';
|
||||
return '未命名群';
|
||||
}
|
||||
|
||||
function digitsOnly(value) {
|
||||
|
|
@ -300,7 +298,7 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
|
|||
const bot = Array.isArray(value?.bots)
|
||||
? value.bots.find((entry) => entry?.botId === account.botId)
|
||||
: null;
|
||||
const grant = normalizeAccessGrant(bot?.accessGrant);
|
||||
let grant = normalizeAccessGrant(bot?.accessGrant);
|
||||
if (grant) {
|
||||
setDraft(cloneGrant(grant, ownerPhone));
|
||||
onSaved?.(grant);
|
||||
|
|
@ -308,6 +306,30 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
|
|||
if (bot?.groupSessionScope) {
|
||||
setGroupSessionScope(normalizeGroupSessionScope(bot.groupSessionScope));
|
||||
}
|
||||
|
||||
const missingTitles = Object.entries(grant?.groups ?? {})
|
||||
.filter(([, group]) => !(typeof group?.title === 'string' && group.title.trim()))
|
||||
.map(([jid]) => jid);
|
||||
const pendingGroupJids = (grant?.pending ?? [])
|
||||
.map((entry) => entry.groupJid)
|
||||
.filter((jid) => typeof jid === 'string' && jid.endsWith('@g.us'));
|
||||
const toSync = [...new Set([...missingTitles, ...pendingGroupJids])];
|
||||
if (toSync.length > 0) {
|
||||
try {
|
||||
const refreshed = unwrapRpcResult(await rpcCall(ACCESS_GROUP_TITLES_REFRESH_ENDPOINT, {
|
||||
botId: account.botId,
|
||||
groupJids: toSync,
|
||||
}));
|
||||
if (cancelled) return;
|
||||
const next = grantFromSnapshot(refreshed, account.botId);
|
||||
if (next) {
|
||||
setDraft(cloneGrant(next, ownerPhone));
|
||||
onSaved?.(next);
|
||||
}
|
||||
} catch {
|
||||
// Title sync is best-effort while the bot is offline.
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
setFeedback({
|
||||
|
|
@ -485,6 +507,7 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
|
|||
h('div', { className: 'dim-accessGroupHeading' },
|
||||
h('h3', null, groupDisplayName(groupJid, draft.groups)),
|
||||
h('code', { className: 'dim-accessGroupJid' }, groupJid)),
|
||||
h('div', { className: 'dim-accessGroupTitleRow' },
|
||||
h('label', { className: 'dim-accessField' },
|
||||
h('span', null, '群名称'),
|
||||
h('input', {
|
||||
|
|
@ -499,6 +522,39 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
|
|||
},
|
||||
})),
|
||||
})),
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'dim-deliveryButton',
|
||||
disabled: saving,
|
||||
onClick: () => void (async () => {
|
||||
setSaving(true);
|
||||
setFeedback(null);
|
||||
try {
|
||||
const refreshed = unwrapRpcResult(await rpcCall(
|
||||
ACCESS_GROUP_TITLES_REFRESH_ENDPOINT,
|
||||
{ botId: account.botId, groupJids: [groupJid] },
|
||||
));
|
||||
const next = grantFromSnapshot(refreshed, account.botId);
|
||||
if (!next) throw new Error('服务没有返回已保存的访问授权。');
|
||||
setDraft(cloneGrant(next, ownerPhone));
|
||||
onSaved?.(next);
|
||||
const title = next.groups?.[groupJid]?.title;
|
||||
setFeedback({
|
||||
tone: title ? 'success' : 'error',
|
||||
message: title
|
||||
? ['已同步群名:', title].join('')
|
||||
: '未能从 WhatsApp 读取群名,请确认机器人在线且仍在该群。',
|
||||
});
|
||||
} catch (error) {
|
||||
setFeedback({
|
||||
tone: 'error',
|
||||
message: error?.message || '同步群名失败,请稍后重试。',
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}),
|
||||
}, '同步群名')),
|
||||
h(AdminPhones, {
|
||||
title: '本群管理员',
|
||||
help: '未配置时,本群申请回落给全局管理员审批(仍只授予本群权)。',
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|||
setAccessPolicy: 'bot.access-policy.set',
|
||||
setAccessGrant: 'bot.access-grant.set',
|
||||
resolveAccessPending: 'bot.access-pending.resolve',
|
||||
refreshAccessGroupTitles: 'bot.access-group-titles.refresh',
|
||||
setGroupSessionScope: 'bot.group-session-scope.set',
|
||||
setWorkspace: 'bot.workspace.set',
|
||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ const EN = Object.freeze({
|
|||
'机器人设置页签': 'Bot settings tabs',
|
||||
'投递设置': 'Delivery settings',
|
||||
'访问设置': 'Access settings',
|
||||
"请提交有效的群名同步请求。": "Submit a valid group-name sync request.",
|
||||
"同步群名失败,请稍后重试。": "Could not sync the group name. Try again later.",
|
||||
"未能从 WhatsApp 读取群名,请确认机器人在线且仍在该群。": "Could not read the group name from WhatsApp. Confirm the bot is online and still in the group.",
|
||||
"已同步群名:": "Synced group name: ",
|
||||
"同步群名": "Sync group name",
|
||||
"未命名群": "Unnamed group",
|
||||
"暂无昵称(私聊/@ 后会自动补齐)": "No nickname yet (filled after DM or @mention)",
|
||||
"群聊 · ": "Group · ",
|
||||
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@ const CSS = String.raw`
|
|||
.dim-accessGroupHeading { min-width: 0; display: grid; gap: 2px; }
|
||||
.dim-accessGroupHeading h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 21px; font-weight: 650; }
|
||||
.dim-accessGroupJid { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 16px; overflow-wrap: anywhere; }
|
||||
.dim-accessGroupTitleRow { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; gap: 10px; align-items: end; }
|
||||
.dim-accessNickname { display: block; margin-top: 4px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; font-weight: 560; }
|
||||
.dim-accessNicknameEmpty { color: var(--dsw-alias-label-tertiary, #8f959e); font-weight: 400; }
|
||||
.dim-accessUserRow { position: relative; z-index: 0; overflow: visible; }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { validateAccessGrant, normalizeAccessGrant } from '../../../../src/chann
|
|||
|
||||
export const SET_ACCESS_GRANT_ENDPOINT = 'bot.access-grant.set';
|
||||
export const RESOLVE_ACCESS_PENDING_ENDPOINT = 'bot.access-pending.resolve';
|
||||
export const REFRESH_ACCESS_GROUP_TITLES_ENDPOINT = 'bot.access-group-titles.refresh';
|
||||
|
||||
export function validAccessGrantPayload(payload) {
|
||||
try {
|
||||
|
|
@ -41,6 +42,21 @@ export function validAccessPendingResolvePayload(payload) {
|
|||
}
|
||||
}
|
||||
|
||||
export function validAccessGroupTitlesRefreshPayload(payload) {
|
||||
try {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return false;
|
||||
if (!Object.hasOwn(payload, 'botId') || typeof payload.botId !== 'string'
|
||||
|| !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) return false;
|
||||
const keys = Reflect.ownKeys(payload);
|
||||
if (keys.length === 1) return true;
|
||||
if (keys.length !== 2 || !Object.hasOwn(payload, 'groupJids')) return false;
|
||||
if (!Array.isArray(payload.groupJids)) return false;
|
||||
return payload.groupJids.every((jid) => typeof jid === 'string' && /^\d{5,32}@g\.us$/.test(jid));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function publicAccessGrant(grant) {
|
||||
const normalized = normalizeAccessGrant(grant);
|
||||
if (!normalized) return null;
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import QRCode from 'qrcode';
|
|||
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||
import {
|
||||
REFRESH_ACCESS_GROUP_TITLES_ENDPOINT,
|
||||
RESOLVE_ACCESS_PENDING_ENDPOINT,
|
||||
SET_ACCESS_GRANT_ENDPOINT,
|
||||
validAccessGrantPayload,
|
||||
validAccessGroupTitlesRefreshPayload,
|
||||
validAccessPendingResolvePayload,
|
||||
} from '../shared/access-grant-rpc.mjs';
|
||||
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||
|
|
@ -25,6 +27,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|||
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
||||
setAccessGrant: SET_ACCESS_GRANT_ENDPOINT,
|
||||
resolveAccessPending: RESOLVE_ACCESS_PENDING_ENDPOINT,
|
||||
refreshAccessGroupTitles: REFRESH_ACCESS_GROUP_TITLES_ENDPOINT,
|
||||
setGroupSessionScope: SET_GROUP_SESSION_SCOPE_ENDPOINT,
|
||||
setWorkspace: SET_WORKSPACE_ENDPOINT,
|
||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||
|
|
@ -76,6 +79,10 @@ function payloadFailure(endpoint, payload) {
|
|||
return validAccessPendingResolvePayload(payload)
|
||||
? null : '请提交有效的审批请求。';
|
||||
}
|
||||
if (endpoint === WHATSAPP_ENDPOINTS.refreshAccessGroupTitles) {
|
||||
return validAccessGroupTitlesRefreshPayload(payload)
|
||||
? null : '请提交有效的群名同步请求。';
|
||||
}
|
||||
if (endpoint === WHATSAPP_ENDPOINTS.setGroupSessionScope) {
|
||||
return validGroupSessionScopePayload(payload)
|
||||
? null : '请提交有效的群会话策略。';
|
||||
|
|
@ -230,6 +237,14 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|||
},
|
||||
(status) => publicStatus(status, cachedEncode),
|
||||
);
|
||||
} else if (endpoint === WHATSAPP_ENDPOINTS.refreshAccessGroupTitles) {
|
||||
if (typeof controller.refreshAccessGroupTitles !== 'function') {
|
||||
throw new Error('Group title refresh is unavailable');
|
||||
}
|
||||
value = await publicStatus(
|
||||
await controller.refreshAccessGroupTitles(payload.botId, payload.groupJids),
|
||||
cachedEncode,
|
||||
);
|
||||
} else if (endpoint === WHATSAPP_ENDPOINTS.setGroupSessionScope) {
|
||||
if (typeof controller.updateGroupSessionScope !== 'function') throw new Error('Group session scope update is unavailable');
|
||||
value = await controller.updateGroupSessionScope(
|
||||
|
|
|
|||
|
|
@ -238,6 +238,22 @@ export class WhatsappController {
|
|||
});
|
||||
}
|
||||
|
||||
async refreshAccessGroupTitles(botId, groupJids) {
|
||||
if (this.#closed) throw new Error('WhatsApp controller is closed');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
if (this.#closed) throw new Error('WhatsApp controller is closed');
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime || typeof runtime.refreshGroupTitles !== 'function') {
|
||||
const error = new Error(t('WhatsApp机器人尚未连接'));
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
await runtime.refreshGroupTitles(groupJids);
|
||||
this.#touch();
|
||||
return this.status();
|
||||
});
|
||||
}
|
||||
|
||||
async setAccessPolicy(botId, value) {
|
||||
if (this.#closed) throw new Error('WhatsApp controller is closed');
|
||||
const accessPolicy = normalizeWhatsappAccessPolicy(value);
|
||||
|
|
|
|||
|
|
@ -241,18 +241,86 @@ export async function enrichWhatsappInboundIdentities(message, raw, {
|
|||
|
||||
/**
|
||||
* Resolve a WhatsApp group subject for display / access-grant group cards.
|
||||
* @param {{ groupMetadata?: (jid: string) => Promise<{ subject?: string }> }|null|undefined} socket
|
||||
* Tries groupMetadata first, then groupFetchAllParticipating as a linked-device fallback.
|
||||
* @param {object|null|undefined} socket
|
||||
* @param {string} groupJid
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function resolveWhatsappGroupSubject(socket, groupJid) {
|
||||
if (!socket || typeof socket.groupMetadata !== 'function') return '';
|
||||
if (typeof groupJid !== 'string' || !groupJid.endsWith('@g.us')) return '';
|
||||
if (!socket || typeof groupJid !== 'string' || !groupJid.endsWith('@g.us')) return '';
|
||||
|
||||
const fromMeta = (meta) => {
|
||||
for (const key of ['subject', 'name', 'topic']) {
|
||||
if (typeof meta?.[key] === 'string' && meta[key].trim()) {
|
||||
return meta[key].trim().slice(0, 128);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
if (typeof socket.groupMetadata === 'function') {
|
||||
try {
|
||||
const meta = await socket.groupMetadata(groupJid);
|
||||
const subject = typeof meta?.subject === 'string' ? meta.subject.trim() : '';
|
||||
return subject.slice(0, 128);
|
||||
const subject = fromMeta(await socket.groupMetadata(groupJid));
|
||||
if (subject) return subject;
|
||||
} catch {
|
||||
// Fall through to participating catalog.
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof socket.groupFetchAllParticipating === 'function') {
|
||||
try {
|
||||
const catalog = await socket.groupFetchAllParticipating();
|
||||
if (catalog && typeof catalog === 'object') {
|
||||
const direct = fromMeta(catalog[groupJid]);
|
||||
if (direct) return direct;
|
||||
for (const meta of Object.values(catalog)) {
|
||||
if (meta?.id === groupJid || meta?.id === groupJid.replace(/@g\.us$/i, '')) {
|
||||
const subject = fromMeta(meta);
|
||||
if (subject) return subject;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve many group subjects in one participating fetch when possible.
|
||||
* @param {object|null|undefined} socket
|
||||
* @param {string[]} groupJids
|
||||
* @returns {Promise<Record<string, string>>}
|
||||
*/
|
||||
export async function resolveWhatsappGroupSubjects(socket, groupJids) {
|
||||
const jids = [...new Set((Array.isArray(groupJids) ? groupJids : [])
|
||||
.filter((jid) => typeof jid === 'string' && jid.endsWith('@g.us')))];
|
||||
/** @type {Record<string, string>} */
|
||||
const titles = {};
|
||||
if (!socket || jids.length === 0) return titles;
|
||||
|
||||
let catalog = null;
|
||||
if (typeof socket.groupFetchAllParticipating === 'function') {
|
||||
try {
|
||||
catalog = await socket.groupFetchAllParticipating();
|
||||
} catch {
|
||||
catalog = null;
|
||||
}
|
||||
}
|
||||
|
||||
for (const jid of jids) {
|
||||
let title = '';
|
||||
if (catalog && typeof catalog === 'object') {
|
||||
const meta = catalog[jid]
|
||||
?? Object.values(catalog).find((entry) => entry?.id === jid);
|
||||
if (typeof meta?.subject === 'string' && meta.subject.trim()) {
|
||||
title = meta.subject.trim().slice(0, 128);
|
||||
}
|
||||
}
|
||||
if (!title) title = await resolveWhatsappGroupSubject(socket, jid);
|
||||
if (title) titles[jid] = title;
|
||||
}
|
||||
return titles;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ import {
|
|||
normalizeMessageContent,
|
||||
} from '@whiskeysockets/baileys';
|
||||
|
||||
import { emptyAccessGrant, ensureGroupBucket } from '../shared/access-grant.mjs';
|
||||
import { splitMessageText } from '../shared/editable-message-stream.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
||||
import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
|
||||
import { createWhatsappBridgeStatus, WhatsappHarnessBridge } from './whatsapp-bridge.mjs';
|
||||
import { enrichWhatsappInboundIdentities, resolveWhatsappGroupSubject } from './whatsapp-identity.mjs';
|
||||
import { enrichWhatsappInboundIdentities, resolveWhatsappGroupSubject, resolveWhatsappGroupSubjects } from './whatsapp-identity.mjs';
|
||||
import {
|
||||
gateWhatsappInbound,
|
||||
loadWhatsappAccessGrant,
|
||||
|
|
@ -859,6 +860,47 @@ export class WhatsappRuntime {
|
|||
return { sent: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync WhatsApp group subjects into accessGrant.groups[*].title.
|
||||
* @param {string[]|null|undefined} groupJids
|
||||
*/
|
||||
async refreshGroupTitles(groupJids) {
|
||||
if (!this.#workspaces || !this.#botId) {
|
||||
const error = new Error('Access grant store is unavailable');
|
||||
error.code = 'access-grant-unavailable';
|
||||
throw error;
|
||||
}
|
||||
const socket = this.#session?.socket;
|
||||
if (!this.#status.ready || !socket) {
|
||||
const error = new Error(t('WhatsApp机器人尚未连接'));
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
let grant = this.#workspaces.accessGrantFor(this.#botId) ?? emptyAccessGrant();
|
||||
const targets = Array.isArray(groupJids) && groupJids.length > 0
|
||||
? groupJids
|
||||
: Object.keys(grant.groups ?? {});
|
||||
const missing = targets.filter((jid) => {
|
||||
const title = grant.groups?.[jid]?.title;
|
||||
return typeof jid === 'string' && jid.endsWith('@g.us')
|
||||
&& !(typeof title === 'string' && title.trim());
|
||||
});
|
||||
// Always allow forced refresh of explicit jids even when title already set.
|
||||
const forced = Array.isArray(groupJids) && groupJids.length > 0
|
||||
? targets.filter((jid) => typeof jid === 'string' && jid.endsWith('@g.us'))
|
||||
: missing;
|
||||
const titles = await resolveWhatsappGroupSubjects(socket, forced);
|
||||
let changed = false;
|
||||
for (const [jid, title] of Object.entries(titles)) {
|
||||
const previousTitle = grant.groups?.[jid]?.title ?? '';
|
||||
if (previousTitle === title) continue;
|
||||
grant = ensureGroupBucket(grant, jid, { title });
|
||||
changed = true;
|
||||
}
|
||||
if (changed) await this.#workspaces.setAccessGrant(this.#botId, grant);
|
||||
return grant;
|
||||
}
|
||||
|
||||
async sendProactiveText(target, text, { signal } = {}) {
|
||||
const jid = typeof target?.route?.jid === 'string' ? target.route.jid.trim() : '';
|
||||
const validUser = target?.kind === 'user'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { resolveWhatsappGroupSubject } from '../../../src/channels/whatsapp/whatsapp-identity.mjs';
|
||||
import {
|
||||
resolveWhatsappGroupSubject,
|
||||
resolveWhatsappGroupSubjects,
|
||||
} from '../../../src/channels/whatsapp/whatsapp-identity.mjs';
|
||||
|
||||
test('resolveWhatsappGroupSubject reads Baileys groupMetadata.subject', async () => {
|
||||
const subject = await resolveWhatsappGroupSubject({
|
||||
|
|
@ -13,6 +16,31 @@ test('resolveWhatsappGroupSubject reads Baileys groupMetadata.subject', async ()
|
|||
assert.equal(subject, '运维值班群');
|
||||
});
|
||||
|
||||
test('resolveWhatsappGroupSubject falls back to groupFetchAllParticipating', async () => {
|
||||
const subject = await resolveWhatsappGroupSubject({
|
||||
groupMetadata: async () => {
|
||||
throw new Error('forbidden');
|
||||
},
|
||||
groupFetchAllParticipating: async () => ({
|
||||
'120363429229984366@g.us': { id: '120363429229984366@g.us', subject: '告警群' },
|
||||
}),
|
||||
}, '120363429229984366@g.us');
|
||||
assert.equal(subject, '告警群');
|
||||
});
|
||||
|
||||
test('resolveWhatsappGroupSubjects batch-fills missing titles', async () => {
|
||||
const titles = await resolveWhatsappGroupSubjects({
|
||||
groupFetchAllParticipating: async () => ({
|
||||
'120363429229984366@g.us': { id: '120363429229984366@g.us', subject: 'A群' },
|
||||
'120363111111111111@g.us': { id: '120363111111111111@g.us', subject: 'B群' },
|
||||
}),
|
||||
}, ['120363429229984366@g.us', '120363111111111111@g.us']);
|
||||
assert.deepStrictEqual(titles, {
|
||||
'120363429229984366@g.us': 'A群',
|
||||
'120363111111111111@g.us': 'B群',
|
||||
});
|
||||
});
|
||||
|
||||
test('resolveWhatsappGroupSubject fails soft without socket metadata', async () => {
|
||||
assert.equal(await resolveWhatsappGroupSubject(null, '120363429229984366@g.us'), '');
|
||||
assert.equal(await resolveWhatsappGroupSubject({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue