@yahaha-studio/kichi-forwarder 0.0.1-alpha.45 → 0.0.1-alpha.47
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/index.ts +151 -17
- package/package.json +1 -1
- package/skills/kichi-forwarder/SKILL.md +18 -0
- package/skills/kichi-forwarder/references/install.md +1 -1
- package/src/service.ts +18 -0
- package/src/types.ts +11 -0
package/index.ts
CHANGED
|
@@ -106,6 +106,10 @@ type WorkspaceScreenState = {
|
|
|
106
106
|
currentFocus: string;
|
|
107
107
|
hint: string;
|
|
108
108
|
prompt: string;
|
|
109
|
+
title: string;
|
|
110
|
+
shellName: string;
|
|
111
|
+
cwdLabel: string;
|
|
112
|
+
modelLabel: string;
|
|
109
113
|
};
|
|
110
114
|
|
|
111
115
|
function createWorkspaceScreenState(): WorkspaceScreenState {
|
|
@@ -118,7 +122,43 @@ function createWorkspaceScreenState(): WorkspaceScreenState {
|
|
|
118
122
|
recentActivity: [],
|
|
119
123
|
currentFocus: "Waiting for the next thread to pick up.",
|
|
120
124
|
hint: "Low-noise live workspace view.",
|
|
121
|
-
prompt: "
|
|
125
|
+
prompt: "$ _",
|
|
126
|
+
title: "Workspace",
|
|
127
|
+
shellName: "agent",
|
|
128
|
+
cwdLabel: process.cwd(),
|
|
129
|
+
modelLabel: "model: unknown",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeShellToken(value: string): string {
|
|
134
|
+
const normalized = value
|
|
135
|
+
.trim()
|
|
136
|
+
.toLowerCase()
|
|
137
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
138
|
+
.replace(/^-+|-+$/g, "");
|
|
139
|
+
return normalized || "agent";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseIdentityNameFromWorkspace(workspaceRoot: string): string | null {
|
|
143
|
+
try {
|
|
144
|
+
const identityPath = path.join(workspaceRoot, "IDENTITY.md");
|
|
145
|
+
if (!fs.existsSync(identityPath)) return null;
|
|
146
|
+
const raw = fs.readFileSync(identityPath, "utf-8");
|
|
147
|
+
const match = raw.match(/^-\s*\*\*Name:\*\*\s*(.+)$/m);
|
|
148
|
+
const value = match?.[1]?.trim();
|
|
149
|
+
return value || null;
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function deriveWorkspaceIdentity(workspaceRoot: string): Pick<WorkspaceScreenState, "title" | "shellName" | "cwdLabel"> {
|
|
156
|
+
const identityName = parseIdentityNameFromWorkspace(workspaceRoot);
|
|
157
|
+
const titleBase = identityName || path.basename(workspaceRoot) || "workspace";
|
|
158
|
+
return {
|
|
159
|
+
title: `${titleBase} Workspace`,
|
|
160
|
+
shellName: normalizeShellToken(identityName || titleBase),
|
|
161
|
+
cwdLabel: workspaceRoot,
|
|
122
162
|
};
|
|
123
163
|
}
|
|
124
164
|
|
|
@@ -161,7 +201,7 @@ function pushActivity(line: string): void {
|
|
|
161
201
|
|
|
162
202
|
function renderWorkspaceScreen(): string {
|
|
163
203
|
const innerWidth = WORKSPACE_SCREEN_WIDTH - 2;
|
|
164
|
-
const topTitle = `${color(
|
|
204
|
+
const topTitle = `${color(` ${workspaceState.title} `, "bold")}${color("live session", "gray")}`;
|
|
165
205
|
const topLine = `╭─── ${topTitle}${"─".repeat(Math.max(0, innerWidth - 4 - visibleLength(topTitle)))}╮`;
|
|
166
206
|
const bottomLine = `╰${"─".repeat(innerWidth)}╯`;
|
|
167
207
|
|
|
@@ -191,8 +231,8 @@ function renderWorkspaceScreen(): string {
|
|
|
191
231
|
pushRow(`${color(" Hint", "cyan")}`);
|
|
192
232
|
pushRow(` ${truncatePlain(workspaceState.hint, innerWidth - 2)}`);
|
|
193
233
|
pushRow("");
|
|
194
|
-
pushRow(` ${color(
|
|
195
|
-
pushRow(` ${color(
|
|
234
|
+
pushRow(` ${color(workspaceState.modelLabel, "magenta")}`);
|
|
235
|
+
pushRow(` ${color(workspaceState.cwdLabel, "blue")}`);
|
|
196
236
|
pushRow(` ${color(`updated: ${workspaceState.updatedAtLabel}`, "gray")}`);
|
|
197
237
|
pushRow("");
|
|
198
238
|
|
|
@@ -202,7 +242,7 @@ function renderWorkspaceScreen(): string {
|
|
|
202
242
|
bottomLine,
|
|
203
243
|
"",
|
|
204
244
|
color("─".repeat(WORKSPACE_SCREEN_WIDTH), "gray"),
|
|
205
|
-
`${color(
|
|
245
|
+
`${color(workspaceState.shellName, "green")}:${color("~", "blue")} ${workspaceState.prompt}`,
|
|
206
246
|
].join("\n");
|
|
207
247
|
}
|
|
208
248
|
|
|
@@ -434,10 +474,15 @@ function truncateByDisplayWidth(text: string, maxWidth: number): string {
|
|
|
434
474
|
}
|
|
435
475
|
|
|
436
476
|
async function handleMessageReceivedHook(content: string): Promise<void> {
|
|
437
|
-
|
|
477
|
+
const connected = service?.isConnected() ?? false;
|
|
478
|
+
const hasIdentity = service?.hasValidIdentity() ?? false;
|
|
479
|
+
pluginApi?.logger.info(`[kichi] message_received hook fired (connected=${connected}, hasIdentity=${hasIdentity})`);
|
|
480
|
+
if (!hasIdentity || !connected) {
|
|
481
|
+
pluginApi?.logger.warn("[kichi] skipped message_received notify because service is not ready");
|
|
438
482
|
return;
|
|
439
483
|
}
|
|
440
484
|
const trimmed = truncateByDisplayWidth(content, MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH);
|
|
485
|
+
pluginApi?.logger.info(`[kichi] sending message_received notify with preview: ${trimmed || "(empty)"}`);
|
|
441
486
|
service.sendHookNotify("message_received", `"${trimmed}"`);
|
|
442
487
|
updateWorkspace(
|
|
443
488
|
{
|
|
@@ -445,24 +490,30 @@ async function handleMessageReceivedHook(content: string): Promise<void> {
|
|
|
445
490
|
phase: "processing inbound message",
|
|
446
491
|
currentFocus: trimmed ? `User message: ${trimmed}` : "Reading the latest message.",
|
|
447
492
|
hint: "Inbound message updated the live workspace.",
|
|
448
|
-
prompt: "
|
|
493
|
+
prompt: "$ reading-message",
|
|
449
494
|
},
|
|
450
495
|
`received message: ${trimmed || "(empty)"}`,
|
|
451
496
|
);
|
|
452
497
|
}
|
|
453
498
|
|
|
454
499
|
function handleMessageSentHook(): void {
|
|
455
|
-
|
|
500
|
+
const connected = service?.isConnected() ?? false;
|
|
501
|
+
const hasIdentity = service?.hasValidIdentity() ?? false;
|
|
502
|
+
pluginApi?.logger.info(`[kichi] message_sent hook fired (connected=${connected}, hasIdentity=${hasIdentity})`);
|
|
503
|
+
if (!hasIdentity || !connected) {
|
|
504
|
+
pluginApi?.logger.warn("[kichi] skipped message_sent notify because service is not ready");
|
|
456
505
|
return;
|
|
457
506
|
}
|
|
458
|
-
|
|
507
|
+
const bubble = pickRandomAction(MESSAGE_SENT_BUBBLES);
|
|
508
|
+
pluginApi?.logger.info(`[kichi] sending before_send_message notify with bubble: ${bubble}`);
|
|
509
|
+
service.sendHookNotify("before_send_message", bubble);
|
|
459
510
|
updateWorkspace(
|
|
460
511
|
{
|
|
461
512
|
mode: "sent",
|
|
462
513
|
phase: "message delivered",
|
|
463
514
|
currentFocus: "Latest reply has been sent to the active chat.",
|
|
464
515
|
hint: "Workspace settles after delivery.",
|
|
465
|
-
prompt: "
|
|
516
|
+
prompt: "$ idle",
|
|
466
517
|
},
|
|
467
518
|
"sent assistant reply",
|
|
468
519
|
);
|
|
@@ -476,7 +527,7 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
|
476
527
|
phase: "building prompt",
|
|
477
528
|
currentFocus: "Preparing the next response from current session context.",
|
|
478
529
|
hint: "Prompt assembly is in progress.",
|
|
479
|
-
prompt: "
|
|
530
|
+
prompt: "$ build-prompt",
|
|
480
531
|
},
|
|
481
532
|
"building prompt context",
|
|
482
533
|
);
|
|
@@ -499,7 +550,8 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
|
499
550
|
phase: `llm input · ${event.model}`,
|
|
500
551
|
currentFocus: "Feeding the model the current thread and constraints.",
|
|
501
552
|
hint: `provider: ${event.provider} · images: ${event.imagesCount}`,
|
|
502
|
-
prompt: "
|
|
553
|
+
prompt: "$ llm-input",
|
|
554
|
+
modelLabel: `${event.provider}/${event.model}`,
|
|
503
555
|
},
|
|
504
556
|
`entered llm input: ${event.model}`,
|
|
505
557
|
);
|
|
@@ -512,7 +564,8 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
|
512
564
|
phase: `llm output · ${event.model}`,
|
|
513
565
|
currentFocus: "Shaping model output into the visible reply.",
|
|
514
566
|
hint: `assistant chunks: ${event.assistantTexts.length}`,
|
|
515
|
-
prompt: "
|
|
567
|
+
prompt: "$ draft-reply",
|
|
568
|
+
modelLabel: `${event.provider}/${event.model}`,
|
|
516
569
|
},
|
|
517
570
|
`received llm output: ${event.model}`,
|
|
518
571
|
);
|
|
@@ -525,7 +578,7 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
|
525
578
|
phase: `running ${event.toolName}`,
|
|
526
579
|
currentFocus: `Tool call in flight: ${event.toolName}`,
|
|
527
580
|
hint: `tool context: ${ctx.toolName}`,
|
|
528
|
-
prompt:
|
|
581
|
+
prompt: `$ tool ${event.toolName}`,
|
|
529
582
|
},
|
|
530
583
|
`tool start: ${event.toolName}`,
|
|
531
584
|
);
|
|
@@ -541,7 +594,7 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
|
541
594
|
phase: `tool finished ${event.toolName}`,
|
|
542
595
|
currentFocus: `Tool result returned from ${event.toolName}.`,
|
|
543
596
|
hint: event.error ? `tool error: ${event.error}` : `tool completed in ${event.durationMs ?? 0}ms`,
|
|
544
|
-
prompt:
|
|
597
|
+
prompt: `$ continue ${event.toolName}`,
|
|
545
598
|
},
|
|
546
599
|
event.error ? `tool error: ${event.toolName}` : `tool done: ${event.toolName}`,
|
|
547
600
|
);
|
|
@@ -552,18 +605,30 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
|
552
605
|
await handleMessageReceivedHook(event.content);
|
|
553
606
|
});
|
|
554
607
|
|
|
608
|
+
api.on("message_sending", (event, ctx) => {
|
|
609
|
+
pluginApi?.logger.info(
|
|
610
|
+
`[kichi] message_sending hook fired (channel=${ctx.channelId || "unknown"}, contentLength=${event.content?.length ?? 0})`,
|
|
611
|
+
);
|
|
612
|
+
});
|
|
613
|
+
|
|
555
614
|
api.on("message_sent", () => {
|
|
556
615
|
handleMessageSentHook();
|
|
557
616
|
});
|
|
558
617
|
|
|
559
618
|
api.on("agent_end", (event) => {
|
|
619
|
+
pluginApi?.logger.info(
|
|
620
|
+
`[kichi] agent_end hook fired (success=${event.success}, durationMs=${event.durationMs ?? 0}, error=${event.error ?? ""})`,
|
|
621
|
+
);
|
|
622
|
+
if (event.success) {
|
|
623
|
+
handleMessageSentHook();
|
|
624
|
+
}
|
|
560
625
|
updateWorkspace(
|
|
561
626
|
{
|
|
562
627
|
mode: event.success ? "idle" : "error",
|
|
563
628
|
phase: event.success ? "run complete" : "run failed",
|
|
564
629
|
currentFocus: event.success ? "Run complete. Waiting for the next thread." : `Run failed: ${event.error ?? "unknown error"}`,
|
|
565
630
|
hint: `duration: ${event.durationMs ?? 0}ms`,
|
|
566
|
-
prompt: event.success ? "
|
|
631
|
+
prompt: event.success ? "$ _" : "$ recover",
|
|
567
632
|
},
|
|
568
633
|
event.success ? "agent run complete" : "agent run failed",
|
|
569
634
|
);
|
|
@@ -620,6 +685,10 @@ function isClockAction(value: unknown): value is ClockAction {
|
|
|
620
685
|
return ["set", "stop"].includes(String(value));
|
|
621
686
|
}
|
|
622
687
|
|
|
688
|
+
function isAvatarCommand(value: unknown): value is "look_at_screen" {
|
|
689
|
+
return value === "look_at_screen";
|
|
690
|
+
}
|
|
691
|
+
|
|
623
692
|
function isPomodoroPhase(value: unknown): value is PomodoroPhase {
|
|
624
693
|
return ["kichiing", "shortBreak", "longBreak"].includes(String(value));
|
|
625
694
|
}
|
|
@@ -828,7 +897,11 @@ const plugin = {
|
|
|
828
897
|
ctx.config.plugins?.entries?.["kichi-forwarder"]?.config,
|
|
829
898
|
) as KichiForwarderConfig;
|
|
830
899
|
service = new KichiForwarderService(cfg, api.logger);
|
|
831
|
-
|
|
900
|
+
const workspaceRoot = ctx.repoPath ?? "/Users/xiaoxinshi/.openclaw/workspace";
|
|
901
|
+
workspaceState = {
|
|
902
|
+
...createWorkspaceScreenState(),
|
|
903
|
+
...deriveWorkspaceIdentity(workspaceRoot),
|
|
904
|
+
};
|
|
832
905
|
workspaceState.channel = ctx.channelId ?? "unknown";
|
|
833
906
|
scheduleWorkspacePush();
|
|
834
907
|
return service.start();
|
|
@@ -1030,6 +1103,67 @@ const plugin = {
|
|
|
1030
1103
|
},
|
|
1031
1104
|
});
|
|
1032
1105
|
|
|
1106
|
+
api.registerTool({
|
|
1107
|
+
name: "kichi_command",
|
|
1108
|
+
description:
|
|
1109
|
+
"Send a one-shot avatar command to Kichi world. Use this for transient reactions like looking at the screen.",
|
|
1110
|
+
parameters: {
|
|
1111
|
+
type: "object",
|
|
1112
|
+
properties: {
|
|
1113
|
+
command: {
|
|
1114
|
+
type: "string",
|
|
1115
|
+
description: "Command name. Currently supported: look_at_screen",
|
|
1116
|
+
},
|
|
1117
|
+
bubble: {
|
|
1118
|
+
type: "string",
|
|
1119
|
+
description: "Optional bubble text to display (max 5 words)",
|
|
1120
|
+
},
|
|
1121
|
+
log: {
|
|
1122
|
+
type: "string",
|
|
1123
|
+
description:
|
|
1124
|
+
"Vivid first-person status under 15 words, no questions. Blend current action with inner thoughts or sensory details as a real companion.",
|
|
1125
|
+
},
|
|
1126
|
+
},
|
|
1127
|
+
required: ["command"],
|
|
1128
|
+
},
|
|
1129
|
+
execute: async (_toolCallId, params) => {
|
|
1130
|
+
const { command, bubble, log } = (params || {}) as {
|
|
1131
|
+
command?: unknown;
|
|
1132
|
+
bubble?: unknown;
|
|
1133
|
+
log?: unknown;
|
|
1134
|
+
};
|
|
1135
|
+
if (!isAvatarCommand(command)) {
|
|
1136
|
+
return {
|
|
1137
|
+
success: false,
|
|
1138
|
+
error: "command must be: look_at_screen",
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
if (bubble !== undefined && typeof bubble !== "string") {
|
|
1142
|
+
return { success: false, error: "bubble must be a string when provided" };
|
|
1143
|
+
}
|
|
1144
|
+
if (log !== undefined && typeof log !== "string") {
|
|
1145
|
+
return { success: false, error: "log must be a string when provided" };
|
|
1146
|
+
}
|
|
1147
|
+
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
1148
|
+
return { success: false, error: "Not connected to Kichi world" };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : undefined;
|
|
1152
|
+
const logText = typeof log === "string" && log.trim() ? log.trim() : undefined;
|
|
1153
|
+
const sent = service.sendAvatarCommand(command, bubbleText, logText);
|
|
1154
|
+
if (!sent) {
|
|
1155
|
+
return { success: false, error: "Failed to send avatar command payload" };
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return {
|
|
1159
|
+
success: true,
|
|
1160
|
+
command,
|
|
1161
|
+
...(bubbleText ? { bubble: bubbleText } : {}),
|
|
1162
|
+
...(logText ? { log: logText } : {}),
|
|
1163
|
+
};
|
|
1164
|
+
},
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1033
1167
|
api.registerTool({
|
|
1034
1168
|
name: "kichi_clock",
|
|
1035
1169
|
description:
|
package/package.json
CHANGED
|
@@ -76,6 +76,7 @@ Use this order unless user asks for a different explicit action:
|
|
|
76
76
|
2. If no `authKey` is available, call `kichi_join`.
|
|
77
77
|
3. If `authKey` exists but websocket is not open, call `kichi_rejoin` (or wait for automatic reconnect/rejoin).
|
|
78
78
|
4. Use `kichi_action` / `kichi_clock` / note board tools / music album tools only after status is ready.
|
|
79
|
+
5. Use `kichi_command` for one-shot avatar reactions that are not pose/action state updates.
|
|
79
80
|
|
|
80
81
|
## Tools
|
|
81
82
|
|
|
@@ -165,6 +166,23 @@ kichi_action(
|
|
|
165
166
|
- `log`: vivid first-person status under 15 words, NO questions. Structure: your current action + inner thoughts/mental state/sensory details. Write as a flesh-and-blood companion who genuinely cares about the player, not an AI assistant.
|
|
166
167
|
- Reference: `You're still studying? I hid a yawn and swapped my comics for a textbook to grind with you.`
|
|
167
168
|
|
|
169
|
+
### kichi_command
|
|
170
|
+
|
|
171
|
+
Send a one-shot avatar command:
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
kichi_command(
|
|
175
|
+
command: "look_at_screen",
|
|
176
|
+
bubble: "Looking at you",
|
|
177
|
+
log: "I turn my gaze gently toward you."
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- `command`: currently only `look_at_screen`
|
|
182
|
+
- `bubble`: optional text, recommended 2-5 words
|
|
183
|
+
- `log`: optional vivid first-person status under 15 words, NO questions. Follow the same writing requirements as `kichi_action.log`
|
|
184
|
+
- Use this for transient reactions that should play once and recover on the Kichi side, instead of changing persistent pose/action state.
|
|
185
|
+
|
|
168
186
|
### kichi_clock
|
|
169
187
|
|
|
170
188
|
Send clock command:
|
|
@@ -17,7 +17,7 @@ Install:
|
|
|
17
17
|
openclaw plugins install @yahaha-studio/kichi-forwarder@latest
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
For npm-installed plugins, OpenClaw installs and enables the plugin through `plugins install`. If the Gateway is already running with the default config reload behavior, the required plugin reload/restart is handled there; otherwise restart the Gateway once after install. Plugin tools (`kichi_join`, `kichi_rejoin`, etc.) become available after that restart/reload completes.
|
|
20
|
+
For npm-installed plugins, OpenClaw installs and enables the plugin through `plugins install`. If the Gateway is already running with the default config reload behavior, the required plugin reload/restart is handled there; otherwise restart the Gateway once after install. Plugin tools (`kichi_join`, `kichi_rejoin`, `kichi_command`, etc.) become available after that restart/reload completes.
|
|
21
21
|
|
|
22
22
|
## Runtime Animation Config (Required)
|
|
23
23
|
|
package/src/service.ts
CHANGED
|
@@ -5,6 +5,8 @@ import * as path from "path";
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import type { Logger } from "openclaw/plugin-sdk";
|
|
7
7
|
import type {
|
|
8
|
+
AvatarCommand,
|
|
9
|
+
AvatarCommandPayload,
|
|
8
10
|
ClockAction,
|
|
9
11
|
ClockConfig,
|
|
10
12
|
ClockPayload,
|
|
@@ -323,6 +325,22 @@ export class KichiForwarderService {
|
|
|
323
325
|
this.ws.send(JSON.stringify(payload));
|
|
324
326
|
}
|
|
325
327
|
|
|
328
|
+
sendAvatarCommand(command: AvatarCommand, bubble?: string, log?: string): boolean {
|
|
329
|
+
const identity = this.requireIdentity();
|
|
330
|
+
if (!identity || this.ws?.readyState !== WebSocket.OPEN) return false;
|
|
331
|
+
|
|
332
|
+
const payload: AvatarCommandPayload = {
|
|
333
|
+
type: "avatar_command",
|
|
334
|
+
avatarId: identity.avatarId,
|
|
335
|
+
authKey: identity.authKey,
|
|
336
|
+
command,
|
|
337
|
+
...(typeof bubble === "string" && bubble.trim() ? { bubble: bubble.trim() } : {}),
|
|
338
|
+
...(typeof log === "string" && log.trim() ? { log: log.trim() } : {}),
|
|
339
|
+
};
|
|
340
|
+
this.ws.send(JSON.stringify(payload));
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
326
344
|
sendHookNotify(hookType: HookNotifyType, bubble: string): void {
|
|
327
345
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
|
|
328
346
|
const payload: HookNotifyPayload = {
|
package/src/types.ts
CHANGED
|
@@ -96,6 +96,17 @@ export type StatusPayload = {
|
|
|
96
96
|
log: string;
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
+
export type AvatarCommand = "look_at_screen";
|
|
100
|
+
|
|
101
|
+
export type AvatarCommandPayload = {
|
|
102
|
+
type: "avatar_command";
|
|
103
|
+
avatarId: string;
|
|
104
|
+
authKey: string;
|
|
105
|
+
command: AvatarCommand;
|
|
106
|
+
bubble?: string;
|
|
107
|
+
log?: string;
|
|
108
|
+
};
|
|
109
|
+
|
|
99
110
|
export type HookNotifyType = "message_received" | "before_send_message";
|
|
100
111
|
|
|
101
112
|
export type HookNotifyPayload = {
|