feat: registrar proyectos con fases, adjuntar documentos y asignar tareas por Telegram

- crear_fase_proyecto: agrega fases/etapas a un proyecto (se puede llamar
  varias veces seguidas para cargar todas las fases de un proyecto nuevo).
- adjuntar_documento_proyecto: guarda el archivo que el usuario acaba de
  enviar por Telegram como documento del proyecto (mismo mecanismo de
  adjuntos ya usado para facturas — se generalizó TelegramAttachment para
  servir a ambos casos).
- listar_usuarios + asignado_id en crear_tarea + asignar_tarea: permite
  asignar tareas a alguien del equipo por Telegram, disparando la misma
  notificación (Telegram/email/sistema) que ya dispara el dashboard.
- El prompt del sistema ahora indica cómo decidir entre adjuntar_factura y
  adjuntar_documento_proyecto según el contexto del archivo recibido.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro GD
2026-08-03 02:11:02 +00:00
co-authored by Claude Sonnet 5
parent cd0ca6d232
commit a0b2e9ae0a
5 changed files with 278 additions and 45 deletions
+27 -25
View File
@@ -12,20 +12,21 @@ import (
"time"
)
// FacturaAttachment es un archivo que un usuario envió por Telegram y que queda
// "pendiente" hasta que el agente lo asocie a una factura (o expire).
type FacturaAttachment struct {
// TelegramAttachment es un archivo que un usuario envió por Telegram y que queda
// "pendiente" hasta que el agente lo asocie a una factura o a un documento de
// proyecto (o expire).
type TelegramAttachment struct {
Path string
OriginalName string
MimeType string
StagedAt time.Time
}
const facturaAttachmentTTL = 15 * time.Minute
const telegramAttachmentTTL = 15 * time.Minute
var (
facturaAttachmentsMu sync.Mutex
facturaAttachments = map[int64]*FacturaAttachment{}
telegramAttachmentsMu sync.Mutex
telegramAttachments = map[int64]*TelegramAttachment{}
)
// DescargarDocumentoTelegram descarga un archivo de Telegram (por file_id) usando
@@ -75,41 +76,42 @@ func DescargarDocumentoTelegram(botToken, fileID, originalName, mimeType string)
return localPath, nil
}
// StageFacturaAttachment registra un archivo ya descargado como pendiente para el
// chat, a la espera de que el agente lo asocie a una factura con la tool adjuntar_factura.
func StageFacturaAttachment(chatID int64, path, originalName, mimeType string) {
facturaAttachmentsMu.Lock()
defer facturaAttachmentsMu.Unlock()
facturaAttachments[chatID] = &FacturaAttachment{
// StageTelegramAttachment registra un archivo ya descargado como pendiente para el
// chat, a la espera de que el agente lo asocie a algo (adjuntar_factura o
// adjuntar_documento_proyecto).
func StageTelegramAttachment(chatID int64, path, originalName, mimeType string) {
telegramAttachmentsMu.Lock()
defer telegramAttachmentsMu.Unlock()
telegramAttachments[chatID] = &TelegramAttachment{
Path: path, OriginalName: originalName, MimeType: mimeType, StagedAt: time.Now(),
}
}
// PopFacturaAttachment retorna y remueve el archivo pendiente de un chat, si
// PopTelegramAttachment retorna y remueve el archivo pendiente de un chat, si
// existe y no expiró (si expiró, borra el archivo temporal del disco).
func PopFacturaAttachment(chatID int64) (*FacturaAttachment, bool) {
facturaAttachmentsMu.Lock()
defer facturaAttachmentsMu.Unlock()
a, ok := facturaAttachments[chatID]
func PopTelegramAttachment(chatID int64) (*TelegramAttachment, bool) {
telegramAttachmentsMu.Lock()
defer telegramAttachmentsMu.Unlock()
a, ok := telegramAttachments[chatID]
if !ok {
return nil, false
}
delete(facturaAttachments, chatID)
if time.Since(a.StagedAt) > facturaAttachmentTTL {
delete(telegramAttachments, chatID)
if time.Since(a.StagedAt) > telegramAttachmentTTL {
_ = os.Remove(a.Path)
return nil, false
}
return a, true
}
// PeekFacturaAttachment consulta el archivo pendiente de un chat sin removerlo
// PeekTelegramAttachment consulta el archivo pendiente de un chat sin removerlo
// (se usa para leer su contenido y pasárselo al modelo antes de que la tool
// adjuntar_factura lo consuma de verdad).
func PeekFacturaAttachment(chatID int64) (*FacturaAttachment, bool) {
facturaAttachmentsMu.Lock()
defer facturaAttachmentsMu.Unlock()
a, ok := facturaAttachments[chatID]
if !ok || time.Since(a.StagedAt) > facturaAttachmentTTL {
func PeekTelegramAttachment(chatID int64) (*TelegramAttachment, bool) {
telegramAttachmentsMu.Lock()
defer telegramAttachmentsMu.Unlock()
a, ok := telegramAttachments[chatID]
if !ok || time.Since(a.StagedAt) > telegramAttachmentTTL {
return nil, false
}
return a, true