mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 14:20:45 +08:00
Show WhatsApp channel peer next to Agent Preset in session header (4.9.1-ops.20).
Plugins inject conversation.session.header.actions; resolve sessionId back to the bound chat and render nickname/phone or group title. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2718586fda
commit
37742043a9
15 changed files with 852 additions and 326 deletions
|
|
@ -28,6 +28,7 @@ import {
|
|||
resolveAccessPending,
|
||||
validateAccessGrant,
|
||||
} from './access-grant.mjs';
|
||||
import { resolveChannelPeerFromBinding } from './channel-peer.mjs';
|
||||
import { CONNECTION_TEST_STATE_IDENTITY } from './connection-test.mjs';
|
||||
import {
|
||||
DEFAULT_CONTEXT_ENHANCEMENT_CONFIG,
|
||||
|
|
@ -1458,10 +1459,16 @@ export function createBotScopedHarness(harness, options) {
|
|||
return createBotWorkspaceScope(harness, options).harness;
|
||||
}
|
||||
|
||||
export function createWorkspaceAwareController(controller, { workspaces, stateFor, agentPresetCatalog } = {}) {
|
||||
export function createWorkspaceAwareController(controller, {
|
||||
workspaces,
|
||||
stateFor,
|
||||
agentPresetCatalog,
|
||||
channel = null,
|
||||
} = {}) {
|
||||
if (!controller || !workspaces || typeof stateFor !== 'function') {
|
||||
throw new TypeError('controller, workspaces, and stateFor are required');
|
||||
}
|
||||
const channelId = typeof channel === 'string' && channel.trim() ? channel.trim() : null;
|
||||
const transitions = new Map();
|
||||
const withBotTransition = (botId, operation) => {
|
||||
const previous = transitions.get(botId) ?? Promise.resolve();
|
||||
|
|
@ -1674,6 +1681,35 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
|||
}
|
||||
});
|
||||
|
||||
const resolveChannelPeer = async (sessionId) => {
|
||||
if (!channelId) return null;
|
||||
const id = typeof sessionId === 'string' ? sessionId.trim() : '';
|
||||
if (!id || id.length > 256) return null;
|
||||
const snapshot = await controller.status();
|
||||
const bots = Array.isArray(snapshot?.bots) ? snapshot.bots : [];
|
||||
for (const bot of bots) {
|
||||
const botId = typeof bot?.botId === 'string' ? bot.botId.trim() : '';
|
||||
if (!botId) continue;
|
||||
let state;
|
||||
try {
|
||||
state = await stateFor(botId);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const conversationKey = typeof state?.keyForSession === 'function'
|
||||
? state.keyForSession(id)
|
||||
: null;
|
||||
if (!conversationKey) continue;
|
||||
return resolveChannelPeerFromBinding({
|
||||
channel: channelId,
|
||||
botId,
|
||||
conversationKey,
|
||||
grant: workspaces.accessGrantFor(botId),
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return new Proxy(controller, {
|
||||
get(target, property) {
|
||||
if (property === 'updateWorkspace') return updateWorkspace;
|
||||
|
|
@ -1683,6 +1719,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
|||
if (property === 'updateGroupSessionScope') return updateGroupSessionScope;
|
||||
if (property === 'updateAccessGrant') return updateAccessGrant;
|
||||
if (property === 'resolveAccessPending') return resolveAccessPendingProxy;
|
||||
if (property === 'resolveChannelPeer') return resolveChannelPeer;
|
||||
const value = Reflect.get(target, property, target);
|
||||
if (typeof value !== 'function') return value;
|
||||
if (property === 'deleteBot') {
|
||||
|
|
|
|||
115
src/channels/shared/channel-peer.mjs
Normal file
115
src/channels/shared/channel-peer.mjs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { normalizeAccessPhone, phoneFromWhatsappJid } from './access-grant.mjs';
|
||||
|
||||
/**
|
||||
* Parse a durable conversation binding key into chat identity fields.
|
||||
* @param {unknown} key
|
||||
* @returns {{ kind: 'direct' | 'group', conversationId: string, senderId: string | null } | null}
|
||||
*/
|
||||
export function parseConversationKey(key) {
|
||||
if (typeof key !== 'string' || !key) return null;
|
||||
if (key.startsWith('direct:')) {
|
||||
const conversationId = key.slice('direct:'.length).trim();
|
||||
return conversationId ? { kind: 'direct', conversationId, senderId: null } : null;
|
||||
}
|
||||
if (!key.startsWith('group:')) return null;
|
||||
const rest = key.slice('group:'.length);
|
||||
const userMarker = ':user:';
|
||||
const userAt = rest.indexOf(userMarker);
|
||||
if (userAt >= 0) {
|
||||
const conversationId = rest.slice(0, userAt).trim();
|
||||
const senderId = rest.slice(userAt + userMarker.length).trim();
|
||||
if (!conversationId || !senderId) return null;
|
||||
return { kind: 'group', conversationId, senderId };
|
||||
}
|
||||
const conversationId = rest.trim();
|
||||
return conversationId ? { kind: 'group', conversationId, senderId: null } : null;
|
||||
}
|
||||
|
||||
function contactPushName(grant, phone) {
|
||||
if (!phone || !grant || !Array.isArray(grant.contacts)) return '';
|
||||
const hit = grant.contacts.find((contact) => contact?.phone === phone);
|
||||
return typeof hit?.pushName === 'string' ? hit.pushName.trim() : '';
|
||||
}
|
||||
|
||||
function groupTitle(grant, groupJid) {
|
||||
const title = grant?.groups?.[groupJid]?.title;
|
||||
return typeof title === 'string' && title.trim() ? title.trim() : '';
|
||||
}
|
||||
|
||||
function phoneFromIdentity(value) {
|
||||
return phoneFromWhatsappJid(value) ?? normalizeAccessPhone(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a public channel-peer summary for session-header display.
|
||||
* @param {{
|
||||
* channel: string,
|
||||
* botId: string,
|
||||
* conversationKey: string,
|
||||
* grant?: object | null,
|
||||
* }} input
|
||||
* @returns {object | null}
|
||||
*/
|
||||
export function resolveChannelPeerFromBinding(input) {
|
||||
const channel = typeof input?.channel === 'string' ? input.channel.trim() : '';
|
||||
const botId = typeof input?.botId === 'string' ? input.botId.trim() : '';
|
||||
const conversationKey = typeof input?.conversationKey === 'string'
|
||||
? input.conversationKey.trim() : '';
|
||||
if (!channel || !botId || !conversationKey) return null;
|
||||
const parsed = parseConversationKey(conversationKey);
|
||||
if (!parsed) return null;
|
||||
const grant = input.grant && typeof input.grant === 'object' ? input.grant : null;
|
||||
|
||||
if (parsed.kind === 'direct') {
|
||||
const phone = phoneFromIdentity(parsed.conversationId);
|
||||
const pushName = contactPushName(grant, phone);
|
||||
const who = pushName || phone || parsed.conversationId;
|
||||
const label = phone && pushName && pushName !== phone
|
||||
? `${pushName} · ${phone}`
|
||||
: who;
|
||||
return Object.freeze({
|
||||
channel,
|
||||
botId,
|
||||
kind: 'direct',
|
||||
conversationKey,
|
||||
conversationId: parsed.conversationId,
|
||||
senderId: null,
|
||||
phone: phone || null,
|
||||
pushName: pushName || null,
|
||||
groupTitle: null,
|
||||
label,
|
||||
});
|
||||
}
|
||||
|
||||
const title = groupTitle(grant, parsed.conversationId) || '未命名群';
|
||||
if (!parsed.senderId) {
|
||||
return Object.freeze({
|
||||
channel,
|
||||
botId,
|
||||
kind: 'group',
|
||||
conversationKey,
|
||||
conversationId: parsed.conversationId,
|
||||
senderId: null,
|
||||
phone: null,
|
||||
pushName: null,
|
||||
groupTitle: title,
|
||||
label: `群 · ${title}`,
|
||||
});
|
||||
}
|
||||
|
||||
const phone = phoneFromIdentity(parsed.senderId);
|
||||
const pushName = contactPushName(grant, phone);
|
||||
const who = pushName || phone || parsed.senderId;
|
||||
return Object.freeze({
|
||||
channel,
|
||||
botId,
|
||||
kind: 'group',
|
||||
conversationKey,
|
||||
conversationId: parsed.conversationId,
|
||||
senderId: parsed.senderId,
|
||||
phone: phone || null,
|
||||
pushName: pushName || null,
|
||||
groupTitle: title,
|
||||
label: `${title} · ${who}`,
|
||||
});
|
||||
}
|
||||
|
|
@ -47,6 +47,19 @@ export class ConversationStateStore {
|
|||
return this.#state.sessions[key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse-map a Harness session id to its conversation binding key.
|
||||
* @param {string} sessionId
|
||||
* @returns {string|null}
|
||||
*/
|
||||
keyForSession(sessionId) {
|
||||
if (typeof sessionId !== 'string' || !sessionId) return null;
|
||||
for (const [key, bound] of Object.entries(this.#state.sessions)) {
|
||||
if (bound === sessionId) return key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async setSession(key, sessionId) {
|
||||
this.#state.sessions[key] = sessionId;
|
||||
await this.#persist();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue