|
|
|
|
@@ -25,6 +25,7 @@ type createChannelRequest struct {
|
|
|
|
|
SystemPrompt string `json:"system_prompt,omitempty"`
|
|
|
|
|
ProviderConfigID *string `json:"provider_config_id,omitempty"`
|
|
|
|
|
Folder string `json:"folder,omitempty"`
|
|
|
|
|
FolderID *string `json:"folder_id,omitempty"`
|
|
|
|
|
Tags []string `json:"tags,omitempty"`
|
|
|
|
|
// v0.23.2: DM creation
|
|
|
|
|
Participants []string `json:"participants,omitempty"` // user IDs for DM (exactly 1 other user)
|
|
|
|
|
@@ -40,6 +41,7 @@ type updateChannelRequest struct {
|
|
|
|
|
IsArchived *bool `json:"is_archived,omitempty"`
|
|
|
|
|
IsPinned *bool `json:"is_pinned,omitempty"`
|
|
|
|
|
Folder *string `json:"folder,omitempty"`
|
|
|
|
|
FolderID *string `json:"folder_id,omitempty"`
|
|
|
|
|
Tags []string `json:"tags,omitempty"`
|
|
|
|
|
Settings *json.RawMessage `json:"settings,omitempty"` // JSONB merge into existing settings
|
|
|
|
|
WorkspaceID *string `json:"workspace_id,omitempty"` // bind workspace (v0.21.5)
|
|
|
|
|
@@ -61,6 +63,7 @@ type channelResponse struct {
|
|
|
|
|
IsArchived bool `json:"is_archived"`
|
|
|
|
|
IsPinned bool `json:"is_pinned"`
|
|
|
|
|
Folder *string `json:"folder"`
|
|
|
|
|
FolderID *string `json:"folder_id,omitempty"`
|
|
|
|
|
ProjectID *string `json:"project_id,omitempty"`
|
|
|
|
|
WorkspaceID *string `json:"workspace_id,omitempty"`
|
|
|
|
|
Tags []string `json:"tags"`
|
|
|
|
|
@@ -162,6 +165,7 @@ func (h *ChannelHandler) ListChannels(c *gin.Context) {
|
|
|
|
|
// Optional filters
|
|
|
|
|
archived := c.DefaultQuery("archived", "false")
|
|
|
|
|
folder := c.Query("folder")
|
|
|
|
|
folderID := c.Query("folder_id") // UUID — preferred over folder (text)
|
|
|
|
|
channelType := c.DefaultQuery("type", "") // empty = all types
|
|
|
|
|
// v0.23.1: multi-value type filter. Supports both ?types=dm&types=channel
|
|
|
|
|
// and comma-joined ?types=dm,channel
|
|
|
|
|
@@ -202,6 +206,11 @@ func (h *ChannelHandler) ListChannels(c *gin.Context) {
|
|
|
|
|
countArgs = append(countArgs, folder)
|
|
|
|
|
argN++
|
|
|
|
|
}
|
|
|
|
|
if folderID != "" {
|
|
|
|
|
countQuery += ` AND c.folder_id = $` + strconv.Itoa(argN)
|
|
|
|
|
countArgs = append(countArgs, folderID)
|
|
|
|
|
argN++
|
|
|
|
|
}
|
|
|
|
|
if search != "" {
|
|
|
|
|
countQuery += ` AND c.title ILIKE $` + strconv.Itoa(argN)
|
|
|
|
|
countArgs = append(countArgs, "%"+search+"%")
|
|
|
|
|
@@ -226,7 +235,7 @@ func (h *ChannelHandler) ListChannels(c *gin.Context) {
|
|
|
|
|
query := `
|
|
|
|
|
SELECT c.id, c.user_id, c.title, c.type, c.ai_mode, c.topic,
|
|
|
|
|
c.description, c.model, c.provider_config_id,
|
|
|
|
|
c.system_prompt, c.is_archived, c.is_pinned, c.folder, c.project_id, c.workspace_id,
|
|
|
|
|
c.system_prompt, c.is_archived, c.is_pinned, c.folder, c.folder_id, c.project_id, c.workspace_id,
|
|
|
|
|
c.tags, c.settings,
|
|
|
|
|
COALESCE(mc.cnt, 0) AS message_count,
|
|
|
|
|
c.created_at, c.updated_at
|
|
|
|
|
@@ -259,6 +268,11 @@ func (h *ChannelHandler) ListChannels(c *gin.Context) {
|
|
|
|
|
args = append(args, folder)
|
|
|
|
|
argN++
|
|
|
|
|
}
|
|
|
|
|
if folderID != "" {
|
|
|
|
|
query += ` AND c.folder_id = $` + strconv.Itoa(argN)
|
|
|
|
|
args = append(args, folderID)
|
|
|
|
|
argN++
|
|
|
|
|
}
|
|
|
|
|
if search != "" {
|
|
|
|
|
query += ` AND c.title ILIKE $` + strconv.Itoa(argN)
|
|
|
|
|
args = append(args, "%"+search+"%")
|
|
|
|
|
@@ -289,7 +303,7 @@ func (h *ChannelHandler) ListChannels(c *gin.Context) {
|
|
|
|
|
err := rows.Scan(
|
|
|
|
|
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.AiMode, &ch.Topic,
|
|
|
|
|
&ch.Description, &ch.Model, &ch.ProviderConfigID,
|
|
|
|
|
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, &ch.WorkspaceID,
|
|
|
|
|
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.FolderID, &ch.ProjectID, &ch.WorkspaceID,
|
|
|
|
|
scanTags(&tags), scanJSON(&ch.Settings),
|
|
|
|
|
&ch.MessageCount, &ch.CreatedAt, &ch.UpdatedAt,
|
|
|
|
|
)
|
|
|
|
|
@@ -380,14 +394,16 @@ func (h *ChannelHandler) CreateChannel(c *gin.Context) {
|
|
|
|
|
var ch channelResponse
|
|
|
|
|
var tags []string
|
|
|
|
|
err := database.DB.QueryRow(database.Q(`
|
|
|
|
|
SELECT id, user_id, title, type, description, model, provider_config_id,
|
|
|
|
|
system_prompt, is_archived, is_pinned, folder, project_id, workspace_id,
|
|
|
|
|
SELECT id, user_id, title, type, ai_mode, topic,
|
|
|
|
|
description, model, provider_config_id,
|
|
|
|
|
system_prompt, is_archived, is_pinned, folder, folder_id, project_id, workspace_id,
|
|
|
|
|
tags, settings,
|
|
|
|
|
created_at, updated_at
|
|
|
|
|
FROM channels WHERE id = $1
|
|
|
|
|
`), existingID).Scan(
|
|
|
|
|
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.ProviderConfigID,
|
|
|
|
|
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, &ch.WorkspaceID,
|
|
|
|
|
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.AiMode, &ch.Topic,
|
|
|
|
|
&ch.Description, &ch.Model, &ch.ProviderConfigID,
|
|
|
|
|
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.FolderID, &ch.ProjectID, &ch.WorkspaceID,
|
|
|
|
|
scanTags(&tags), scanJSON(&ch.Settings), &ch.CreatedAt, &ch.UpdatedAt,
|
|
|
|
|
)
|
|
|
|
|
if err == nil {
|
|
|
|
|
@@ -410,10 +426,10 @@ func (h *ChannelHandler) CreateChannel(c *gin.Context) {
|
|
|
|
|
id := store.NewID()
|
|
|
|
|
_, err := database.DB.Exec(`
|
|
|
|
|
INSERT INTO channels (id, user_id, title, type, description, model,
|
|
|
|
|
system_prompt, provider_config_id, folder, tags, ai_mode)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
system_prompt, provider_config_id, folder, folder_id, tags, ai_mode)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
id, userID, req.Title, channelType, req.Description, req.Model,
|
|
|
|
|
req.SystemPrompt, req.ProviderConfigID, req.Folder, writeTagsArg(req.Tags), aiMode,
|
|
|
|
|
req.SystemPrompt, req.ProviderConfigID, req.Folder, req.FolderID, writeTagsArg(req.Tags), aiMode,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create channel"})
|
|
|
|
|
@@ -421,13 +437,15 @@ func (h *ChannelHandler) CreateChannel(c *gin.Context) {
|
|
|
|
|
}
|
|
|
|
|
// Read back the row
|
|
|
|
|
err = database.DB.QueryRow(`
|
|
|
|
|
SELECT id, user_id, title, type, description, model, provider_config_id,
|
|
|
|
|
system_prompt, is_archived, is_pinned, folder, project_id, workspace_id,
|
|
|
|
|
SELECT id, user_id, title, type, ai_mode, topic,
|
|
|
|
|
description, model, provider_config_id,
|
|
|
|
|
system_prompt, is_archived, is_pinned, folder, folder_id, project_id, workspace_id,
|
|
|
|
|
tags, settings,
|
|
|
|
|
created_at, updated_at
|
|
|
|
|
FROM channels WHERE id = ?`, id).Scan(
|
|
|
|
|
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.ProviderConfigID,
|
|
|
|
|
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, &ch.WorkspaceID,
|
|
|
|
|
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.AiMode, &ch.Topic,
|
|
|
|
|
&ch.Description, &ch.Model, &ch.ProviderConfigID,
|
|
|
|
|
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.FolderID, &ch.ProjectID, &ch.WorkspaceID,
|
|
|
|
|
scanTags(&tags), scanJSON(&ch.Settings), &ch.CreatedAt, &ch.UpdatedAt,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
@@ -436,15 +454,17 @@ func (h *ChannelHandler) CreateChannel(c *gin.Context) {
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
err := database.DB.QueryRow(`
|
|
|
|
|
INSERT INTO channels (user_id, title, type, description, model, system_prompt, provider_config_id, folder, tags, ai_mode)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
|
|
|
RETURNING id, user_id, title, type, description, model, provider_config_id, system_prompt,
|
|
|
|
|
is_archived, is_pinned, folder, project_id, workspace_id, tags, settings, created_at, updated_at
|
|
|
|
|
INSERT INTO channels (user_id, title, type, description, model, system_prompt, provider_config_id, folder, folder_id, tags, ai_mode)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
|
|
|
RETURNING id, user_id, title, type, ai_mode, topic,
|
|
|
|
|
description, model, provider_config_id, system_prompt,
|
|
|
|
|
is_archived, is_pinned, folder, folder_id, project_id, workspace_id, tags, settings, created_at, updated_at
|
|
|
|
|
`, userID, req.Title, channelType, req.Description, req.Model, req.SystemPrompt, req.ProviderConfigID,
|
|
|
|
|
req.Folder, pq.Array(req.Tags), aiMode,
|
|
|
|
|
req.Folder, req.FolderID, pq.Array(req.Tags), aiMode,
|
|
|
|
|
).Scan(
|
|
|
|
|
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.ProviderConfigID,
|
|
|
|
|
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, &ch.WorkspaceID,
|
|
|
|
|
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.AiMode, &ch.Topic,
|
|
|
|
|
&ch.Description, &ch.Model, &ch.ProviderConfigID,
|
|
|
|
|
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.FolderID, &ch.ProjectID, &ch.WorkspaceID,
|
|
|
|
|
pq.Array(&tags), scanJSON(&ch.Settings), &ch.CreatedAt, &ch.UpdatedAt,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
@@ -525,8 +545,9 @@ func (h *ChannelHandler) GetChannel(c *gin.Context) {
|
|
|
|
|
var ch channelResponse
|
|
|
|
|
var tags []string
|
|
|
|
|
err := database.DB.QueryRow(database.Q(`
|
|
|
|
|
SELECT c.id, c.user_id, c.title, c.type, c.description, c.model, c.provider_config_id,
|
|
|
|
|
c.system_prompt, c.is_archived, c.is_pinned, c.folder, c.project_id, c.workspace_id,
|
|
|
|
|
SELECT c.id, c.user_id, c.title, c.type, c.ai_mode, c.topic,
|
|
|
|
|
c.description, c.model, c.provider_config_id,
|
|
|
|
|
c.system_prompt, c.is_archived, c.is_pinned, c.folder, c.folder_id, c.project_id, c.workspace_id,
|
|
|
|
|
c.tags, c.settings,
|
|
|
|
|
COALESCE(mc.cnt, 0) AS message_count,
|
|
|
|
|
c.created_at, c.updated_at
|
|
|
|
|
@@ -534,10 +555,13 @@ func (h *ChannelHandler) GetChannel(c *gin.Context) {
|
|
|
|
|
LEFT JOIN (
|
|
|
|
|
SELECT channel_id, COUNT(*) AS cnt FROM messages GROUP BY channel_id
|
|
|
|
|
) mc ON mc.channel_id = c.id
|
|
|
|
|
WHERE c.id = $1 AND c.user_id = $2
|
|
|
|
|
WHERE c.id = $1 AND (c.user_id = $2 OR c.id IN (
|
|
|
|
|
SELECT channel_id FROM channel_participants WHERE participant_type = 'user' AND participant_id = $2
|
|
|
|
|
))
|
|
|
|
|
`), channelID, userID).Scan(
|
|
|
|
|
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.ProviderConfigID,
|
|
|
|
|
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, &ch.WorkspaceID,
|
|
|
|
|
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.AiMode, &ch.Topic,
|
|
|
|
|
&ch.Description, &ch.Model, &ch.ProviderConfigID,
|
|
|
|
|
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.FolderID, &ch.ProjectID, &ch.WorkspaceID,
|
|
|
|
|
scanTags(&tags), scanJSON(&ch.Settings),
|
|
|
|
|
&ch.MessageCount, &ch.CreatedAt, &ch.UpdatedAt,
|
|
|
|
|
)
|
|
|
|
|
@@ -621,6 +645,13 @@ func (h *ChannelHandler) UpdateChannel(c *gin.Context) {
|
|
|
|
|
if req.Folder != nil {
|
|
|
|
|
addClause("folder", *req.Folder)
|
|
|
|
|
}
|
|
|
|
|
if req.FolderID != nil {
|
|
|
|
|
if *req.FolderID == "" {
|
|
|
|
|
addClause("folder_id", nil) // unbind from folder
|
|
|
|
|
} else {
|
|
|
|
|
addClause("folder_id", *req.FolderID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if req.Tags != nil {
|
|
|
|
|
addClause("tags", writeTagsArg(req.Tags))
|
|
|
|
|
}
|
|
|
|
|
|