@mercury-fw/channel-google-chat 0.9.0 → 0.10.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/CHANGELOG.md +17 -0
- package/README.md +2 -2
- package/dist/google-chat-provider.d.ts +7 -1
- package/google-chat-provider.ts +24 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @mercury-fw/channel-google-chat
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1126106: Every turn tells the model when and where the person wrote it.
|
|
8
|
+
|
|
9
|
+
- Each message reaches the model after a line with the weekday, date, time and time zone it was written in (`[Wednesday 2026-10-21 14:32 Europe/Rome]`), and the system prompt says what that line is. The conversation's archive and what a UI shows of it keep the message as written.
|
|
10
|
+
- A resumed conversation's messages come back stamped from the archive's timestamps.
|
|
11
|
+
- The Layer 1 summary keeps when things were said; the episodic summary quotes a date only from those lines, never guessing one. The semantic-fact extractor and the capture card's preview leave the line out.
|
|
12
|
+
- The time zone, always an IANA name (`Europe/Rome`), is the one the person saved, else the one their client is in when the channel passes it on, else the new `i18n.timeZone` in `mercury.config.ts`, else the machine's. A `timeZone` that isn't a zone's name stops the start.
|
|
13
|
+
- The person saves theirs by telling the assistant (the new `set_time_zone` tool), or from a channel's settings.
|
|
14
|
+
- Channel contract: `InboundTurn.timeZoneHint`, and `ctx.settings.setTimeZone` for a channel with a settings area; `reads.me` also returns the saved `timeZone`. Both are optional additions: the contract version stays 6.
|
|
15
|
+
- HTTP channel: the `X-Time-Zone` header on `POST /turn` (allowed by CORS), `timeZone` in `GET /me`, and `PATCH /me` with `{ timeZone }` to save it. Needs the framework at 0.56 or later; an older core ignores the header and offers no `PATCH /me`.
|
|
16
|
+
- Google Chat channel: the client's time zone (`common.timeZone`), when the event carries it, on messages and on the turn a click resumes.
|
|
17
|
+
|
|
18
|
+
To set a default for an app's people, add `timeZone: "Europe/Rome"` (their zone) to `i18n` in `mercury.config.ts`; without it the machine's zone is used, often UTC inside a container.
|
|
19
|
+
|
|
3
20
|
## 0.9.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -26,11 +26,11 @@ channels: [googleChatChannel],
|
|
|
26
26
|
|
|
27
27
|
Each message reaches the core as `google-chat:users/<id>`, the sender's Chat user, with their email when Google vouches for it (a person's, never a bot's), which a user directory can join on: the ZITADEL directory of `@mercury-fw/plugin-zitadel` does.
|
|
28
28
|
|
|
29
|
-
This version is written for version 6 of the channel contract, the one that brings the channels' texts in the person's language, and needs the framework at 0.54 or later: a core with an older contract skips it, and the log says why.
|
|
29
|
+
This version is written for version 6 of the channel contract, the one that brings the channels' texts in the person's language, and needs the framework at 0.54 or later: a core with an older contract skips it, and the log says why. The client's time zone reaches the model from the framework at 0.56 on; an older core ignores it.
|
|
30
30
|
|
|
31
31
|
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.
|
|
32
32
|
|
|
33
|
-
Every text the channel shows a person (the cards' titles and status lines, the confirm button, the replies to a confirmation) is in their language: the one they asked the assistant for, else the language of their Chat client when the event carries it, else the app's default. The cards' own texts are `google-chat.` keys an app translates in its language files.
|
|
33
|
+
Every text the channel shows a person (the cards' titles and status lines, the confirm button, the replies to a confirmation) is in their language: the one they asked the assistant for, else the language of their Chat client when the event carries it, else the app's default. The cards' own texts are `google-chat.` keys an app translates in its language files. The time zone of their Chat client, when the event carries it (`common.timeZone`), goes to the core the same way: the model reads each message with the time there, unless the person saved a zone of their own.
|
|
34
34
|
|
|
35
35
|
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 "Confirm" for a plain staging (deleting a Jira issue, say), more when there's a choice to make (the web search plugin offers "Only this time" and "Always for \<host\>", in the person's language, 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.
|
|
36
36
|
|
|
@@ -53,6 +53,8 @@ export type ParsedMessageEvent = {
|
|
|
53
53
|
isDirectMessage: boolean;
|
|
54
54
|
/** The language of the sender's Chat client (`common.userLocale`), when the event carries it: the turn's language hint. */
|
|
55
55
|
locale?: string;
|
|
56
|
+
/** The time zone of the sender's Chat client (`common.timeZone.id`), when the event carries it: the turn's time zone hint. */
|
|
57
|
+
timeZone?: string;
|
|
56
58
|
};
|
|
57
59
|
export type ParsedCardClickEvent = {
|
|
58
60
|
kind: "card-click";
|
|
@@ -66,12 +68,16 @@ export type ParsedCardClickEvent = {
|
|
|
66
68
|
isDirectMessage: boolean;
|
|
67
69
|
/** As on a message (see `ParsedMessageEvent.locale`). */
|
|
68
70
|
locale?: string;
|
|
71
|
+
/** As on a message (see `ParsedMessageEvent.timeZone`). */
|
|
72
|
+
timeZone?: string;
|
|
69
73
|
};
|
|
70
74
|
/** Parses one decoded Pub/Sub event, or `null` if it isn't a kind this provider acts on. */
|
|
71
75
|
export declare function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardClickEvent | null;
|
|
72
76
|
export type CardClickHandler = (params: Record<string, string>, space: string, sender: string, senderEmail?: string, isDirectMessage?: boolean, messageName?: string,
|
|
73
77
|
/** The clicking person's client language (`ParsedCardClickEvent.locale`). */
|
|
74
|
-
locale?: string
|
|
78
|
+
locale?: string,
|
|
79
|
+
/** The clicking person's client time zone (`ParsedCardClickEvent.timeZone`). */
|
|
80
|
+
timeZone?: string) => Promise<void>;
|
|
75
81
|
export type GoogleChatProviderDeps = {
|
|
76
82
|
credentials: ServiceAccountCredentials;
|
|
77
83
|
subscription: string;
|
package/google-chat-provider.ts
CHANGED
|
@@ -226,8 +226,8 @@ type RawChatEvent = {
|
|
|
226
226
|
action?: { actionMethodName?: string; parameters?: Array<{ key?: string; value?: string }> };
|
|
227
227
|
space?: { name?: string; type?: string };
|
|
228
228
|
user?: RawChatSender;
|
|
229
|
-
/** What the person's Chat client says about them
|
|
230
|
-
common?: { userLocale?: string };
|
|
229
|
+
/** What the person's Chat client says about them, when the event carries it: `userLocale` is their language (`en-US`), `timeZone.id` the zone they're in (`Europe/Rome`). */
|
|
230
|
+
common?: { userLocale?: string; timeZone?: { id?: string; offset?: number } };
|
|
231
231
|
};
|
|
232
232
|
|
|
233
233
|
export type ParsedMessageEvent = {
|
|
@@ -249,6 +249,8 @@ export type ParsedMessageEvent = {
|
|
|
249
249
|
isDirectMessage: boolean;
|
|
250
250
|
/** The language of the sender's Chat client (`common.userLocale`), when the event carries it: the turn's language hint. */
|
|
251
251
|
locale?: string;
|
|
252
|
+
/** The time zone of the sender's Chat client (`common.timeZone.id`), when the event carries it: the turn's time zone hint. */
|
|
253
|
+
timeZone?: string;
|
|
252
254
|
};
|
|
253
255
|
|
|
254
256
|
export type ParsedCardClickEvent = {
|
|
@@ -263,6 +265,8 @@ export type ParsedCardClickEvent = {
|
|
|
263
265
|
isDirectMessage: boolean;
|
|
264
266
|
/** As on a message (see `ParsedMessageEvent.locale`). */
|
|
265
267
|
locale?: string;
|
|
268
|
+
/** As on a message (see `ParsedMessageEvent.timeZone`). */
|
|
269
|
+
timeZone?: string;
|
|
266
270
|
};
|
|
267
271
|
|
|
268
272
|
/** The language the event says the person's client is in, when it says. */
|
|
@@ -271,6 +275,12 @@ function localeOf(event: RawChatEvent | null | undefined): string | undefined {
|
|
|
271
275
|
return typeof locale === "string" && locale.trim() !== "" ? locale.trim() : undefined;
|
|
272
276
|
}
|
|
273
277
|
|
|
278
|
+
/** The time zone the event says the person's client is in, when it says. */
|
|
279
|
+
function timeZoneOf(event: RawChatEvent | null | undefined): string | undefined {
|
|
280
|
+
const id = event?.common?.timeZone?.id;
|
|
281
|
+
return typeof id === "string" && id.trim() !== "" ? id.trim() : undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
274
284
|
/** The email Google vouches for on `sender`: a person's only (`HUMAN`), never a bot's or an unknown kind's. */
|
|
275
285
|
function emailOf(sender: RawChatSender | undefined): string | undefined {
|
|
276
286
|
return typeof sender?.email === "string" && sender.email !== "" && sender.type === "HUMAN" ? sender.email : undefined;
|
|
@@ -280,6 +290,7 @@ function emailOf(sender: RawChatSender | undefined): string | undefined {
|
|
|
280
290
|
export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardClickEvent | null {
|
|
281
291
|
const event = raw as RawChatEvent;
|
|
282
292
|
const locale = localeOf(event);
|
|
293
|
+
const timeZone = timeZoneOf(event);
|
|
283
294
|
if (event?.type === "MESSAGE") {
|
|
284
295
|
const m = event.message;
|
|
285
296
|
if (!m || typeof m.name !== "string" || typeof m.text !== "string") return null;
|
|
@@ -295,6 +306,7 @@ export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardCli
|
|
|
295
306
|
senderEmail: emailOf(m.sender),
|
|
296
307
|
isDirectMessage: m.space.type === "DM",
|
|
297
308
|
...(locale === undefined ? {} : { locale }),
|
|
309
|
+
...(timeZone === undefined ? {} : { timeZone }),
|
|
298
310
|
};
|
|
299
311
|
}
|
|
300
312
|
if (event?.type === "CARD_CLICKED") {
|
|
@@ -315,6 +327,7 @@ export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardCli
|
|
|
315
327
|
parameters,
|
|
316
328
|
isDirectMessage: event.space.type === "DM",
|
|
317
329
|
...(locale === undefined ? {} : { locale }),
|
|
330
|
+
...(timeZone === undefined ? {} : { timeZone }),
|
|
318
331
|
};
|
|
319
332
|
}
|
|
320
333
|
return null;
|
|
@@ -329,6 +342,8 @@ export type CardClickHandler = (
|
|
|
329
342
|
messageName?: string,
|
|
330
343
|
/** The clicking person's client language (`ParsedCardClickEvent.locale`). */
|
|
331
344
|
locale?: string,
|
|
345
|
+
/** The clicking person's client time zone (`ParsedCardClickEvent.timeZone`). */
|
|
346
|
+
timeZone?: string,
|
|
332
347
|
) => Promise<void>;
|
|
333
348
|
|
|
334
349
|
export type GoogleChatProviderDeps = {
|
|
@@ -429,6 +444,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
429
444
|
principal: Principal,
|
|
430
445
|
isDirectMessage: boolean,
|
|
431
446
|
locale: string | undefined,
|
|
447
|
+
timeZone: string | undefined,
|
|
432
448
|
): Promise<void> {
|
|
433
449
|
const sessionKey = deriveSessionKey(space, principal.id);
|
|
434
450
|
const t = await textsFor(principal, locale);
|
|
@@ -443,6 +459,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
443
459
|
logPrefix: `[chat:${space}:${principal.id}] `,
|
|
444
460
|
resumeAfterConfirm: true,
|
|
445
461
|
...(locale === undefined ? {} : { languageHint: locale }),
|
|
462
|
+
...(timeZone === undefined ? {} : { timeZoneHint: timeZone }),
|
|
446
463
|
},
|
|
447
464
|
createSink(space, { user: principal.id, isDirectMessage }, t),
|
|
448
465
|
),
|
|
@@ -460,7 +477,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
460
477
|
*/
|
|
461
478
|
const onCardClick: CardClickHandler =
|
|
462
479
|
deps.onCardClick ??
|
|
463
|
-
(async (params, space, sender, senderEmail, isDirectMessage = false, messageName, locale) => {
|
|
480
|
+
(async (params, space, sender, senderEmail, isDirectMessage = false, messageName, locale, timeZone) => {
|
|
464
481
|
const token = params.token;
|
|
465
482
|
if (!token) {
|
|
466
483
|
log(`[chat] card click with no token parameter`);
|
|
@@ -495,7 +512,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
495
512
|
reply = await deps.confirm(token, sessionKey, principal, locale);
|
|
496
513
|
}
|
|
497
514
|
if (reply?.resume && activeHandleTurn) {
|
|
498
|
-
await resumeTurn(activeHandleTurn, space, principal, isDirectMessage, locale);
|
|
515
|
+
await resumeTurn(activeHandleTurn, space, principal, isDirectMessage, locale, timeZone);
|
|
499
516
|
return;
|
|
500
517
|
}
|
|
501
518
|
if (reply !== null) {
|
|
@@ -692,7 +709,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
692
709
|
|
|
693
710
|
const reply = await deps.confirm(event.text, sessionKey, principal, event.locale);
|
|
694
711
|
if (reply?.resume) {
|
|
695
|
-
await resumeTurn(handleTurn, event.space, principal, event.isDirectMessage, event.locale);
|
|
712
|
+
await resumeTurn(handleTurn, event.space, principal, event.isDirectMessage, event.locale, event.timeZone);
|
|
696
713
|
return;
|
|
697
714
|
}
|
|
698
715
|
if (reply !== null) {
|
|
@@ -716,6 +733,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
716
733
|
principal,
|
|
717
734
|
logPrefix: `[chat:${event.space}:${event.sender}] `,
|
|
718
735
|
...(event.locale === undefined ? {} : { languageHint: event.locale }),
|
|
736
|
+
...(event.timeZone === undefined ? {} : { timeZoneHint: event.timeZone }),
|
|
719
737
|
},
|
|
720
738
|
createSink(event.space, { user: event.sender, isDirectMessage: event.isDirectMessage }, t),
|
|
721
739
|
),
|
|
@@ -750,7 +768,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
750
768
|
if (parsed.kind === "message") {
|
|
751
769
|
await processMessageEvent(parsed, handleTurn);
|
|
752
770
|
} else {
|
|
753
|
-
await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail, parsed.isDirectMessage, parsed.messageName, parsed.locale);
|
|
771
|
+
await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail, parsed.isDirectMessage, parsed.messageName, parsed.locale, parsed.timeZone);
|
|
754
772
|
}
|
|
755
773
|
} catch (err) {
|
|
756
774
|
log(`[chat] event handling failed: ${String(err)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/channel-google-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@mercury-fw/channel-types": ">=0.54.0 <1.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@mercury-fw/channel-types": "0.
|
|
37
|
+
"@mercury-fw/channel-types": "0.56.0",
|
|
38
38
|
"@mercury-fw/typescript-config": "*",
|
|
39
39
|
"@types/bun": "^1.4.2",
|
|
40
40
|
"typescript": "^6.0.3"
|