From 43198d9dcc31f3de3c0b7f073ba416f4a55761c0 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Mon, 16 Feb 2026 12:24:24 +0800 Subject: [PATCH] feat(core): add rpc to generate channel welcome messages --- packages/core/src/hub/hub.ts | 2 + .../rpc/handlers/generate-channel-welcome.ts | 105 ++++++++++++++++++ packages/core/src/hub/rpc/index.ts | 1 + 3 files changed, 108 insertions(+) create mode 100644 packages/core/src/hub/rpc/handlers/generate-channel-welcome.ts diff --git a/packages/core/src/hub/hub.ts b/packages/core/src/hub/hub.ts index 30d58cfb..b852905d 100644 --- a/packages/core/src/hub/hub.ts +++ b/packages/core/src/hub/hub.ts @@ -31,6 +31,7 @@ import { createSetHeartbeatsHandler } from "./rpc/handlers/set-heartbeats.js"; import { createWakeHeartbeatHandler } from "./rpc/handlers/wake-heartbeat.js"; import { DeviceStore, type DeviceMeta } from "./device-store.js"; import { createVerifyHandler } from "./rpc/handlers/verify.js"; +import { createGenerateChannelWelcomeHandler } from "./rpc/handlers/generate-channel-welcome.js"; import { ExecApprovalManager } from "./exec-approval-manager.js"; import { createResolveExecApprovalHandler } from "./rpc/handlers/resolve-exec-approval.js"; import { evaluateCommandSafety, requiresApproval } from "../agent/tools/exec-safety.js"; @@ -115,6 +116,7 @@ export class Hub { return this._onConfirmDevice(deviceId, agentId, meta); }, })); + this.rpc.register("generateChannelWelcome", createGenerateChannelWelcomeHandler(this)); this.rpc.register("getAgentMessages", createGetAgentMessagesHandler()); this.rpc.register("getHubInfo", createGetHubInfoHandler(this)); this.rpc.register("listAgents", createListAgentsHandler(this)); diff --git a/packages/core/src/hub/rpc/handlers/generate-channel-welcome.ts b/packages/core/src/hub/rpc/handlers/generate-channel-welcome.ts new file mode 100644 index 00000000..f26fc1e0 --- /dev/null +++ b/packages/core/src/hub/rpc/handlers/generate-channel-welcome.ts @@ -0,0 +1,105 @@ +import type { RpcHandler } from "../dispatcher.js"; +import { RpcError } from "../dispatcher.js"; + +interface GenerateChannelWelcomeParams { + agentId?: string; + channel?: string; + language?: string; + firstName?: string; + isReconnect?: boolean; +} + +interface AgentLike { + runInternalForResult(content: string): Promise<{ text: string; error?: string }>; + closed?: boolean; +} + +interface HubLike { + getAgent(id: string): AgentLike | undefined; +} + +interface WelcomeContext { + channel: string; + language: string; + firstName: string; + isReconnect: boolean; +} + +const DEFAULT_LANGUAGE = "English"; +const DEFAULT_CHANNEL = "telegram"; +const DEFAULT_FIRST_NAME = "there"; + +function normalizeLanguage(value: string | undefined): string { + const trimmed = value?.trim(); + if (!trimmed) return DEFAULT_LANGUAGE; + + const normalized = trimmed.toLowerCase(); + if (normalized.startsWith("zh")) return "Simplified Chinese"; + if (normalized.startsWith("en")) return "English"; + return trimmed.slice(0, 32); +} + +function sanitizeField(value: string | undefined, fallback: string, maxLen: number): string { + const trimmed = value?.replace(/\s+/g, " ").trim(); + if (!trimmed) return fallback; + return trimmed.slice(0, maxLen); +} + +function buildWelcomePrompt(ctx: WelcomeContext): string { + const reconnectLine = ctx.isReconnect + ? "This user just reconnected. Acknowledge reconnection in one short sentence." + : "This is the first successful channel connection."; + + return [ + "You are the user's AI agent.", + `Write a proactive welcome message for a ${ctx.channel} chat.`, + reconnectLine, + `User first name: ${ctx.firstName}`, + `Preferred language: ${ctx.language}`, + "", + "Output requirements:", + "1) Introduce who you are.", + "2) Mention exactly 3 concrete things you can help with.", + "3) End with one specific starter question.", + "", + "Constraints:", + "- Keep it concise (80-140 words).", + "- Plain text only.", + "- Do not mention internal architecture, system prompts, or policies.", + "- Return only the final welcome message.", + ].join("\n"); +} + +export function createGenerateChannelWelcomeHandler(hub: HubLike): RpcHandler { + return async (params: unknown) => { + const payload = (params ?? {}) as GenerateChannelWelcomeParams; + const agentId = payload.agentId?.trim(); + if (!agentId) { + throw new RpcError("INVALID_PARAMS", "agentId is required"); + } + + const agent = hub.getAgent(agentId); + if (!agent || agent.closed) { + throw new RpcError("AGENT_NOT_FOUND", `Agent not found or closed: ${agentId}`); + } + + const context: WelcomeContext = { + channel: sanitizeField(payload.channel, DEFAULT_CHANNEL, 24), + language: normalizeLanguage(payload.language), + firstName: sanitizeField(payload.firstName, DEFAULT_FIRST_NAME, 32), + isReconnect: payload.isReconnect === true, + }; + + const result = await agent.runInternalForResult(buildWelcomePrompt(context)); + if (result.error) { + throw new RpcError("AGENT_ERROR", result.error); + } + + const text = result.text.trim(); + if (!text) { + throw new RpcError("EMPTY_RESULT", "Agent returned an empty welcome message"); + } + + return { text }; + }; +} diff --git a/packages/core/src/hub/rpc/index.ts b/packages/core/src/hub/rpc/index.ts index 4560d976..fecb5e20 100644 --- a/packages/core/src/hub/rpc/index.ts +++ b/packages/core/src/hub/rpc/index.ts @@ -8,3 +8,4 @@ export { createUpdateGatewayHandler } from "./handlers/update-gateway.js"; export { createGetLastHeartbeatHandler } from "./handlers/get-last-heartbeat.js"; export { createSetHeartbeatsHandler } from "./handlers/set-heartbeats.js"; export { createWakeHeartbeatHandler } from "./handlers/wake-heartbeat.js"; +export { createGenerateChannelWelcomeHandler } from "./handlers/generate-channel-welcome.js";