@coffer-org/plugin-webchat 7.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ChatWidget-BhGpxS9f.js +2179 -0
- package/dist/index.js +2 -0
- package/dist/runtime/actions.d.ts +18 -2
- package/dist/runtime/actions.js +91 -12
- package/dist/runtime/chain-store.d.ts +20 -4
- package/dist/runtime/chain-store.js +86 -9
- package/dist/runtime/channel-registry.d.ts +16 -0
- package/dist/runtime/channel-registry.js +126 -0
- package/dist/runtime/config.js +0 -1
- package/dist/runtime/connector.d.ts +4 -7
- package/dist/runtime/connector.js +42 -48
- package/dist/runtime/format.d.ts +0 -1
- package/dist/runtime/format.js +2 -11
- package/dist/runtime/index.d.ts +1 -2
- package/dist/runtime/index.js +11 -9
- package/dist/runtime/send.d.ts +10 -7
- package/dist/runtime/send.js +93 -36
- package/dist/schema.js +463 -105
- package/dist/web.js +1 -1
- package/locales/en.json +3 -0
- package/locales/ru.json +3 -0
- package/locales/uk.json +3 -0
- package/package.json +5 -6
- package/dist/ChatWidget-DFVDexpD.js +0 -24553
package/dist/index.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { type ActionCaller } from '@coffer-org/server/plugin-hooks';
|
|
2
|
-
import { type AgentDescriptor } from '@coffer-org/server/orchestrator';
|
|
3
|
-
import { type ThreadSelection } from '
|
|
2
|
+
import { type AgentDescriptor, type ConnectorCapabilities } from '@coffer-org/server/orchestrator';
|
|
3
|
+
import { type Conversation as ThreadSelection } from '@coffer-org/server/conversation-store';
|
|
4
|
+
export interface StreamCtx {
|
|
5
|
+
caller: ActionCaller;
|
|
6
|
+
emit: (event: string, data: unknown) => void;
|
|
7
|
+
signal: AbortSignal;
|
|
8
|
+
}
|
|
4
9
|
export interface ThreadSummary {
|
|
5
10
|
convId: string;
|
|
6
11
|
title: string;
|
|
7
12
|
lastTs: number;
|
|
8
13
|
count: number;
|
|
14
|
+
ownerName: string | null;
|
|
15
|
+
mine: boolean;
|
|
16
|
+
private: boolean;
|
|
9
17
|
}
|
|
10
18
|
export interface HistoryMsg {
|
|
11
19
|
msgId: string;
|
|
@@ -38,3 +46,11 @@ export declare function agentsAction(body: Record<string, unknown>, caller: Acti
|
|
|
38
46
|
export declare function selectAgentAction(body: Record<string, unknown>, caller: ActionCaller): Promise<{
|
|
39
47
|
selection: ThreadSelection;
|
|
40
48
|
}>;
|
|
49
|
+
export declare function channelAction(body: Record<string, unknown>, ctx: StreamCtx): Promise<void>;
|
|
50
|
+
export interface SetVisibilityDeps {
|
|
51
|
+
capabilities?: ConnectorCapabilities;
|
|
52
|
+
}
|
|
53
|
+
export declare function setVisibilityAction(body: Record<string, unknown>, caller: ActionCaller, deps?: SetVisibilityDeps): Promise<{
|
|
54
|
+
visibility: 'private' | null;
|
|
55
|
+
}>;
|
|
56
|
+
export declare function stopAction(body: Record<string, unknown>, caller: ActionCaller): Promise<void>;
|
package/dist/runtime/actions.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { HttpError } from '@coffer-org/server/plugin-hooks';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { SIDECAR_ROLES } from '@coffer-org/server/conversation-store';
|
|
3
|
+
import { listUsers } from '@coffer-org/server/auth-store';
|
|
4
|
+
import { abortTurn } from '@coffer-org/server/turn-gate';
|
|
5
|
+
import { listAgentCatalog, getDefaultAgentId, listRegisteredAgents, getConversationStarters, mayBePrivate, mayRead, } from '@coffer-org/server/orchestrator';
|
|
6
|
+
import { getConversation, setConversation, listConversations, readConversation, } from '@coffer-org/server/conversation-store';
|
|
7
|
+
import { mayWrite } from '@coffer-org/server/orchestrator';
|
|
8
|
+
import { CAPABILITIES } from "./send.js";
|
|
9
|
+
import { subscribe, evictWhere } from "./channel-registry.js";
|
|
4
10
|
const TITLE_MAX = 60;
|
|
5
11
|
function parseSuggestions(text) {
|
|
6
12
|
try {
|
|
@@ -16,14 +22,18 @@ function title(firstUserText) {
|
|
|
16
22
|
return t.length <= TITLE_MAX ? t : `${t.slice(0, TITLE_MAX - 1)}…`;
|
|
17
23
|
}
|
|
18
24
|
export async function threadsAction(_body, caller) {
|
|
19
|
-
const chats = await
|
|
20
|
-
const
|
|
25
|
+
const chats = await listConversations(caller.id).then((rows) => rows.map((r) => ({ ...r, convId: r.id })));
|
|
26
|
+
const users = await listUsers();
|
|
27
|
+
const nameById = new Map(users.map((u) => [String(u.id), u.displayName]));
|
|
21
28
|
return {
|
|
22
29
|
threads: chats.map((c) => ({
|
|
23
|
-
convId: c.
|
|
24
|
-
title: title(c.firstUserText),
|
|
30
|
+
convId: c.convId,
|
|
31
|
+
title: c.title ?? title(c.firstUserText),
|
|
25
32
|
lastTs: c.lastTs,
|
|
26
33
|
count: c.count,
|
|
34
|
+
ownerName: c.owner === null ? null : (nameById.get(c.owner) ?? null),
|
|
35
|
+
mine: c.owner === caller.id,
|
|
36
|
+
private: c.visibility === 'private',
|
|
27
37
|
})),
|
|
28
38
|
};
|
|
29
39
|
}
|
|
@@ -33,15 +43,27 @@ function requireConvId(body) {
|
|
|
33
43
|
throw new HttpError(400, 'missing required field: convId');
|
|
34
44
|
return convId;
|
|
35
45
|
}
|
|
46
|
+
const EMPTY_SELECTION = {
|
|
47
|
+
id: '',
|
|
48
|
+
agentId: null,
|
|
49
|
+
presetId: null,
|
|
50
|
+
title: null,
|
|
51
|
+
owner: null,
|
|
52
|
+
visibility: null,
|
|
53
|
+
updatedAt: '',
|
|
54
|
+
};
|
|
36
55
|
export async function historyAction(body, caller) {
|
|
37
56
|
const convId = requireConvId(body);
|
|
38
|
-
const
|
|
57
|
+
const conversationId = await openConversation(convId, caller.id, 'read');
|
|
58
|
+
if (conversationId === null)
|
|
59
|
+
return { convId, messages: [], headMsgId: null };
|
|
60
|
+
const rows = await readConversation(conversationId);
|
|
39
61
|
const reasoningByBot = new Map(rows.filter((m) => m.role === 'reasoning').map((m) => [m.msgId.slice(0, -2), m.text]));
|
|
40
62
|
const suggestionsByBot = new Map(rows
|
|
41
63
|
.filter((m) => m.role === 'suggestions')
|
|
42
64
|
.map((m) => [m.msgId.slice(0, -2), parseSuggestions(m.text)])
|
|
43
65
|
.filter((entry) => entry[1] !== null));
|
|
44
|
-
const visible = rows.filter((m) =>
|
|
66
|
+
const visible = rows.filter((m) => !SIDECAR_ROLES.includes(m.role));
|
|
45
67
|
return {
|
|
46
68
|
convId,
|
|
47
69
|
messages: visible.map((m) => ({
|
|
@@ -58,16 +80,19 @@ export async function historyAction(body, caller) {
|
|
|
58
80
|
}
|
|
59
81
|
export async function agentsAction(body, caller) {
|
|
60
82
|
const convId = requireConvId(body);
|
|
83
|
+
const conversationId = await openConversation(convId, caller.id, 'read');
|
|
61
84
|
return {
|
|
62
85
|
agents: await listAgentCatalog(),
|
|
63
86
|
defaultAgentId: getDefaultAgentId() ?? null,
|
|
64
|
-
selection: await
|
|
87
|
+
selection: conversationId === null ? EMPTY_SELECTION : await getConversation(conversationId),
|
|
65
88
|
starters: await getConversationStarters(),
|
|
66
89
|
};
|
|
67
90
|
}
|
|
68
91
|
export async function selectAgentAction(body, caller) {
|
|
69
92
|
const convId = requireConvId(body);
|
|
70
|
-
const
|
|
93
|
+
const conversationId = await openConversation(convId, caller.id, 'write');
|
|
94
|
+
if (conversationId === null)
|
|
95
|
+
return { selection: EMPTY_SELECTION };
|
|
71
96
|
const patch = {};
|
|
72
97
|
if ('agentId' in body) {
|
|
73
98
|
const agentId = typeof body['agentId'] === 'string' && body['agentId'] ? body['agentId'] : null;
|
|
@@ -77,6 +102,60 @@ export async function selectAgentAction(body, caller) {
|
|
|
77
102
|
}
|
|
78
103
|
if ('presetId' in body)
|
|
79
104
|
patch.presetId = typeof body['presetId'] === 'string' && body['presetId'] ? body['presetId'] : null;
|
|
80
|
-
await
|
|
81
|
-
return { selection: await
|
|
105
|
+
await setConversation(conversationId, patch);
|
|
106
|
+
return { selection: await getConversation(conversationId) };
|
|
107
|
+
}
|
|
108
|
+
export async function channelAction(body, ctx) {
|
|
109
|
+
const convId = requireConvId(body);
|
|
110
|
+
const conversationId = await openConversation(convId, ctx.caller.id, 'read');
|
|
111
|
+
if (conversationId === null)
|
|
112
|
+
return;
|
|
113
|
+
let resolveHeld;
|
|
114
|
+
const held = new Promise((resolve) => {
|
|
115
|
+
resolveHeld = resolve;
|
|
116
|
+
});
|
|
117
|
+
const off = subscribe(conversationId, {
|
|
118
|
+
viewerId: ctx.caller.id,
|
|
119
|
+
emit: ctx.emit,
|
|
120
|
+
close: () => resolveHeld(),
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
if (!ctx.signal.aborted) {
|
|
124
|
+
ctx.signal.addEventListener('abort', () => resolveHeld(), { once: true });
|
|
125
|
+
await held;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
off();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export async function setVisibilityAction(body, caller, deps = {}) {
|
|
133
|
+
const convId = requireConvId(body);
|
|
134
|
+
const makePrivate = body['private'] === true;
|
|
135
|
+
const conversationId = await openConversation(convId, caller.id, 'write');
|
|
136
|
+
if (conversationId === null)
|
|
137
|
+
return { visibility: null };
|
|
138
|
+
const selection = await getConversation(conversationId);
|
|
139
|
+
if (selection.owner !== caller.id)
|
|
140
|
+
return { visibility: selection.visibility };
|
|
141
|
+
if (makePrivate && !mayBePrivate(deps.capabilities ?? CAPABILITIES))
|
|
142
|
+
return { visibility: selection.visibility };
|
|
143
|
+
const visibility = makePrivate ? 'private' : null;
|
|
144
|
+
await setConversation(conversationId, { visibility });
|
|
145
|
+
evictWhere(conversationId, (viewerId) => !mayRead({ owner: selection.owner, visibility }, viewerId));
|
|
146
|
+
return { visibility };
|
|
147
|
+
}
|
|
148
|
+
export async function stopAction(body, caller) {
|
|
149
|
+
const convId = requireConvId(body);
|
|
150
|
+
const conversationId = await openConversation(convId, caller.id, 'write');
|
|
151
|
+
if (conversationId === null)
|
|
152
|
+
return;
|
|
153
|
+
abortTurn('webchat', conversationId);
|
|
154
|
+
}
|
|
155
|
+
async function openConversation(convId, viewerId, need) {
|
|
156
|
+
if (!convId || convId.includes(':'))
|
|
157
|
+
return null;
|
|
158
|
+
const conv = await getConversation(convId);
|
|
159
|
+
const allowed = need === 'read' ? mayRead(conv, viewerId) : mayWrite(conv, viewerId);
|
|
160
|
+
return allowed ? convId : null;
|
|
82
161
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type StoredMsg } from '@coffer-org/server/thread-store';
|
|
2
2
|
import { type ThreadSelection } from '@coffer-org/server/thread-state';
|
|
3
|
-
import type { ConvMessage } from '@coffer-org/server/orchestrator';
|
|
3
|
+
import type { ConvMessage, ContextFact } from '@coffer-org/server/orchestrator';
|
|
4
4
|
export declare const CONNECTOR = "webchat";
|
|
5
5
|
export declare function chatIdFor(userId: string, convId: string): string;
|
|
6
|
-
export declare function chatPrefixFor(userId: string): string;
|
|
7
6
|
export declare function recordUser(m: {
|
|
8
7
|
chatId: string;
|
|
9
8
|
msgId: string;
|
|
@@ -34,13 +33,30 @@ export declare function recordSuggestions(m: {
|
|
|
34
33
|
suggestions: string[];
|
|
35
34
|
now: number;
|
|
36
35
|
}): Promise<boolean>;
|
|
36
|
+
export declare function recordContext(m: {
|
|
37
|
+
chatId: string;
|
|
38
|
+
userMsgId: string;
|
|
39
|
+
facts: ContextFact[];
|
|
40
|
+
ts: number;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
export declare function userTurnCount(chatId: string): Promise<number>;
|
|
37
43
|
export declare function buildChain(headMsgId: string, opts: {
|
|
38
44
|
chatId: string;
|
|
39
45
|
maxDepth?: number;
|
|
40
46
|
}): Promise<ConvMessage[]>;
|
|
41
47
|
export declare function history(chatId: string, limit?: number): Promise<StoredMsg[]>;
|
|
42
|
-
export
|
|
48
|
+
export interface ConversationSummary {
|
|
49
|
+
convId: string;
|
|
50
|
+
lastTs: number;
|
|
51
|
+
count: number;
|
|
52
|
+
firstUserText: string;
|
|
53
|
+
title: string | null;
|
|
54
|
+
owner: string | null;
|
|
55
|
+
visibility: 'private' | null;
|
|
56
|
+
}
|
|
57
|
+
export declare function conversations(viewerId: string): Promise<ConversationSummary[]>;
|
|
43
58
|
export type { ThreadSelection };
|
|
59
|
+
export declare function openConversation(convId: string, viewerId: string, need: 'read' | 'write'): Promise<string | null>;
|
|
44
60
|
export declare function getSelection(chatId: string): Promise<ThreadSelection>;
|
|
45
61
|
export declare function setSelection(chatId: string, patch: Partial<ThreadSelection>): Promise<void>;
|
|
46
62
|
export declare function readSelectionForTurn(chatId: string): Promise<ThreadSelection>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getThreadMessage, putThreadMessage, listThreadMessages,
|
|
2
|
-
import { getThreadState, setThreadState, readAndTouchThreadState, } from '@coffer-org/server/thread-state';
|
|
1
|
+
import { getThreadMessage, putThreadMessage, listThreadMessages, listAllThreadChats, countUserTurns, SIDECAR_ROLES, } from '@coffer-org/server/thread-store';
|
|
2
|
+
import { getThreadState, getThreadStates, setThreadState, readAndTouchThreadState, findChatByConvId, } from '@coffer-org/server/thread-state';
|
|
3
|
+
import { CONNECTOR_FACT_NAME, mayRead, mayWrite } from '@coffer-org/server/orchestrator';
|
|
3
4
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
4
5
|
const log = getLogger('webchat');
|
|
5
6
|
export const CONNECTOR = 'webchat';
|
|
@@ -7,8 +8,10 @@ const DEFAULT_MAX_DEPTH = 40;
|
|
|
7
8
|
export function chatIdFor(userId, convId) {
|
|
8
9
|
return `u:${userId}:${convId}`;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
function convIdOf(chatId) {
|
|
12
|
+
const first = chatId.indexOf(':');
|
|
13
|
+
const second = first === -1 ? -1 : chatId.indexOf(':', first + 1);
|
|
14
|
+
return second === -1 ? null : chatId.slice(second + 1);
|
|
12
15
|
}
|
|
13
16
|
export async function recordUser(m) {
|
|
14
17
|
await putThreadMessage({
|
|
@@ -65,17 +68,46 @@ export async function recordSuggestions(m) {
|
|
|
65
68
|
});
|
|
66
69
|
return true;
|
|
67
70
|
}
|
|
71
|
+
export async function recordContext(m) {
|
|
72
|
+
await putThreadMessage({
|
|
73
|
+
connector: CONNECTOR,
|
|
74
|
+
chatId: m.chatId,
|
|
75
|
+
msgId: `${m.userMsgId}~c`,
|
|
76
|
+
role: 'context',
|
|
77
|
+
text: JSON.stringify(m.facts),
|
|
78
|
+
ts: m.ts,
|
|
79
|
+
replyToId: null,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function parseFacts(text) {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(text);
|
|
85
|
+
return Array.isArray(parsed)
|
|
86
|
+
? parsed.filter((f) => typeof f?.name === 'string' &&
|
|
87
|
+
CONNECTOR_FACT_NAME.test(f.name) &&
|
|
88
|
+
typeof f.value === 'string' &&
|
|
89
|
+
Object.entries(f.attrs ?? {}).every(([k, v]) => CONNECTOR_FACT_NAME.test(k) && typeof v === 'string'))
|
|
90
|
+
: [];
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function userTurnCount(chatId) {
|
|
97
|
+
return countUserTurns(CONNECTOR, chatId);
|
|
98
|
+
}
|
|
68
99
|
export async function buildChain(headMsgId, opts) {
|
|
69
100
|
const max = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
70
101
|
const acc = [];
|
|
102
|
+
let turns = 0;
|
|
71
103
|
let cur = headMsgId;
|
|
72
104
|
const seen = new Set();
|
|
73
|
-
while (cur &&
|
|
105
|
+
while (cur && turns < max && !seen.has(cur)) {
|
|
74
106
|
seen.add(cur);
|
|
75
107
|
const stored = await getThreadMessage(CONNECTOR, opts.chatId, cur);
|
|
76
108
|
if (!stored)
|
|
77
109
|
break;
|
|
78
|
-
if (
|
|
110
|
+
if (!SIDECAR_ROLES.includes(stored.role)) {
|
|
79
111
|
acc.push({
|
|
80
112
|
role: stored.role,
|
|
81
113
|
content: stored.text,
|
|
@@ -84,10 +116,17 @@ export async function buildChain(headMsgId, opts) {
|
|
|
84
116
|
msgId: stored.msgId,
|
|
85
117
|
ts: stored.ts,
|
|
86
118
|
});
|
|
119
|
+
turns += 1;
|
|
120
|
+
if (stored.role === 'user') {
|
|
121
|
+
const ctx = await getThreadMessage(CONNECTOR, opts.chatId, `${stored.msgId}~c`);
|
|
122
|
+
const facts = ctx ? parseFacts(ctx.text) : [];
|
|
123
|
+
if (facts.length)
|
|
124
|
+
acc[acc.length - 1].context = facts;
|
|
125
|
+
}
|
|
87
126
|
}
|
|
88
127
|
cur = stored.replyToId;
|
|
89
128
|
}
|
|
90
|
-
if (
|
|
129
|
+
if (turns >= max)
|
|
91
130
|
log.info(`chain capped at ${max} for ${headMsgId}`);
|
|
92
131
|
while (acc.length && acc.at(-1)?.role !== 'user')
|
|
93
132
|
acc.pop();
|
|
@@ -96,8 +135,46 @@ export async function buildChain(headMsgId, opts) {
|
|
|
96
135
|
export function history(chatId, limit) {
|
|
97
136
|
return listThreadMessages(CONNECTOR, chatId, limit);
|
|
98
137
|
}
|
|
99
|
-
export function conversations(
|
|
100
|
-
|
|
138
|
+
export async function conversations(viewerId) {
|
|
139
|
+
const chats = await listAllThreadChats(CONNECTOR);
|
|
140
|
+
const states = await getThreadStates(CONNECTOR, chats.map((c) => c.chatId));
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const c of chats) {
|
|
143
|
+
const access = states.get(c.chatId) ?? {
|
|
144
|
+
agentId: null,
|
|
145
|
+
presetId: null,
|
|
146
|
+
title: null,
|
|
147
|
+
owner: null,
|
|
148
|
+
visibility: null,
|
|
149
|
+
};
|
|
150
|
+
if (!mayRead(access, viewerId))
|
|
151
|
+
continue;
|
|
152
|
+
const convId = convIdOf(c.chatId);
|
|
153
|
+
if (convId === null)
|
|
154
|
+
continue;
|
|
155
|
+
out.push({
|
|
156
|
+
convId,
|
|
157
|
+
lastTs: c.lastTs,
|
|
158
|
+
count: c.count,
|
|
159
|
+
firstUserText: c.firstUserText,
|
|
160
|
+
title: access.title,
|
|
161
|
+
owner: access.owner,
|
|
162
|
+
visibility: access.visibility,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
export async function openConversation(convId, viewerId, need) {
|
|
168
|
+
if (!convId || convId.includes(':'))
|
|
169
|
+
return null;
|
|
170
|
+
const lookup = await findChatByConvId(CONNECTOR, convId);
|
|
171
|
+
if (lookup.found === 'ambiguous')
|
|
172
|
+
return null;
|
|
173
|
+
if (lookup.found === 'none')
|
|
174
|
+
return chatIdFor(viewerId, convId);
|
|
175
|
+
const access = await getThreadState(CONNECTOR, lookup.chatId);
|
|
176
|
+
const allowed = need === 'read' ? mayRead(access, viewerId) : mayWrite(access, viewerId);
|
|
177
|
+
return allowed ? lookup.chatId : null;
|
|
101
178
|
}
|
|
102
179
|
export async function getSelection(chatId) {
|
|
103
180
|
return getThreadState(CONNECTOR, chatId);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface Subscriber {
|
|
2
|
+
viewerId: string;
|
|
3
|
+
emit(event: string, data: unknown): void;
|
|
4
|
+
close(): void;
|
|
5
|
+
}
|
|
6
|
+
export type ChannelCallback = (frame: {
|
|
7
|
+
event: string;
|
|
8
|
+
data: unknown;
|
|
9
|
+
}) => void;
|
|
10
|
+
export declare function subscribe(conversationId: string, sub: Subscriber | ChannelCallback): () => void;
|
|
11
|
+
export declare function subscribeAs(conversationId: string, viewerId: string, cb: ChannelCallback): Promise<() => void>;
|
|
12
|
+
export declare function broadcast(conversationId: string, event: string, data: unknown): void;
|
|
13
|
+
export declare function evictWhere(conversationId: string, drop: (viewerId: string) => boolean): void;
|
|
14
|
+
export declare function subscriberCount(conversationId: string): number;
|
|
15
|
+
export declare function initFanout(): () => void;
|
|
16
|
+
export declare function teardownFanout(): void;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
2
|
+
import { getConversation, onMessage, conversationEmitter, HIDDEN_ROLES } from '@coffer-org/server/conversation-store';
|
|
3
|
+
import { mayRead } from '@coffer-org/server/orchestrator';
|
|
4
|
+
const log = getLogger('webchat');
|
|
5
|
+
const channels = new Map();
|
|
6
|
+
export function subscribe(conversationId, sub) {
|
|
7
|
+
if (typeof sub === 'function') {
|
|
8
|
+
return subscribe(conversationId, {
|
|
9
|
+
viewerId: '*',
|
|
10
|
+
emit: (event, data) => sub({ event, data }),
|
|
11
|
+
close: () => { },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
let set = channels.get(conversationId);
|
|
15
|
+
if (!set) {
|
|
16
|
+
set = new Set();
|
|
17
|
+
channels.set(conversationId, set);
|
|
18
|
+
}
|
|
19
|
+
set.add(sub);
|
|
20
|
+
return () => {
|
|
21
|
+
const current = channels.get(conversationId);
|
|
22
|
+
if (!current)
|
|
23
|
+
return;
|
|
24
|
+
current.delete(sub);
|
|
25
|
+
if (current.size === 0)
|
|
26
|
+
channels.delete(conversationId);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export async function subscribeAs(conversationId, viewerId, cb) {
|
|
30
|
+
if (viewerId !== '*') {
|
|
31
|
+
try {
|
|
32
|
+
const conv = await getConversation(conversationId);
|
|
33
|
+
if (conv && !mayRead(conv, viewerId)) {
|
|
34
|
+
return () => { };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return subscribe(conversationId, {
|
|
41
|
+
viewerId,
|
|
42
|
+
emit: (event, data) => cb({ event, data }),
|
|
43
|
+
close: () => { },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function broadcast(conversationId, event, data) {
|
|
47
|
+
const set = channels.get(conversationId);
|
|
48
|
+
if (!set)
|
|
49
|
+
return;
|
|
50
|
+
for (const sub of set) {
|
|
51
|
+
try {
|
|
52
|
+
sub.emit(event, data);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
log.warn(`channel emit failed for viewer ${sub.viewerId} on ${conversationId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function evictWhere(conversationId, drop) {
|
|
60
|
+
const set = channels.get(conversationId);
|
|
61
|
+
if (!set)
|
|
62
|
+
return;
|
|
63
|
+
for (const sub of set) {
|
|
64
|
+
if (!drop(sub.viewerId))
|
|
65
|
+
continue;
|
|
66
|
+
set.delete(sub);
|
|
67
|
+
sub.close();
|
|
68
|
+
}
|
|
69
|
+
if (set.size === 0)
|
|
70
|
+
channels.delete(conversationId);
|
|
71
|
+
}
|
|
72
|
+
export function subscriberCount(conversationId) {
|
|
73
|
+
return channels.get(conversationId)?.size ?? 0;
|
|
74
|
+
}
|
|
75
|
+
let unsubscribeMessage;
|
|
76
|
+
let unsubscribeConversation;
|
|
77
|
+
export function initFanout() {
|
|
78
|
+
if (!unsubscribeMessage) {
|
|
79
|
+
unsubscribeMessage = onMessage((m) => {
|
|
80
|
+
if (HIDDEN_ROLES.includes(m.role))
|
|
81
|
+
return;
|
|
82
|
+
if (m.role === 'assistant') {
|
|
83
|
+
broadcast(m.conversationId, 'message', { msgId: m.msgId, text: m.text });
|
|
84
|
+
}
|
|
85
|
+
else if (m.role === 'user') {
|
|
86
|
+
broadcast(m.conversationId, 'user', {
|
|
87
|
+
msgId: m.msgId,
|
|
88
|
+
text: m.text,
|
|
89
|
+
ts: m.ts,
|
|
90
|
+
...(m.attachments?.length ? { attachments: m.attachments } : {}),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (!unsubscribeConversation) {
|
|
96
|
+
const handler = (e) => {
|
|
97
|
+
if (e instanceof CustomEvent) {
|
|
98
|
+
const { id, patch } = e.detail;
|
|
99
|
+
if (patch && (patch.visibility !== undefined || patch.owner !== undefined)) {
|
|
100
|
+
void (async () => {
|
|
101
|
+
try {
|
|
102
|
+
const conv = await getConversation(id);
|
|
103
|
+
evictWhere(id, (viewerId) => !mayRead(conv, viewerId));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
conversationEmitter.addEventListener('conversation', handler);
|
|
112
|
+
unsubscribeConversation = () => {
|
|
113
|
+
conversationEmitter.removeEventListener('conversation', handler);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return () => {
|
|
117
|
+
teardownFanout();
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function teardownFanout() {
|
|
121
|
+
unsubscribeMessage?.();
|
|
122
|
+
unsubscribeMessage = undefined;
|
|
123
|
+
unsubscribeConversation?.();
|
|
124
|
+
unsubscribeConversation = undefined;
|
|
125
|
+
}
|
|
126
|
+
initFanout();
|
package/dist/runtime/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Connector } from '@coffer-org/server/orchestrator';
|
|
2
|
-
import {
|
|
2
|
+
import { setConversation as storeSelection } from '@coffer-org/server/conversation-store';
|
|
3
3
|
import type { ReasoningDisplay } from './settings.ts';
|
|
4
4
|
export interface StreamingConnector {
|
|
5
5
|
connector: Connector;
|
|
@@ -7,12 +7,9 @@ export interface StreamingConnector {
|
|
|
7
7
|
suggestions(): string[] | null;
|
|
8
8
|
}
|
|
9
9
|
export interface StreamingConnectorOpts {
|
|
10
|
-
|
|
11
|
-
botMsgId
|
|
12
|
-
emit: (event: string, data: unknown) => void;
|
|
10
|
+
conversationId?: string;
|
|
11
|
+
botMsgId?: string;
|
|
13
12
|
display: ReasoningDisplay;
|
|
14
|
-
|
|
15
|
-
recordReasoning?: typeof storeReasoning;
|
|
16
|
-
recordSuggestions?: typeof storeSuggestions;
|
|
13
|
+
setSelection?: typeof storeSelection;
|
|
17
14
|
}
|
|
18
15
|
export declare function makeStreamingConnector(opts: StreamingConnectorOpts): StreamingConnector;
|
|
@@ -1,76 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { makeLiveSink, plainRender } from '@coffer-org/server/orchestrator';
|
|
2
|
+
import { setConversation as storeSelection } from '@coffer-org/server/conversation-store';
|
|
3
|
+
import { broadcast } from "./channel-registry.js";
|
|
4
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
5
|
+
const log = getLogger('webchat');
|
|
3
6
|
const WEBCHAT_THROTTLE_MS = 250;
|
|
4
7
|
const WEBCHAT_MAX = 100_000;
|
|
5
8
|
export function makeStreamingConnector(opts) {
|
|
6
|
-
const
|
|
7
|
-
const recordReasoningFn = opts.recordReasoning ?? storeReasoning;
|
|
8
|
-
const recordSuggestionsFn = opts.recordSuggestions ?? storeSuggestions;
|
|
9
|
+
const setSelectionFn = opts.setSelection ?? storeSelection;
|
|
9
10
|
let recordedId = null;
|
|
10
11
|
let lastSuggestions = null;
|
|
12
|
+
let hasAnswer = false;
|
|
11
13
|
const connector = {
|
|
12
14
|
id: 'webchat',
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
+
open(envelope, msgId) {
|
|
16
|
+
const activeBotMsgId = msgId ?? opts.botMsgId ?? '';
|
|
17
|
+
const conversationId = envelope.conversationId ?? opts.conversationId ?? '';
|
|
18
|
+
const inner = makeLiveSink({
|
|
15
19
|
ops: {
|
|
16
20
|
send: async (text) => {
|
|
17
|
-
|
|
18
|
-
return
|
|
21
|
+
broadcast(conversationId, 'delta', { msgId: activeBotMsgId, text });
|
|
22
|
+
return activeBotMsgId;
|
|
19
23
|
},
|
|
20
|
-
edit: async (
|
|
21
|
-
|
|
24
|
+
edit: async (id, text) => {
|
|
25
|
+
broadcast(conversationId, 'delta', { msgId: id, text });
|
|
22
26
|
},
|
|
23
27
|
},
|
|
24
28
|
throttleMs: WEBCHAT_THROTTLE_MS,
|
|
25
29
|
maxLength: WEBCHAT_MAX,
|
|
26
30
|
render: plainRender(WEBCHAT_MAX),
|
|
27
31
|
});
|
|
32
|
+
let afterwordWork = Promise.resolve();
|
|
28
33
|
return {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
34
|
+
emit(e) {
|
|
35
|
+
if (e.kind === 'reasoning') {
|
|
36
|
+
if (opts.display !== 'off')
|
|
37
|
+
broadcast(conversationId, 'reasoning', { msgId: activeBotMsgId, text: e.text });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (e.kind === 'suggestions') {
|
|
41
|
+
lastSuggestions = e.items;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (e.kind === 'title') {
|
|
45
|
+
afterwordWork = afterwordWork.then(() => setSelectionFn(conversationId, { title: e.text }).catch((err) => {
|
|
46
|
+
log.warn(`setSelection(title) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
47
|
+
}));
|
|
36
48
|
return;
|
|
37
|
-
|
|
49
|
+
}
|
|
50
|
+
if (e.kind === 'answer') {
|
|
51
|
+
hasAnswer = true;
|
|
52
|
+
recordedId = activeBotMsgId;
|
|
53
|
+
}
|
|
54
|
+
inner.emit(e);
|
|
55
|
+
},
|
|
56
|
+
async done() {
|
|
57
|
+
await inner.done();
|
|
58
|
+
await afterwordWork;
|
|
38
59
|
},
|
|
39
60
|
};
|
|
40
61
|
},
|
|
41
|
-
async
|
|
42
|
-
const now = Math.floor(Date.now() / 1000);
|
|
43
|
-
const wrote = await record({
|
|
44
|
-
chatId: opts.chatId,
|
|
45
|
-
parentMsgId: m.parentMsgId,
|
|
46
|
-
botMsgId: m.botMsgId,
|
|
47
|
-
text: m.text,
|
|
48
|
-
now,
|
|
49
|
-
});
|
|
50
|
-
recordedId = wrote ? m.botMsgId : null;
|
|
51
|
-
if (wrote && opts.display !== 'off' && m.reasoning && m.botMsgId) {
|
|
52
|
-
await recordReasoningFn({
|
|
53
|
-
chatId: opts.chatId,
|
|
54
|
-
botMsgId: m.botMsgId,
|
|
55
|
-
parentMsgId: m.parentMsgId,
|
|
56
|
-
text: m.reasoning,
|
|
57
|
-
now,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
if (wrote && lastSuggestions?.length && m.botMsgId) {
|
|
61
|
-
await recordSuggestionsFn({
|
|
62
|
-
chatId: opts.chatId,
|
|
63
|
-
botMsgId: m.botMsgId,
|
|
64
|
-
parentMsgId: m.parentMsgId,
|
|
65
|
-
suggestions: lastSuggestions,
|
|
66
|
-
now,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
62
|
+
async bindMessage(_envelope, _msgId, _externalId) {
|
|
69
63
|
},
|
|
70
64
|
};
|
|
71
65
|
return {
|
|
72
66
|
connector,
|
|
73
|
-
recorded: () => recordedId,
|
|
67
|
+
recorded: () => (hasAnswer ? recordedId : null),
|
|
74
68
|
suggestions: () => lastSuggestions,
|
|
75
69
|
};
|
|
76
70
|
}
|
package/dist/runtime/format.d.ts
CHANGED