mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +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
80
test/channels/shared/channel-peer.test.mjs
Normal file
80
test/channels/shared/channel-peer.test.mjs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import {
|
||||
parseConversationKey,
|
||||
resolveChannelPeerFromBinding,
|
||||
} from '../../../src/channels/shared/channel-peer.mjs';
|
||||
import { ConversationStateStore } from '../../../src/channels/shared/conversation-state-store.mjs';
|
||||
|
||||
test('parseConversationKey covers direct, shared group, and per-user group', () => {
|
||||
assert.deepEqual(parseConversationKey('direct:8618111111111@s.whatsapp.net'), {
|
||||
kind: 'direct',
|
||||
conversationId: '8618111111111@s.whatsapp.net',
|
||||
senderId: null,
|
||||
});
|
||||
assert.deepEqual(parseConversationKey('group:120363@g.us'), {
|
||||
kind: 'group',
|
||||
conversationId: '120363@g.us',
|
||||
senderId: null,
|
||||
});
|
||||
assert.deepEqual(parseConversationKey('group:120363@g.us:user:8618222222222@s.whatsapp.net'), {
|
||||
kind: 'group',
|
||||
conversationId: '120363@g.us',
|
||||
senderId: '8618222222222@s.whatsapp.net',
|
||||
});
|
||||
assert.equal(parseConversationKey(''), null);
|
||||
assert.equal(parseConversationKey('other:x'), null);
|
||||
});
|
||||
|
||||
test('resolveChannelPeerFromBinding builds DM and group labels from access grant', () => {
|
||||
const grant = {
|
||||
contacts: [{ phone: '8618222222222', pushName: 'Alice' }],
|
||||
groups: {
|
||||
'120363@g.us': { title: 'Ops Room', admins: [], members: [] },
|
||||
},
|
||||
};
|
||||
const direct = resolveChannelPeerFromBinding({
|
||||
channel: 'whatsapp',
|
||||
botId: 'bot-1',
|
||||
conversationKey: 'direct:8618222222222@s.whatsapp.net',
|
||||
grant,
|
||||
});
|
||||
assert.equal(direct.label, 'Alice · 8618222222222');
|
||||
assert.equal(direct.kind, 'direct');
|
||||
assert.equal(direct.phone, '8618222222222');
|
||||
|
||||
const groupUser = resolveChannelPeerFromBinding({
|
||||
channel: 'whatsapp',
|
||||
botId: 'bot-1',
|
||||
conversationKey: 'group:120363@g.us:user:8618222222222@s.whatsapp.net',
|
||||
grant,
|
||||
});
|
||||
assert.equal(groupUser.label, 'Ops Room · Alice');
|
||||
assert.equal(groupUser.groupTitle, 'Ops Room');
|
||||
|
||||
const groupShared = resolveChannelPeerFromBinding({
|
||||
channel: 'whatsapp',
|
||||
botId: 'bot-1',
|
||||
conversationKey: 'group:120363@g.us',
|
||||
grant,
|
||||
});
|
||||
assert.equal(groupShared.label, '群 · Ops Room');
|
||||
});
|
||||
|
||||
test('ConversationStateStore.keyForSession reverses session bindings', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-im-channel-peer-'));
|
||||
try {
|
||||
const store = await new ConversationStateStore(join(root, 'state.json')).load();
|
||||
await store.setSession('direct:u1', 'session-a');
|
||||
await store.setSession('group:g1:user:u2', 'session-b');
|
||||
assert.equal(store.keyForSession('session-a'), 'direct:u1');
|
||||
assert.equal(store.keyForSession('session-b'), 'group:g1:user:u2');
|
||||
assert.equal(store.keyForSession('missing'), null);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
|
@ -764,7 +764,10 @@ test('client registers one top-level bilingual IM settings section with a direct
|
|||
},
|
||||
slots: {
|
||||
inject(name, install) {
|
||||
assert.equal(name, 'settings.section');
|
||||
assert.ok(
|
||||
name === 'settings.section' || name === 'conversation.session.header.actions',
|
||||
`unexpected slot inject: ${name}`,
|
||||
);
|
||||
install();
|
||||
},
|
||||
register(options, component) {
|
||||
|
|
@ -783,15 +786,22 @@ test('client registers one top-level bilingual IM settings section with a direct
|
|||
assert.deepEqual(clientInject, ['slots', 'connection', 'locale', 'workspaces']);
|
||||
assert.equal(dictionaries[0].namespace, IM_LOCALE_NAMESPACE);
|
||||
assert.deepEqual(Object.keys(dictionaries[0].value.en).sort(), Object.keys(dictionaries[0].value.zh).sort());
|
||||
assert.equal(registrations.length, 1);
|
||||
assert.equal(registrations[0].options.name, 'settings.section');
|
||||
assert.equal(registrations[0].options.id, 'xmanrui-dsh-im');
|
||||
assert.equal(registrations[0].options.order, 21);
|
||||
assert.equal(registrations[0].options.locale, IM_LOCALE_NAMESPACE);
|
||||
assert.equal(registrations[0].options.label(), 'IM bots');
|
||||
assert.equal(registrations[0].component, IMSettingsTab);
|
||||
assert.equal(registrations.length, 2);
|
||||
const header = registrations.find((entry) => entry.options.name === 'conversation.session.header.actions');
|
||||
const settings = registrations.find((entry) => entry.options.name === 'settings.section');
|
||||
assert.ok(header);
|
||||
assert.equal(header.options.id, 'im-channel-peer');
|
||||
assert.equal(header.options.order, 0);
|
||||
assert.equal(header.options.locale, IM_LOCALE_NAMESPACE);
|
||||
assert.equal(typeof header.options.inject().resolveChannelPeer, 'function');
|
||||
assert.ok(settings);
|
||||
assert.equal(settings.options.id, 'xmanrui-dsh-im');
|
||||
assert.equal(settings.options.order, 21);
|
||||
assert.equal(settings.options.locale, IM_LOCALE_NAMESPACE);
|
||||
assert.equal(settings.options.label(), 'IM bots');
|
||||
assert.equal(settings.component, IMSettingsTab);
|
||||
|
||||
const injected = registrations[0].options.inject();
|
||||
const injected = settings.options.inject();
|
||||
const signal = new AbortController().signal;
|
||||
await injected.updateRpcCall('update.status', {}, signal);
|
||||
assert.deepEqual(rpcCalls, [['/dsh-im', 'update.status', {}, signal]]);
|
||||
|
|
@ -806,7 +816,7 @@ test('client registers one top-level bilingual IM settings section with a direct
|
|||
]);
|
||||
|
||||
const markup = renderToStaticMarkup(React.createElement(
|
||||
registrations[0].component,
|
||||
settings.component,
|
||||
injected,
|
||||
));
|
||||
assert.match(markup, /Connecting DeepSeek Harness/);
|
||||
|
|
@ -846,7 +856,10 @@ test('client directory picker uses the current DSH uiWorkspace service', async (
|
|||
},
|
||||
slots: {
|
||||
inject(name, install) {
|
||||
assert.equal(name, 'settings.section');
|
||||
assert.ok(
|
||||
name === 'settings.section' || name === 'conversation.session.header.actions',
|
||||
`unexpected slot inject: ${name}`,
|
||||
);
|
||||
install();
|
||||
},
|
||||
register(options, component) {
|
||||
|
|
@ -869,7 +882,9 @@ test('client directory picker uses the current DSH uiWorkspace service', async (
|
|||
},
|
||||
};
|
||||
|
||||
const injected = registrations[0].options.inject();
|
||||
const settings = registrations.find((entry) => entry.options.name === 'settings.section');
|
||||
assert.ok(settings);
|
||||
const injected = settings.options.inject();
|
||||
const signal = new AbortController().signal;
|
||||
assert.deepEqual(
|
||||
await injected.workspaceDirectoryPicker.listDirectory('/workspace/current', signal),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue