@mercury-fw/channel-google-chat 0.8.0 → 0.8.2
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/CHANGELOG.md +16 -0
- package/README.md +2 -0
- package/dist/google-chat-provider.d.ts +14 -5
- package/google-chat-provider.ts +70 -9
- package/index.ts +1 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @mercury-fw/channel-google-chat
|
|
2
2
|
|
|
3
|
+
## 0.8.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- edfebb5: - `@mercury-fw/plugin-web-search`: an answer no longer lists its sources twice. When the model writes its own list of sources at the end of the answer, the plugin's `Fonti:` list replaces it, and `web_fetch`'s description tells the model the pages it reads are listed for it.
|
|
8
|
+
- `@mercury-fw/channel-google-chat`: once a choice on a confirmation card is taken, the card shows it ("Scelto: …") instead of the buttons. A click that confirms nothing (expired, or someone else's in a shared space) leaves the card as it is.
|
|
9
|
+
- `@mercury-fw/channel-types`: `confirmReply(outcome)`, the reply to each confirmation outcome, for a channel that resolves a token through `resolveConfirmation`.
|
|
10
|
+
- `@mercury-fw/confirm-engine`: `tryConfirm` answers with `confirmReply`, same texts as before.
|
|
11
|
+
|
|
12
|
+
## 0.8.1
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 4431285: - plugin-web-search README: the minimum core (0.53), the Italian labels the person reads, where a person's list lives and how to edit it, and a config example matching the app `mfw create` writes.
|
|
17
|
+
- channel-google-chat README: the confirmation card has a button per choice, and a choice whose result goes back to the model runs the turn again.
|
|
18
|
+
|
|
3
19
|
## 0.8.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -28,6 +28,8 @@ Each message reaches the core as `google-chat:users/<id>`, the sender's Chat use
|
|
|
28
28
|
|
|
29
29
|
When a plugin that acts as the person (Jira, Bitbucket) needs them to log in, the channel sends a card with a button that opens the service's login, visible only to that person: in a direct message the space is already theirs, in a shared space the card is a private message to whoever asked, since anyone else opening it would log their own account in as them. The service sends the browser back to the HTTP channel's `/login/callback`, so the app needs [`@mercury-fw/channel-http`](https://www.npmjs.com/package/@mercury-fw/channel-http) with `HTTP_SURFACE_PUBLIC_URL` too; without it Mercury answers that nobody can log in on this instance.
|
|
30
30
|
|
|
31
|
+
When a tool stages an action that needs confirming, the channel sends a card with the action and a button per choice the tool offers: one `Conferma` for a plain staging (deleting a Jira issue, say), more when there's a choice to make (the web search plugin offers "Solo questa volta" and "Sempre per \<host\>" before reading a page on a host nobody allowed). Clicking a button confirms that choice and the others die with it, and the card shows the choice taken instead of the buttons; typing the token in the conversation does the same, leaving the card as it is. A click that confirms nothing (the request expired, or in a shared space it's someone else's) leaves the card for whoever has to choose. When the confirmed action's result goes back to the model (a page read after the person allowed it), the channel runs the conversation's turn again instead of sending the raw result, so the answer comes from the model, built on what came back.
|
|
32
|
+
|
|
31
33
|
## Table of contents
|
|
32
34
|
|
|
33
35
|
- [Setting up the Chat app](#setting-up-the-chat-app)
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { sendMessage, sendCard, updateCard, getOrCreateDmSpace, type ServiceAccountCredentials, type TokenSource } from "./google-chat-app-client.ts";
|
|
23
23
|
import { type PubSubSubscription } from "./google-chat-pubsub-stream.ts";
|
|
24
|
-
import { type Provider, type Principal } from "@mercury-fw/channel-types";
|
|
24
|
+
import { type ConfirmOutcome, type Provider, type Principal } from "@mercury-fw/channel-types";
|
|
25
25
|
/**
|
|
26
26
|
* Composite session key: space + sender. Deliberately does NOT include
|
|
27
27
|
* `thread` — confirmed live (two consecutive messages in the same DM
|
|
@@ -54,6 +54,8 @@ export type ParsedMessageEvent = {
|
|
|
54
54
|
};
|
|
55
55
|
export type ParsedCardClickEvent = {
|
|
56
56
|
kind: "card-click";
|
|
57
|
+
/** The message holding the clicked card, when the event names it. */
|
|
58
|
+
messageName?: string;
|
|
57
59
|
space: string;
|
|
58
60
|
sender: string;
|
|
59
61
|
senderEmail: string | undefined;
|
|
@@ -63,18 +65,25 @@ export type ParsedCardClickEvent = {
|
|
|
63
65
|
};
|
|
64
66
|
/** Parses one decoded Pub/Sub event, or `null` if it isn't a kind this provider acts on. */
|
|
65
67
|
export declare function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardClickEvent | null;
|
|
66
|
-
export type CardClickHandler = (params: Record<string, string>, space: string, sender: string, senderEmail?: string, isDirectMessage?: boolean) => Promise<void>;
|
|
68
|
+
export type CardClickHandler = (params: Record<string, string>, space: string, sender: string, senderEmail?: string, isDirectMessage?: boolean, messageName?: string) => Promise<void>;
|
|
67
69
|
export type GoogleChatProviderDeps = {
|
|
68
70
|
credentials: ServiceAccountCredentials;
|
|
69
71
|
subscription: string;
|
|
70
72
|
/**
|
|
71
73
|
* Resolves a confirmation token against the core's shared store, injected by
|
|
72
74
|
* the channel loader (`ChannelRuntimeContext.confirm`). Returns the reply to
|
|
73
|
-
* send, or `null` when the input wasn't token-shaped.
|
|
74
|
-
* the channel's contact with confirmation
|
|
75
|
-
* stay in the core.
|
|
75
|
+
* send, or `null` when the input wasn't token-shaped. With
|
|
76
|
+
* `resolveConfirmation` below, the channel's whole contact with confirmation:
|
|
77
|
+
* the store, vault and note-writer stay in the core.
|
|
76
78
|
*/
|
|
77
79
|
confirm: (token: string, sessionKey: string, principal: Principal) => Promise<string | null>;
|
|
80
|
+
/**
|
|
81
|
+
* The structured sibling of `confirm` (`ChannelRuntimeContext.resolveConfirmation`).
|
|
82
|
+
* When it's there, a click resolves through it, and an accepted choice
|
|
83
|
+
* turns its card into the choice taken; without it, a click goes through
|
|
84
|
+
* `confirm` and the card stays as it was.
|
|
85
|
+
*/
|
|
86
|
+
resolveConfirmation?: (token: string, sessionKey: string, principal: Principal) => Promise<ConfirmOutcome>;
|
|
78
87
|
/**
|
|
79
88
|
* Handles a `CARD_CLICKED` event's action parameters. Defaults to resolving
|
|
80
89
|
* the confirm button's token through `deps.confirm` (the same path a bare
|
package/google-chat-provider.ts
CHANGED
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
loginLinkText,
|
|
38
38
|
PENDING_CONFIRMATION_NOTE,
|
|
39
39
|
RESUME_AFTER_CONFIRM,
|
|
40
|
+
confirmReply,
|
|
41
|
+
type ConfirmOutcome,
|
|
40
42
|
NO_REPLY,
|
|
41
43
|
type LoginRequest,
|
|
42
44
|
type PendingConfirmation,
|
|
@@ -53,7 +55,9 @@ import {
|
|
|
53
55
|
* (a single `Conferma` for a plain staging). Each button's parameters carry
|
|
54
56
|
* the token, so a click routes straight into the same `tryConfirm` path
|
|
55
57
|
* a bare token typed on the terminal uses (see `onCardClick`'s `confirm`
|
|
56
|
-
* case, below) — the user never has to see or type the token themselves
|
|
58
|
+
* case, below) — the user never has to see or type the token themselves —
|
|
59
|
+
* plus its label and the summary, so the click can turn the card into
|
|
60
|
+
* `buildChosenCard` without the channel keeping any state.
|
|
57
61
|
*/
|
|
58
62
|
function buildConfirmCard(pending: PendingConfirmation): ChatCard {
|
|
59
63
|
return {
|
|
@@ -66,7 +70,16 @@ function buildConfirmCard(pending: PendingConfirmation): ChatCard {
|
|
|
66
70
|
buttonList: {
|
|
67
71
|
buttons: pending.choices.map((choice) => ({
|
|
68
72
|
text: choice.label,
|
|
69
|
-
onClick: {
|
|
73
|
+
onClick: {
|
|
74
|
+
action: {
|
|
75
|
+
function: "confirm",
|
|
76
|
+
parameters: [
|
|
77
|
+
{ key: "token", value: choice.token },
|
|
78
|
+
{ key: "label", value: choice.label },
|
|
79
|
+
{ key: "summary", value: pending.summary },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
70
83
|
})),
|
|
71
84
|
},
|
|
72
85
|
},
|
|
@@ -76,6 +89,21 @@ function buildConfirmCard(pending: PendingConfirmation): ChatCard {
|
|
|
76
89
|
};
|
|
77
90
|
}
|
|
78
91
|
|
|
92
|
+
/** The confirmation card once a choice is taken: the same request, the choice instead of the buttons. */
|
|
93
|
+
function buildChosenCard(summary: string, label: string | undefined): ChatCard {
|
|
94
|
+
return {
|
|
95
|
+
header: { title: "Conferma richiesta" },
|
|
96
|
+
sections: [
|
|
97
|
+
{
|
|
98
|
+
widgets: [
|
|
99
|
+
{ textParagraph: { text: `\`${summary}\`` } },
|
|
100
|
+
{ textParagraph: { text: label === undefined ? "Scelta registrata." : `Scelto: ${label}` } },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
79
107
|
/** The card that shows a person the link to log in to a service a plugin acts on as them: one button that opens it. */
|
|
80
108
|
function buildLoginCard(login: LoginRequest): ChatCard {
|
|
81
109
|
return {
|
|
@@ -221,6 +249,8 @@ export type ParsedMessageEvent = {
|
|
|
221
249
|
|
|
222
250
|
export type ParsedCardClickEvent = {
|
|
223
251
|
kind: "card-click";
|
|
252
|
+
/** The message holding the clicked card, when the event names it. */
|
|
253
|
+
messageName?: string;
|
|
224
254
|
space: string;
|
|
225
255
|
sender: string;
|
|
226
256
|
senderEmail: string | undefined;
|
|
@@ -264,6 +294,7 @@ export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardCli
|
|
|
264
294
|
}
|
|
265
295
|
return {
|
|
266
296
|
kind: "card-click",
|
|
297
|
+
...(typeof event.message?.name === "string" ? { messageName: event.message.name } : {}),
|
|
267
298
|
space: event.space.name,
|
|
268
299
|
sender: event.user.name,
|
|
269
300
|
senderEmail: emailOf(event.user),
|
|
@@ -280,6 +311,7 @@ export type CardClickHandler = (
|
|
|
280
311
|
sender: string,
|
|
281
312
|
senderEmail?: string,
|
|
282
313
|
isDirectMessage?: boolean,
|
|
314
|
+
messageName?: string,
|
|
283
315
|
) => Promise<void>;
|
|
284
316
|
|
|
285
317
|
export type GoogleChatProviderDeps = {
|
|
@@ -288,11 +320,18 @@ export type GoogleChatProviderDeps = {
|
|
|
288
320
|
/**
|
|
289
321
|
* Resolves a confirmation token against the core's shared store, injected by
|
|
290
322
|
* the channel loader (`ChannelRuntimeContext.confirm`). Returns the reply to
|
|
291
|
-
* send, or `null` when the input wasn't token-shaped.
|
|
292
|
-
* the channel's contact with confirmation
|
|
293
|
-
* stay in the core.
|
|
323
|
+
* send, or `null` when the input wasn't token-shaped. With
|
|
324
|
+
* `resolveConfirmation` below, the channel's whole contact with confirmation:
|
|
325
|
+
* the store, vault and note-writer stay in the core.
|
|
294
326
|
*/
|
|
295
327
|
confirm: (token: string, sessionKey: string, principal: Principal) => Promise<string | null>;
|
|
328
|
+
/**
|
|
329
|
+
* The structured sibling of `confirm` (`ChannelRuntimeContext.resolveConfirmation`).
|
|
330
|
+
* When it's there, a click resolves through it, and an accepted choice
|
|
331
|
+
* turns its card into the choice taken; without it, a click goes through
|
|
332
|
+
* `confirm` and the card stays as it was.
|
|
333
|
+
*/
|
|
334
|
+
resolveConfirmation?: (token: string, sessionKey: string, principal: Principal) => Promise<ConfirmOutcome>;
|
|
296
335
|
/**
|
|
297
336
|
* Handles a `CARD_CLICKED` event's action parameters. Defaults to resolving
|
|
298
337
|
* the confirm button's token through `deps.confirm` (the same path a bare
|
|
@@ -383,11 +422,14 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
383
422
|
* Default `onCardClick`: the confirm button's token routes through the exact
|
|
384
423
|
* same `deps.confirm` a bare token typed on the terminal uses — one execution
|
|
385
424
|
* path, one set of valid/expired/wrong-session-token behaviors, regardless of
|
|
386
|
-
* how the token got here.
|
|
425
|
+
* how the token got here. A click the core accepted (the action ran, failed,
|
|
426
|
+
* or the turn resumes) turns its card into the choice taken, before
|
|
427
|
+
* anything else; one it didn't (`not-found`: expired, used, or someone
|
|
428
|
+
* else's in a shared space) leaves the card for whoever has to choose.
|
|
387
429
|
*/
|
|
388
430
|
const onCardClick: CardClickHandler =
|
|
389
431
|
deps.onCardClick ??
|
|
390
|
-
(async (params, space, sender, senderEmail, isDirectMessage = false) => {
|
|
432
|
+
(async (params, space, sender, senderEmail, isDirectMessage = false, messageName) => {
|
|
391
433
|
const token = params.token;
|
|
392
434
|
if (!token) {
|
|
393
435
|
log(`[chat] card click with no token parameter`);
|
|
@@ -398,7 +440,26 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
398
440
|
provider: "google-chat",
|
|
399
441
|
...(senderEmail === undefined ? {} : { claims: { email: senderEmail } }),
|
|
400
442
|
};
|
|
401
|
-
const
|
|
443
|
+
const sessionKey = deriveSessionKey(space, sender);
|
|
444
|
+
let reply: string | null;
|
|
445
|
+
if (deps.resolveConfirmation) {
|
|
446
|
+
const outcome = await deps.resolveConfirmation(token, sessionKey, principal);
|
|
447
|
+
const accepted = outcome.status === "ok" || outcome.status === "failed" || outcome.status === "resume";
|
|
448
|
+
if (accepted && messageName !== undefined && params.summary !== undefined) {
|
|
449
|
+
try {
|
|
450
|
+
await updateCardFn(messageName, buildChosenCard(params.summary, params.label), clientDeps);
|
|
451
|
+
log(`[chat:${space}] [out] confirm card ${messageName} patched to the choice taken`);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
log(`[chat:${space}] confirm card patch failed: ${String(err)}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// On not-found nothing was taken, so `confirm` gives the reply as it
|
|
457
|
+
// always did: the core's own message for someone it won't talk to,
|
|
458
|
+
// which the outcome doesn't carry.
|
|
459
|
+
reply = outcome.status === "not-found" ? await deps.confirm(token, sessionKey, principal) : confirmReply(outcome);
|
|
460
|
+
} else {
|
|
461
|
+
reply = await deps.confirm(token, sessionKey, principal);
|
|
462
|
+
}
|
|
402
463
|
if (reply === RESUME_AFTER_CONFIRM && activeHandleTurn) {
|
|
403
464
|
await resumeTurn(activeHandleTurn, space, principal, isDirectMessage);
|
|
404
465
|
return;
|
|
@@ -648,7 +709,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
648
709
|
if (parsed.kind === "message") {
|
|
649
710
|
await processMessageEvent(parsed, handleTurn);
|
|
650
711
|
} else {
|
|
651
|
-
await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail, parsed.isDirectMessage);
|
|
712
|
+
await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail, parsed.isDirectMessage, parsed.messageName);
|
|
652
713
|
}
|
|
653
714
|
} catch (err) {
|
|
654
715
|
log(`[chat] event handling failed: ${String(err)}`);
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/channel-google-chat",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"@google-cloud/pubsub": "^5.3.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@mercury-fw/channel-types": ">=0.
|
|
34
|
+
"@mercury-fw/channel-types": ">=0.53.1 <1.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@mercury-fw/channel-types": "0.
|
|
37
|
+
"@mercury-fw/channel-types": "0.53.1",
|
|
38
38
|
"@mercury-fw/typescript-config": "*",
|
|
39
39
|
"@types/bun": "^1.4.2",
|
|
40
40
|
"typescript": "^6.0.3"
|