fix(telegram-bot): fix velin rendering, action loop, and LLM compatibility (#1333)

This commit is contained in:
yuanbo
2026-03-13 18:31:31 +08:00
committed by GitHub
parent 3aa47ff43d
commit f654d175f8
4 changed files with 93 additions and 47 deletions
@@ -69,20 +69,18 @@ export async function sendMessage(
chatContext.currentTask = null
}
// Check if we should abort due to new messages since processing began
if (botContext.unreadMessages[chatId] && botContext.unreadMessages[chatId].length > 0) {
botContext.logger.log(`Not sending message to ${chatId} - new messages arrived`)
return // Don't send the message, let the next processing loop handle it
}
// Note: removed "new messages arrived" check — it was preventing the bot
// from ever responding in fast-paced chats. The agent loop handles new messages naturally.
const systemContent = String(await messageSplit())
const req = {
apiKey: env.LLM_API_KEY!,
baseURL: env.LLM_API_BASE_URL!,
model: env.LLM_MODEL!,
messages: message.messages(
message.system(await messageSplit()),
message.user('This is the input message:'),
message.user(responseText),
{ role: 'system' as const, content: systemContent },
{ role: 'user' as const, content: 'This is the input message:' },
{ role: 'user' as const, content: String(responseText) },
),
abortSignal: abortController.signal,
} satisfies GenerateTextOptions
@@ -55,7 +55,7 @@ async function dispatchAction(ctx: BotContext, action: Action, abortController:
}
}
return () => handleLoopStep(ctx, chatCtx)
return
}
case 'send_sticker':
{
@@ -135,7 +135,7 @@ async function dispatchAction(ctx: BotContext, action: Action, abortController:
chatCtx.actions.push({ action, result: `AIRI System: List of chats:${(await listJoinedChats()).map(chat => `ID:${chat.chat_id}, Name:${chat.chat_name}`).join('\n')}` })
}
return () => handleLoopStep(ctx, chatCtx)
return
case 'send_message':
{
const chatCtx = ensureChatContext(ctx, action.chatId)
@@ -168,8 +168,6 @@ async function dispatchAction(ctx: BotContext, action: Action, abortController:
if (chatCtx) {
chatCtx.messages.push(message.user(`AIRI System: The action you sent ${action.action} haven't implemented yet by developer.`))
}
return () => handleLoopStep(ctx, chatCtx)
}
}
+78 -29
View File
@@ -33,36 +33,34 @@ export async function imagineAnAction(
let responseText = ''
const systemContent = String(div(
await systemTicking(),
await personality(),
))
const userContent = String(div(
vif(
globalStates?.incomingMessages?.length > 0,
div(
'Incoming messages:',
globalStates?.incomingMessages?.filter(Boolean).map(msg => `- ${msg?.text}`).join('\n'),
),
),
'History actions:',
actions.map(a => `- Action: ${JSON.stringify(a.action)}, Result: ${JSON.stringify(a.result)}`).join('\n'),
span(`
Currently, it's ${new Date()} on the server that hosts you.
The others in the group may live in a different timezone, so please be aware of the time difference.
`),
`You have total ${Object.values(globalStates.unreadMessages).reduce((acc, cur) => acc + cur.length, 0)} unread messages.`,
'Unread messages count are:',
Object.entries(globalStates.unreadMessages).map(([key, value]) => `ID:${key}, Unread message count:${value.length}`).join('\n'),
'Based on the context, What do you want to do? Choose a right action from the listing of the tools you want to take next.',
'Respond with the action and parameters you choose in JSON only, without any explanation and markups.',
))
const requestMessages = message.messages(
message.system(
div(
await systemTicking(),
await personality(),
),
),
{ role: 'system' as const, content: systemContent },
...messages,
message.user(
div(
vif(
globalStates?.incomingMessages?.length > 0,
div(
'Incoming messages:',
globalStates?.incomingMessages?.filter(Boolean).map(msg => `- ${msg?.text}`).join('\n'),
),
),
'History actions:',
actions.map(a => `- Action: ${JSON.stringify(a.action)}, Result: ${JSON.stringify(a.result)}`).join('\n'),
span(`
Currently, it's ${new Date()} on the server that hosts you.
The others in the group may live in a different timezone, so please be aware of the time difference.
`),
`You have total ${Object.values(globalStates.unreadMessages).reduce((acc, cur) => acc + cur.length, 0)} unread messages.`,
'Unread messages count are:',
Object.entries(globalStates.unreadMessages).map(([key, value]) => `ID:${key}, Unread message count:${value.length}`).join('\n'),
'Based on the context, What do you want to do? Choose a right action from the listing of the tools you want to take next.',
'Respond with the action and parameters you choose in JSON only, without any explanation and markups.',
),
),
{ role: 'user' as const, content: userContent },
)
try {
@@ -114,7 +112,58 @@ export async function imagineAnAction(
.replace(/\n```$/, '')
.trim()
const action = parse(responseText) as Action
const raw = parse(responseText) as Record<string, unknown>
// Normalize: some models wrap fields inside "parameters", flatten to top level
if (raw.parameters && typeof raw.parameters === 'object') {
const params = raw.parameters as Record<string, unknown>
for (const [key, value] of Object.entries(params)) {
if (!(key in raw))
raw[key] = value
}
delete raw.parameters
}
// Normalize action name aliases
const actionAliases: Record<string, string> = {
read_messages: 'read_unread_messages',
get_unread_messages: 'read_unread_messages',
check_messages: 'read_unread_messages',
reply_to_a_message_from_a_chat: 'send_message',
reply_message: 'send_message',
get_messages_from_chat: 'read_unread_messages',
Read_unread_messages: 'read_unread_messages',
}
if (typeof raw.action === 'string' && actionAliases[raw.action])
raw.action = actionAliases[raw.action]
// Normalize field name aliases
if (!raw.chatId) {
raw.chatId = raw.recipient_id ?? raw.group_id ?? raw.chat_id ?? raw.user_id ?? raw.conversation_id ?? raw.unread_message_id ?? raw.id
delete raw.recipient_id
delete raw.group_id
delete raw.chat_id
delete raw.user_id
delete raw.conversation_id
delete raw.unread_message_id
}
if (!raw.content) {
raw.content = raw.message ?? raw.text ?? raw.chat_message
delete raw.message
delete raw.text
delete raw.chat_message
}
// Fallback: infer chatId from unreadMessages if only one chat has unread
if (!raw.chatId) {
const unreadChatIds = Object.keys(globalStates.unreadMessages).filter(
k => globalStates.unreadMessages[k]?.length > 0,
)
if (unreadChatIds.length >= 1)
raw.chatId = unreadChatIds[0]
}
const action = raw as unknown as Action
s.setAttribute('telegram.bot.id', botId)
s.setAttribute('telegram.module.generate_agent_action.parsed_action', JSON.stringify(action))
+7 -6
View File
@@ -16,12 +16,13 @@ export function importVelin(module: string, base: string): VelinModule {
return {
render: async (data) => {
const content = (await readFile(relativeOf(module, base))).toString('utf-8')
if (isMarkdown(module)) {
return renderMarkdownString(content, data)
}
return renderSFCString(content, data)
const result = isMarkdown(module)
? await renderMarkdownString(content, data)
: await renderSFCString(content, data)
// renderMarkdownString returns { rendered, props } instead of a plain string
if (result && typeof result === 'object' && 'rendered' in result)
return (result as { rendered: string }).rendered
return result
},
}
}