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 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">
|
||||
<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 -->
|
||||
<button
|
||||
@click.stop="showDebug = !showDebug"
|
||||
@@ -72,6 +88,8 @@ interface Chat {
|
||||
lastMessageType?: string
|
||||
unreadCount: number
|
||||
isGroup?: boolean
|
||||
alias?: string | null
|
||||
originalName?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -82,6 +100,7 @@ interface Props {
|
||||
const props = defineProps<Props>()
|
||||
defineEmits<{
|
||||
click: []
|
||||
editAlias: [chat: Chat]
|
||||
}>()
|
||||
|
||||
const showDebug = ref(false)
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
:chat="chat"
|
||||
:active="selectedChat?.id === chat.id"
|
||||
@click="selectedChat = chat"
|
||||
@edit-alias="openAliasModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,6 +252,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alias Modal -->
|
||||
<MessagesChatAliasModal
|
||||
v-model:open="showAliasModal"
|
||||
:chat="chatToEditAlias"
|
||||
:instance-id="selectedInstance?.value || ''"
|
||||
@saved="handleAliasSaved"
|
||||
/>
|
||||
|
||||
<!-- New Chat Modal -->
|
||||
<UModal v-model:open="showNewChatModal">
|
||||
<template #content>
|
||||
@@ -344,6 +353,10 @@ const newChatPhoneNumber = ref('')
|
||||
const newChatLoading = ref(false)
|
||||
const newChatError = ref('')
|
||||
|
||||
// Alias modal state
|
||||
const showAliasModal = ref(false)
|
||||
const chatToEditAlias = ref<any>(null)
|
||||
|
||||
// Instance options for selector
|
||||
const instanceOptions = computed(() =>
|
||||
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
|
||||
const closeNewChatModal = () => {
|
||||
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(
|
||||
`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
|
||||
WHEN c.is_group THEN COALESCE(gm.subject, c.name, c.jid)
|
||||
ELSE COALESCE(ct.name, ct.push_name, c.name, SPLIT_PART(c.jid, '@', 1))
|
||||
WHEN c.is_group THEN COALESCE(c.alias, gm.subject, c.name, c.jid)
|
||||
ELSE COALESCE(c.alias, ct.name, ct.push_name, c.name, SPLIT_PART(c.jid, '@', 1))
|
||||
END as name
|
||||
FROM chats c
|
||||
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,
|
||||
jid: row.jid,
|
||||
name: row.name,
|
||||
alias: row.alias,
|
||||
isGroup: row.is_group,
|
||||
unreadCount: row.unread_count || 0,
|
||||
lastMessageAt: row.last_message_at,
|
||||
@@ -725,10 +726,10 @@ export async function handleToolCall(toolName: string, args: Record<string, any>
|
||||
|
||||
const result = await query(
|
||||
`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
|
||||
WHEN c.is_group THEN COALESCE(gm.subject, c.name, c.jid)
|
||||
ELSE COALESCE(ct.name, ct.push_name, c.name, SPLIT_PART(c.jid, '@', 1))
|
||||
WHEN c.is_group THEN COALESCE(c.alias, gm.subject, c.name, c.jid)
|
||||
ELSE COALESCE(c.alias, ct.name, ct.push_name, c.name, SPLIT_PART(c.jid, '@', 1))
|
||||
END as name,
|
||||
gm.participants as group_participants
|
||||
FROM chats c
|
||||
@@ -747,6 +748,7 @@ export async function handleToolCall(toolName: string, args: Record<string, any>
|
||||
id: chat.id,
|
||||
jid: chat.jid,
|
||||
name: chat.name,
|
||||
alias: chat.alias,
|
||||
isGroup: chat.is_group,
|
||||
unreadCount: chat.unread_count || 0,
|
||||
lastMessageAt: chat.last_message_at,
|
||||
@@ -773,16 +775,16 @@ export async function handleToolCall(toolName: string, args: Record<string, any>
|
||||
|
||||
const result = await query(
|
||||
`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
|
||||
WHEN c.is_group THEN COALESCE(gm.subject, c.name, c.jid)
|
||||
ELSE COALESCE(ct.name, ct.push_name, c.name, SPLIT_PART(c.jid, '@', 1))
|
||||
WHEN c.is_group THEN COALESCE(c.alias, gm.subject, c.name, c.jid)
|
||||
ELSE COALESCE(c.alias, ct.name, ct.push_name, c.name, SPLIT_PART(c.jid, '@', 1))
|
||||
END as name
|
||||
FROM chats c
|
||||
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
|
||||
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
|
||||
LIMIT $3`,
|
||||
[instanceId, `%${searchQuery}%`, Math.min(limit, 50)]
|
||||
@@ -794,6 +796,7 @@ export async function handleToolCall(toolName: string, args: Record<string, any>
|
||||
id: row.id,
|
||||
jid: row.jid,
|
||||
name: row.name,
|
||||
alias: row.alias,
|
||||
isGroup: row.is_group,
|
||||
unreadCount: row.unread_count || 0,
|
||||
lastMessageAt: row.last_message_at
|
||||
|
||||
Reference in New Issue
Block a user