@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/runtime/format.js
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import { frontendInstructions } from '@coffer-org/server/frontend-agent';
|
|
2
|
-
|
|
3
|
-
'CRITICAL OUTPUT FORMAT — MARKDOWN ONLY. Format the reply strictly as Markdown (GFM). NEVER emit raw HTML tags — they are stripped before rendering and their content may be lost.',
|
|
4
|
-
'Inline: **bold**, *italic*, `inline code`, ~~strikethrough~~. Links: [text](https://…).',
|
|
5
|
-
'Blocks: blank-line-separated paragraphs, `#`–`###` headings, `>` blockquotes, `---` separators.',
|
|
6
|
-
'Lists: `- item` for bullets, `1. item` for numbered steps. Nest with two spaces.',
|
|
7
|
-
'Tables render as real tables — use GFM pipe syntax with a header separator row.',
|
|
8
|
-
'Code blocks: triple backticks with a language tag (```python) — highlighted on render.',
|
|
9
|
-
'The reply is read in a narrow chat panel (~360px) — keep it short and skimmable: short paragraphs, lists over prose, narrow tables.',
|
|
10
|
-
'Use Unicode symbols and emoji to structure replies: arrows (→), status marks (✅ ⚠️ ❌ ⏳), topic emoji (📅 💰 🏥 🛒 🌱 …).',
|
|
11
|
-
].join('\n');
|
|
2
|
+
import { NEUTRAL_FORMAT } from '@coffer-org/markdown';
|
|
12
3
|
export async function webChannelSystem() {
|
|
13
4
|
const site = await frontendInstructions('');
|
|
14
|
-
return [
|
|
5
|
+
return [NEUTRAL_FORMAT, site].filter(Boolean).join('\n\n');
|
|
15
6
|
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { PluginHooks } from '@coffer-org/server/plugin-hooks';
|
|
2
2
|
export type { ThreadSummary, HistoryMsg } from './actions.ts';
|
|
3
|
-
export { WEB_FORMAT } from './format.ts';
|
|
4
3
|
export { makeStreamingConnector } from './connector.ts';
|
|
5
|
-
export {
|
|
4
|
+
export { getConversation as getSelection, setConversation as setSelection, } from '@coffer-org/server/conversation-store';
|
|
6
5
|
export { sendAction, pageContext } from './send.ts';
|
|
7
6
|
export declare const serverHooks: PluginHooks;
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { threadsAction, historyAction, agentsAction, selectAgentAction } from "./actions.js";
|
|
1
|
+
import { pruneConversations } from '@coffer-org/server/conversation-store';
|
|
2
|
+
import { threadsAction, historyAction, agentsAction, selectAgentAction, setVisibilityAction, channelAction, stopAction, } from "./actions.js";
|
|
4
3
|
import { sendAction } from "./send.js";
|
|
5
|
-
import { CONNECTOR } from "./chain-store.js";
|
|
6
4
|
import { registerConnector } from '@coffer-org/server/orchestrator';
|
|
5
|
+
import { initFanout, teardownFanout } from "./channel-registry.js";
|
|
7
6
|
const THREAD_TTL_MS = Number(process.env['WEBCHAT_THREAD_TTL_MS'] ?? 30 * 86_400_000);
|
|
8
7
|
let unregisterConnector;
|
|
9
|
-
export { WEB_FORMAT } from "./format.js";
|
|
10
8
|
export { makeStreamingConnector } from "./connector.js";
|
|
11
|
-
export {
|
|
9
|
+
export { getConversation as getSelection, setConversation as setSelection, } from '@coffer-org/server/conversation-store';
|
|
12
10
|
export { sendAction, pageContext } from "./send.js";
|
|
13
11
|
export const serverHooks = {
|
|
14
12
|
init: () => {
|
|
15
13
|
unregisterConnector = registerConnector({ id: 'webchat' });
|
|
14
|
+
initFanout();
|
|
16
15
|
},
|
|
17
16
|
teardown: () => {
|
|
18
17
|
unregisterConnector?.();
|
|
19
18
|
unregisterConnector = undefined;
|
|
19
|
+
teardownFanout();
|
|
20
20
|
},
|
|
21
21
|
userActions: {
|
|
22
22
|
threads: threadsAction,
|
|
23
23
|
history: historyAction,
|
|
24
24
|
agents: agentsAction,
|
|
25
25
|
selectAgent: selectAgentAction,
|
|
26
|
+
setVisibility: (body, caller) => setVisibilityAction(body, caller),
|
|
27
|
+
send: (body, caller) => sendAction(body, caller),
|
|
28
|
+
stop: (body, caller) => stopAction(body, caller),
|
|
26
29
|
},
|
|
27
30
|
streamActions: {
|
|
28
|
-
|
|
31
|
+
channel: (body, ctx) => channelAction(body, ctx),
|
|
29
32
|
},
|
|
30
33
|
backgroundTasks: [
|
|
31
34
|
{
|
|
@@ -33,8 +36,7 @@ export const serverHooks = {
|
|
|
33
36
|
intervalMs: 86_400_000,
|
|
34
37
|
run: async () => {
|
|
35
38
|
const cutoff = new Date(Date.now() - THREAD_TTL_MS);
|
|
36
|
-
await
|
|
37
|
-
await pruneThreadState(CONNECTOR, cutoff.toISOString());
|
|
39
|
+
await pruneConversations(Math.floor(cutoff.getTime() / 1000), cutoff.toISOString());
|
|
38
40
|
},
|
|
39
41
|
},
|
|
40
42
|
],
|
package/dist/runtime/send.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { handleIncoming } from '@coffer-org/server/orchestrator';
|
|
2
|
-
import type
|
|
2
|
+
import { type ActionCaller } from '@coffer-org/server/plugin-hooks';
|
|
3
|
+
import { conversationEmitter } from '@coffer-org/server/conversation-store';
|
|
4
|
+
import type { ConnectorCapabilities, ContextFact } from '@coffer-org/server/orchestrator';
|
|
5
|
+
export { conversationEmitter };
|
|
6
|
+
export declare const CAPABILITIES: ConnectorCapabilities;
|
|
3
7
|
export interface SendDeps {
|
|
4
8
|
handleIncoming?: typeof handleIncoming;
|
|
5
9
|
}
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
export declare function pageContext(ctx: unknown): ContextFact[];
|
|
11
|
+
export interface SendResult {
|
|
12
|
+
msgId: string | null;
|
|
13
|
+
botMsgId: string | null;
|
|
10
14
|
}
|
|
11
|
-
export declare function
|
|
12
|
-
export declare function sendAction(body: Record<string, unknown>, ctx: StreamCtx, deps?: SendDeps): Promise<void>;
|
|
15
|
+
export declare function sendAction(body: Record<string, unknown>, caller: ActionCaller, deps?: SendDeps): Promise<SendResult>;
|
package/dist/runtime/send.js
CHANGED
|
@@ -1,65 +1,115 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { handleIncoming, liveAgentId } from '@coffer-org/server/orchestrator';
|
|
3
|
-
import {
|
|
3
|
+
import { beginTurn } from '@coffer-org/server/turn-gate';
|
|
4
|
+
import { HttpError } from '@coffer-org/server/plugin-hooks';
|
|
5
|
+
import { lastMessageId, conversationEmitter, onMessage, } from '@coffer-org/server/conversation-store';
|
|
6
|
+
import { getConversation, setConversation, readAndTouchConversation, } from '@coffer-org/server/conversation-store';
|
|
7
|
+
import { mayWrite, mayRead } from '@coffer-org/server/orchestrator';
|
|
4
8
|
import { policy } from "./config.js";
|
|
5
9
|
import { makeStreamingConnector } from "./connector.js";
|
|
10
|
+
import { broadcast, initFanout } from "./channel-registry.js";
|
|
6
11
|
import { webChannelSystem } from "./format.js";
|
|
7
12
|
import { loadReasoningDisplay } from "./settings.js";
|
|
13
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
14
|
+
const log = getLogger('webchat');
|
|
15
|
+
initFanout();
|
|
16
|
+
export { conversationEmitter };
|
|
17
|
+
export const CAPABILITIES = {
|
|
18
|
+
events: ['delta', 'reasoning', 'segment', 'suggestions', 'title'],
|
|
19
|
+
privateChats: true,
|
|
20
|
+
};
|
|
8
21
|
export function pageContext(ctx) {
|
|
9
22
|
if (typeof ctx !== 'object' || ctx === null)
|
|
10
|
-
return
|
|
23
|
+
return [];
|
|
11
24
|
const c = ctx;
|
|
12
25
|
const path = typeof c['path'] === 'string' ? c['path'] : '';
|
|
13
26
|
if (!path)
|
|
14
|
-
return
|
|
27
|
+
return [];
|
|
15
28
|
const library = typeof c['library'] === 'string' ? c['library'] : '';
|
|
16
|
-
const
|
|
29
|
+
const shelf = typeof c['type'] === 'string' ? c['type'] : '';
|
|
17
30
|
const id = typeof c['id'] === 'string' ? c['id'] : '';
|
|
18
|
-
const
|
|
19
|
-
|
|
31
|
+
const title = typeof c['title'] === 'string' ? c['title'] : '';
|
|
32
|
+
if (library && shelf && id)
|
|
33
|
+
return [{ name: 'record', value: title, attrs: { library, shelf, id } }];
|
|
34
|
+
if (library && shelf)
|
|
35
|
+
return [{ name: 'page', value: '', attrs: { library, shelf } }];
|
|
36
|
+
return [{ name: 'page', value: path }];
|
|
20
37
|
}
|
|
21
|
-
export async function sendAction(body,
|
|
38
|
+
export async function sendAction(body, caller, deps = {}) {
|
|
22
39
|
const doHandle = deps.handleIncoming ?? handleIncoming;
|
|
23
40
|
const convId = typeof body['convId'] === 'string' ? body['convId'] : '';
|
|
24
41
|
const text = typeof body['text'] === 'string' ? body['text'] : '';
|
|
25
|
-
if (!convId)
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
if (!convId)
|
|
43
|
+
throw new HttpError(400, 'missing required field: convId');
|
|
44
|
+
if (!text.trim())
|
|
45
|
+
throw new HttpError(400, 'missing required field: text');
|
|
46
|
+
const conversationId = await openConversation(convId, caller.id, 'write');
|
|
47
|
+
if (conversationId === null) {
|
|
48
|
+
return { msgId: null, botMsgId: null };
|
|
28
49
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
const selection = await readAndTouchConversation(conversationId);
|
|
51
|
+
if (selection.owner === null) {
|
|
52
|
+
await setConversation(conversationId, { owner: caller.id });
|
|
32
53
|
}
|
|
33
|
-
const chatId = chatIdFor(ctx.caller.id, convId);
|
|
34
|
-
const selection = await readSelectionForTurn(chatId);
|
|
35
54
|
const agentId = liveAgentId(selection.agentId);
|
|
36
55
|
const msgId = randomUUID();
|
|
37
56
|
const botMsgId = randomUUID();
|
|
38
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
39
57
|
const attachments = parseAttachments(body['attachments']);
|
|
40
58
|
const explicitReplyTo = typeof body['replyTo'] === 'string' && body['replyTo'] ? body['replyTo'] : null;
|
|
41
|
-
const replyTo = explicitReplyTo ?? (await
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
const replyTo = explicitReplyTo ?? (await lastMessageId(conversationId));
|
|
60
|
+
let userMsgId = msgId;
|
|
61
|
+
let userMsgResolved = false;
|
|
62
|
+
let resolveUserMsg;
|
|
63
|
+
const userMsgPromise = new Promise((resolve) => {
|
|
64
|
+
resolveUserMsg = resolve;
|
|
65
|
+
});
|
|
66
|
+
const unsubUserMsg = onMessage((m) => {
|
|
67
|
+
if (!userMsgResolved && m.conversationId === conversationId && m.role === 'user') {
|
|
68
|
+
userMsgResolved = true;
|
|
69
|
+
resolveUserMsg(m.msgId);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
44
72
|
const { connector, recorded, suggestions } = makeStreamingConnector({
|
|
45
|
-
|
|
73
|
+
conversationId,
|
|
46
74
|
botMsgId,
|
|
47
|
-
emit: ctx.emit,
|
|
48
75
|
display: await loadReasoningDisplay(),
|
|
49
76
|
});
|
|
50
77
|
const turnContext = pageContext(body['context']);
|
|
51
|
-
await
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
const gate = await beginTurn('webchat', conversationId, { supersede: true });
|
|
79
|
+
const turnPromise = (async () => {
|
|
80
|
+
try {
|
|
81
|
+
await doHandle(connector, {
|
|
82
|
+
envelope: { connectorId: 'webchat', conversationId, turnId: msgId },
|
|
83
|
+
sender: { userId: Number(caller.id) },
|
|
84
|
+
message: {
|
|
85
|
+
text,
|
|
86
|
+
...(attachments?.length ? { attachments } : {}),
|
|
87
|
+
replyToMsgId: replyTo,
|
|
88
|
+
},
|
|
89
|
+
capabilities: CAPABILITIES,
|
|
90
|
+
systemPrompt: { channel: [await webChannelSystem()] },
|
|
91
|
+
...(turnContext.length ? { turnContext } : {}),
|
|
92
|
+
...(agentId ? { agentId } : {}),
|
|
93
|
+
...(selection.presetId ? { presetId: selection.presetId } : {}),
|
|
94
|
+
signal: gate.signal,
|
|
95
|
+
}, { policy: policy() });
|
|
96
|
+
broadcast(conversationId, 'done', {
|
|
97
|
+
msgId: recorded() ?? botMsgId,
|
|
98
|
+
parentMsgId: userMsgId,
|
|
99
|
+
suggestions: suggestions(),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
log.error(`sendAction: handleIncoming threw for chat ${conversationId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
|
+
broadcast(conversationId, 'error', { message: null, parentMsgId: userMsgId });
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
gate.end();
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
userMsgId = await Promise.race([userMsgPromise, turnPromise.then(() => msgId)]);
|
|
111
|
+
unsubUserMsg();
|
|
112
|
+
return { msgId: userMsgId, botMsgId };
|
|
63
113
|
}
|
|
64
114
|
function parseAttachments(value) {
|
|
65
115
|
if (!Array.isArray(value))
|
|
@@ -73,11 +123,18 @@ function parseAttachments(value) {
|
|
|
73
123
|
return [
|
|
74
124
|
{
|
|
75
125
|
name: r['name'],
|
|
76
|
-
...(typeof r['mime'] === 'string' ? { mime: r['mime'] } : {}),
|
|
77
|
-
...(typeof r['size'] === 'number' ? { size: r['size'] } : {}),
|
|
78
|
-
...(typeof r['label'] === 'string' ? { label: r['label'] } : {}),
|
|
126
|
+
...(typeof r['mime'] === 'string' && r['mime'] ? { mime: r['mime'] } : {}),
|
|
127
|
+
...(typeof r['size'] === 'number' && Number.isFinite(r['size']) ? { size: r['size'] } : {}),
|
|
128
|
+
...(typeof r['label'] === 'string' && r['label'] ? { label: r['label'] } : {}),
|
|
79
129
|
},
|
|
80
130
|
];
|
|
81
131
|
});
|
|
82
132
|
return refs.length ? refs : undefined;
|
|
83
133
|
}
|
|
134
|
+
async function openConversation(convId, viewerId, need) {
|
|
135
|
+
if (!convId || convId.includes(':'))
|
|
136
|
+
return null;
|
|
137
|
+
const conv = await getConversation(convId);
|
|
138
|
+
const allowed = need === 'read' ? mayRead(conv, viewerId) : mayWrite(conv, viewerId);
|
|
139
|
+
return allowed ? convId : null;
|
|
140
|
+
}
|