Feat: Agregar sistema de alias para chats
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m10s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m10s
- Agregar campo alias a tabla chats con migración 003 - Crear endpoint PUT /api/messages/:instanceId/:chatId/alias - Modificar MCP para priorizar alias sobre nombres automáticos - Crear modal ChatAliasModal para editar alias desde UI - Agregar botón de editar alias en ChatItem - Integrar modal en página de mensajes El alias permite asignar nombres personalizados a chats que tienen prioridad sobre los nombres de WhatsApp tanto en la interfaz como en el MCP para agentes IA.
This commit is contained in:
126
app/components/messages/ChatAliasModal.vue
Normal file
126
app/components/messages/ChatAliasModal.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<UModal v-model:open="isOpen">
|
||||||
|
<template #content>
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-white">Editar Alias</h3>
|
||||||
|
<UButton
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-lucide-x"
|
||||||
|
size="sm"
|
||||||
|
@click="isOpen = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-sm text-[var(--wa-text-muted)]">
|
||||||
|
Asigna un nombre personalizado a este chat. El alias tiene prioridad sobre el nombre de WhatsApp.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<UFormField label="Alias">
|
||||||
|
<UInput
|
||||||
|
v-model="aliasValue"
|
||||||
|
placeholder="Nombre personalizado (dejar vacio para usar nombre original)"
|
||||||
|
icon="i-lucide-pencil"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<div v-if="chat" class="p-3 rounded-lg bg-[var(--wa-bg-light)]">
|
||||||
|
<p class="text-xs text-[var(--wa-text-muted)] mb-1">Nombre original:</p>
|
||||||
|
<p class="text-sm text-white">{{ chat.originalName || chat.jid }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-between gap-2">
|
||||||
|
<UButton
|
||||||
|
v-if="aliasValue"
|
||||||
|
variant="ghost"
|
||||||
|
color="error"
|
||||||
|
icon="i-lucide-trash-2"
|
||||||
|
@click="clearAlias"
|
||||||
|
>
|
||||||
|
Quitar alias
|
||||||
|
</UButton>
|
||||||
|
<div class="flex-1" />
|
||||||
|
<UButton variant="ghost" @click="isOpen = false">
|
||||||
|
Cancelar
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
:loading="isSaving"
|
||||||
|
@click="handleSave"
|
||||||
|
>
|
||||||
|
Guardar
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Chat {
|
||||||
|
id: string
|
||||||
|
jid: string
|
||||||
|
name: string
|
||||||
|
alias?: string | null
|
||||||
|
originalName?: string
|
||||||
|
isGroup?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
chat: Chat | null
|
||||||
|
instanceId: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isOpen = defineModel<boolean>('open', { default: false })
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
saved: [chat: Chat]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isSaving = ref(false)
|
||||||
|
const aliasValue = ref('')
|
||||||
|
|
||||||
|
const clearAlias = () => {
|
||||||
|
aliasValue.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!props.chat || !props.instanceId) return
|
||||||
|
|
||||||
|
isSaving.value = true
|
||||||
|
try {
|
||||||
|
const response = await $fetch(`/api/messages/${props.instanceId}/${props.chat.id}/alias`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: {
|
||||||
|
alias: aliasValue.value.trim() || null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
emit('saved', {
|
||||||
|
...props.chat,
|
||||||
|
alias: response.chat.alias,
|
||||||
|
name: response.chat.alias || props.chat.originalName || props.chat.name
|
||||||
|
})
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving alias:', error)
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize alias when modal opens or chat changes
|
||||||
|
watch([isOpen, () => props.chat], ([open, chat]) => {
|
||||||
|
if (open && chat) {
|
||||||
|
aliasValue.value = chat.alias || ''
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
</script>
|
||||||
@@ -13,9 +13,25 @@
|
|||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<p class="font-medium text-[var(--wa-text)] truncate">{{ chat.name }}</p>
|
<div class="flex items-center gap-1 min-w-0">
|
||||||
|
<p class="font-medium text-[var(--wa-text)] truncate">{{ chat.name }}</p>
|
||||||
|
<UIcon
|
||||||
|
v-if="chat.alias"
|
||||||
|
name="i-lucide-pencil"
|
||||||
|
class="w-3 h-3 text-[var(--wa-blue)] flex-shrink-0"
|
||||||
|
title="Tiene alias personalizado"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<span class="text-xs text-[var(--wa-text-muted)]">{{ formatTime(chat.lastMessageAt) }}</span>
|
<span class="text-xs text-[var(--wa-text-muted)]">{{ formatTime(chat.lastMessageAt) }}</span>
|
||||||
|
<!-- Edit alias button -->
|
||||||
|
<button
|
||||||
|
@click.stop="$emit('editAlias', chat)"
|
||||||
|
class="text-xs text-[var(--wa-text-muted)] hover:text-[var(--wa-blue)] opacity-50 hover:opacity-100"
|
||||||
|
title="Editar alias"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-user-pen" class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
<!-- Debug button -->
|
<!-- Debug button -->
|
||||||
<button
|
<button
|
||||||
@click.stop="showDebug = !showDebug"
|
@click.stop="showDebug = !showDebug"
|
||||||
@@ -72,6 +88,8 @@ interface Chat {
|
|||||||
lastMessageType?: string
|
lastMessageType?: string
|
||||||
unreadCount: number
|
unreadCount: number
|
||||||
isGroup?: boolean
|
isGroup?: boolean
|
||||||
|
alias?: string | null
|
||||||
|
originalName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -82,6 +100,7 @@ interface Props {
|
|||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
click: []
|
click: []
|
||||||
|
editAlias: [chat: Chat]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const showDebug = ref(false)
|
const showDebug = ref(false)
|
||||||
|
|||||||
@@ -118,6 +118,7 @@
|
|||||||
:chat="chat"
|
:chat="chat"
|
||||||
:active="selectedChat?.id === chat.id"
|
:active="selectedChat?.id === chat.id"
|
||||||
@click="selectedChat = chat"
|
@click="selectedChat = chat"
|
||||||
|
@edit-alias="openAliasModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -251,6 +252,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Alias Modal -->
|
||||||
|
<MessagesChatAliasModal
|
||||||
|
v-model:open="showAliasModal"
|
||||||
|
:chat="chatToEditAlias"
|
||||||
|
:instance-id="selectedInstance?.value || ''"
|
||||||
|
@saved="handleAliasSaved"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- New Chat Modal -->
|
<!-- New Chat Modal -->
|
||||||
<UModal v-model:open="showNewChatModal">
|
<UModal v-model:open="showNewChatModal">
|
||||||
<template #content>
|
<template #content>
|
||||||
@@ -344,6 +353,10 @@ const newChatPhoneNumber = ref('')
|
|||||||
const newChatLoading = ref(false)
|
const newChatLoading = ref(false)
|
||||||
const newChatError = ref('')
|
const newChatError = ref('')
|
||||||
|
|
||||||
|
// Alias modal state
|
||||||
|
const showAliasModal = ref(false)
|
||||||
|
const chatToEditAlias = ref<any>(null)
|
||||||
|
|
||||||
// Instance options for selector
|
// Instance options for selector
|
||||||
const instanceOptions = computed(() =>
|
const instanceOptions = computed(() =>
|
||||||
instances.value
|
instances.value
|
||||||
@@ -723,6 +736,36 @@ const handleReact = async (message: any, emoji: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alias modal functions
|
||||||
|
const openAliasModal = (chat: any) => {
|
||||||
|
chatToEditAlias.value = {
|
||||||
|
...chat,
|
||||||
|
originalName: chat.originalName || chat.name
|
||||||
|
}
|
||||||
|
showAliasModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAliasSaved = (updatedChat: any) => {
|
||||||
|
// Update chat in list
|
||||||
|
const index = chats.value.findIndex(c => c.id === updatedChat.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
chats.value[index] = {
|
||||||
|
...chats.value[index],
|
||||||
|
alias: updatedChat.alias,
|
||||||
|
name: updatedChat.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update selected chat if it's the same
|
||||||
|
if (selectedChat.value?.id === updatedChat.id) {
|
||||||
|
selectedChat.value = {
|
||||||
|
...selectedChat.value,
|
||||||
|
alias: updatedChat.alias,
|
||||||
|
name: updatedChat.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// New chat modal functions
|
// New chat modal functions
|
||||||
const closeNewChatModal = () => {
|
const closeNewChatModal = () => {
|
||||||
showNewChatModal.value = false
|
showNewChatModal.value = false
|
||||||
|
|||||||
68
server/api/messages/[instanceId]/[chatId]/alias.put.ts
Normal file
68
server/api/messages/[instanceId]/[chatId]/alias.put.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* PUT /api/messages/:instanceId/:chatId/alias
|
||||||
|
* Update the alias of a chat
|
||||||
|
*/
|
||||||
|
import { query } from '../../../../utils/database'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const username = getHeader(event, 'x-authentik-username')
|
||||||
|
if (!username) {
|
||||||
|
throw createError({ statusCode: 401, message: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceId = getRouterParam(event, 'instanceId')
|
||||||
|
const chatId = getRouterParam(event, 'chatId')
|
||||||
|
|
||||||
|
if (!instanceId) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Missing instanceId' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chatId) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Missing chatId' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody<{ alias: string | null }>(event)
|
||||||
|
|
||||||
|
// alias can be null to remove it, or a string to set it
|
||||||
|
if (body?.alias === undefined) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Missing alias in request body' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update the alias
|
||||||
|
const result = await query(
|
||||||
|
`UPDATE chats
|
||||||
|
SET alias = $1, updated_at = NOW()
|
||||||
|
WHERE id = $2 AND instance_id = $3
|
||||||
|
RETURNING id, jid, name, alias, is_group`,
|
||||||
|
[body.alias || null, chatId, instanceId]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
throw createError({ statusCode: 404, message: 'Chat not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const chat = result.rows[0]
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
chat: {
|
||||||
|
id: chat.id,
|
||||||
|
jid: chat.jid,
|
||||||
|
name: chat.name,
|
||||||
|
alias: chat.alias,
|
||||||
|
isGroup: chat.is_group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Alias API] Error updating alias:', error)
|
||||||
|
|
||||||
|
if (error.statusCode) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
message: error.message || 'Error updating alias'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
12
server/database/migrations/003_add_alias.sql
Normal file
12
server/database/migrations/003_add_alias.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- Migration 003: Add alias field to chats
|
||||||
|
-- =====================================================
|
||||||
|
-- Allows users to assign custom names (aliases) to chats
|
||||||
|
-- Alias takes priority over automatic names from WhatsApp
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Add alias column to chats table
|
||||||
|
ALTER TABLE chats ADD COLUMN IF NOT EXISTS alias VARCHAR(255);
|
||||||
|
|
||||||
|
-- Add index for alias searches (partial index for non-null values)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chats_alias ON chats(alias) WHERE alias IS NOT NULL;
|
||||||
@@ -688,10 +688,10 @@ export async function handleToolCall(toolName: string, args: Record<string, any>
|
|||||||
|
|
||||||
const result = await query(
|
const result = await query(
|
||||||
`SELECT
|
`SELECT
|
||||||
c.id, c.jid, c.is_group, c.unread_count, c.last_message_at, c.last_message_type,
|
c.id, c.jid, c.is_group, c.unread_count, c.last_message_at, c.last_message_type, c.alias,
|
||||||
CASE
|
CASE
|
||||||
WHEN c.is_group THEN COALESCE(gm.subject, c.name, c.jid)
|
WHEN c.is_group THEN COALESCE(c.alias, gm.subject, c.name, c.jid)
|
||||||
ELSE COALESCE(ct.name, ct.push_name, c.name, SPLIT_PART(c.jid, '@', 1))
|
ELSE COALESCE(c.alias, ct.name, ct.push_name, c.name, SPLIT_PART(c.jid, '@', 1))
|
||||||
END as name
|
END as name
|
||||||
FROM chats c
|
FROM chats c
|
||||||
LEFT JOIN contacts ct ON c.instance_id = ct.instance_id AND c.jid = ct.jid
|
LEFT JOIN contacts ct ON c.instance_id = ct.instance_id AND c.jid = ct.jid
|
||||||
@@ -708,6 +708,7 @@ export async function handleToolCall(toolName: string, args: Record<string, any>
|
|||||||
id: row.id,
|
id: row.id,
|
||||||
jid: row.jid,
|
jid: row.jid,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
alias: row.alias,
|
||||||
isGroup: row.is_group,
|
isGroup: row.is_group,
|
||||||
unreadCount: row.unread_count || 0,
|
unreadCount: row.unread_count || 0,
|
||||||
lastMessageAt: row.last_message_at,
|
lastMessageAt: row.last_message_at,
|
||||||
@@ -725,10 +726,10 @@ export async function handleToolCall(toolName: string, args: Record<string, any>
|
|||||||
|
|
||||||
const result = await query(
|
const result = await query(
|
||||||
`SELECT
|
`SELECT
|
||||||
c.id, c.jid, c.is_group, c.unread_count, c.last_message_at, c.last_message_type,
|
c.id, c.jid, c.is_group, c.unread_count, c.last_message_at, c.last_message_type, c.alias,
|
||||||
CASE
|
CASE
|
||||||
WHEN c.is_group THEN COALESCE(gm.subject, c.name, c.jid)
|
WHEN c.is_group THEN COALESCE(c.alias, gm.subject, c.name, c.jid)
|
||||||
ELSE COALESCE(ct.name, ct.push_name, c.name, SPLIT_PART(c.jid, '@', 1))
|
ELSE COALESCE(c.alias, ct.name, ct.push_name, c.name, SPLIT_PART(c.jid, '@', 1))
|
||||||
END as name,
|
END as name,
|
||||||
gm.participants as group_participants
|
gm.participants as group_participants
|
||||||
FROM chats c
|
FROM chats c
|
||||||
@@ -747,6 +748,7 @@ export async function handleToolCall(toolName: string, args: Record<string, any>
|
|||||||
id: chat.id,
|
id: chat.id,
|
||||||
jid: chat.jid,
|
jid: chat.jid,
|
||||||
name: chat.name,
|
name: chat.name,
|
||||||
|
alias: chat.alias,
|
||||||
isGroup: chat.is_group,
|
isGroup: chat.is_group,
|
||||||
unreadCount: chat.unread_count || 0,
|
unreadCount: chat.unread_count || 0,
|
||||||
lastMessageAt: chat.last_message_at,
|
lastMessageAt: chat.last_message_at,
|
||||||
@@ -773,16 +775,16 @@ export async function handleToolCall(toolName: string, args: Record<string, any>
|
|||||||
|
|
||||||
const result = await query(
|
const result = await query(
|
||||||
`SELECT
|
`SELECT
|
||||||
c.id, c.jid, c.is_group, c.unread_count, c.last_message_at,
|
c.id, c.jid, c.is_group, c.unread_count, c.last_message_at, c.alias,
|
||||||
CASE
|
CASE
|
||||||
WHEN c.is_group THEN COALESCE(gm.subject, c.name, c.jid)
|
WHEN c.is_group THEN COALESCE(c.alias, gm.subject, c.name, c.jid)
|
||||||
ELSE COALESCE(ct.name, ct.push_name, c.name, SPLIT_PART(c.jid, '@', 1))
|
ELSE COALESCE(c.alias, ct.name, ct.push_name, c.name, SPLIT_PART(c.jid, '@', 1))
|
||||||
END as name
|
END as name
|
||||||
FROM chats c
|
FROM chats c
|
||||||
LEFT JOIN contacts ct ON c.instance_id = ct.instance_id AND c.jid = ct.jid
|
LEFT JOIN contacts ct ON c.instance_id = ct.instance_id AND c.jid = ct.jid
|
||||||
LEFT JOIN group_metadata gm ON c.instance_id = gm.instance_id AND c.jid = gm.jid
|
LEFT JOIN group_metadata gm ON c.instance_id = gm.instance_id AND c.jid = gm.jid
|
||||||
WHERE c.instance_id = $1
|
WHERE c.instance_id = $1
|
||||||
AND (c.name ILIKE $2 OR c.jid ILIKE $2 OR ct.name ILIKE $2 OR ct.push_name ILIKE $2 OR gm.subject ILIKE $2)
|
AND (c.alias ILIKE $2 OR c.name ILIKE $2 OR c.jid ILIKE $2 OR ct.name ILIKE $2 OR ct.push_name ILIKE $2 OR gm.subject ILIKE $2)
|
||||||
ORDER BY c.last_message_at DESC NULLS LAST
|
ORDER BY c.last_message_at DESC NULLS LAST
|
||||||
LIMIT $3`,
|
LIMIT $3`,
|
||||||
[instanceId, `%${searchQuery}%`, Math.min(limit, 50)]
|
[instanceId, `%${searchQuery}%`, Math.min(limit, 50)]
|
||||||
@@ -794,6 +796,7 @@ export async function handleToolCall(toolName: string, args: Record<string, any>
|
|||||||
id: row.id,
|
id: row.id,
|
||||||
jid: row.jid,
|
jid: row.jid,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
alias: row.alias,
|
||||||
isGroup: row.is_group,
|
isGroup: row.is_group,
|
||||||
unreadCount: row.unread_count || 0,
|
unreadCount: row.unread_count || 0,
|
||||||
lastMessageAt: row.last_message_at
|
lastMessageAt: row.last_message_at
|
||||||
|
|||||||
Reference in New Issue
Block a user