Files
simstudioai--sim/apps/sim/lib/workflows/orchestration/chat-deploy.ts
T
wehub-resource-sync d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

250 lines
6.7 KiB
TypeScript

import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { chat } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, isNull } from 'drizzle-orm'
import { encryptSecret } from '@/lib/core/security/encryption'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { performFullDeploy } from '@/lib/workflows/orchestration/deploy'
const logger = createLogger('ChatDeployOrchestration')
export interface ChatDeployPayload {
workflowId: string
userId: string
identifier: string
title: string
description?: string
/** Summary of what changed in this deployment version (distinct from the chat-facing `description`). */
versionDescription?: string
/** Short name/label for this deployment version. */
versionName?: string
customizations?: { primaryColor?: string; welcomeMessage?: string; imageUrl?: string }
authType?: 'public' | 'password' | 'email' | 'sso'
password?: string | null
allowedEmails?: string[]
outputConfigs?: Array<{ blockId: string; path: string }>
workspaceId?: string | null
}
export interface PerformChatDeployResult {
success: boolean
chatId?: string
chatUrl?: string
deployedAt?: Date | null
version?: number
error?: string
}
/**
* Deploys a chat: deploys the underlying workflow via `performFullDeploy`,
* encrypts passwords, creates or updates the chat record, fires telemetry,
* and records an audit entry. Both the chat API route and the copilot
* `deploy_chat` tool must use this function.
*/
export async function performChatDeploy(
params: ChatDeployPayload
): Promise<PerformChatDeployResult> {
const {
workflowId,
userId,
identifier,
title,
description = '',
authType = 'public',
password,
allowedEmails = [],
outputConfigs = [],
} = params
const customizations = {
primaryColor: params.customizations?.primaryColor || 'var(--brand-hover)',
welcomeMessage: params.customizations?.welcomeMessage || 'Hi there! How can I help you today?',
...(params.customizations?.imageUrl ? { imageUrl: params.customizations.imageUrl } : {}),
}
const deployResult = await performFullDeploy({
workflowId,
userId,
versionDescription: params.versionDescription,
versionName: params.versionName,
})
if (!deployResult.success) {
return { success: false, error: deployResult.error || 'Failed to deploy workflow' }
}
let encryptedPassword: string | null = null
if (authType === 'password' && password) {
const { encrypted } = await encryptSecret(password)
encryptedPassword = encrypted
}
const [existingDeployment] = await db
.select()
.from(chat)
.where(and(eq(chat.workflowId, workflowId), isNull(chat.archivedAt)))
.limit(1)
let chatId: string
if (existingDeployment) {
chatId = existingDeployment.id
let passwordToStore: string | null
if (authType === 'password') {
passwordToStore = encryptedPassword || existingDeployment.password
} else {
passwordToStore = null
}
await db
.update(chat)
.set({
identifier,
title,
description: description || null,
customizations,
authType,
password: passwordToStore,
allowedEmails: authType === 'email' || authType === 'sso' ? allowedEmails : [],
outputConfigs,
updatedAt: new Date(),
})
.where(eq(chat.id, chatId))
} else {
chatId = generateId()
await db.insert(chat).values({
id: chatId,
workflowId,
userId,
identifier,
title,
description: description || null,
customizations,
isActive: true,
authType,
password: encryptedPassword,
allowedEmails: authType === 'email' || authType === 'sso' ? allowedEmails : [],
outputConfigs,
createdAt: new Date(),
updatedAt: new Date(),
})
}
const baseUrl = getBaseUrl()
let chatUrl: string
try {
const url = new URL(baseUrl)
let host = url.host
if (host.startsWith('www.')) {
host = host.substring(4)
}
chatUrl = `${url.protocol}//${host}/chat/${identifier}`
} catch {
chatUrl = `${baseUrl}/chat/${identifier}`
}
logger.info(`Chat "${title}" deployed successfully at ${chatUrl}`)
try {
const { PlatformEvents } = await import('@/lib/core/telemetry')
PlatformEvents.chatDeployed({
chatId,
workflowId,
authType,
hasOutputConfigs: outputConfigs.length > 0,
})
} catch (_e) {
// Telemetry is best-effort
}
recordAudit({
workspaceId: params.workspaceId || null,
actorId: userId,
action: AuditAction.CHAT_DEPLOYED,
resourceType: AuditResourceType.CHAT,
resourceId: chatId,
resourceName: title,
description: `Deployed chat "${title}"`,
metadata: {
workflowId,
identifier,
authType,
chatUrl,
isUpdate: !!existingDeployment,
hasOutputConfigs: outputConfigs.length > 0,
hasCustomizations: !!(
params.customizations?.primaryColor ||
params.customizations?.welcomeMessage ||
params.customizations?.imageUrl
),
},
})
return {
success: true,
chatId,
chatUrl,
deployedAt: deployResult.deployedAt,
version: deployResult.version,
}
}
export interface PerformChatUndeployParams {
chatId: string
userId: string
workspaceId?: string | null
}
export interface PerformChatUndeployResult {
success: boolean
error?: string
}
/**
* Undeploys a chat: deletes the chat record and records an audit entry.
* Both the chat manage DELETE route and the copilot `deploy_chat` undeploy
* action must use this function.
*/
export async function performChatUndeploy(
params: PerformChatUndeployParams
): Promise<PerformChatUndeployResult> {
const { chatId, userId, workspaceId } = params
const [chatRecord] = await db
.select({
title: chat.title,
workflowId: chat.workflowId,
identifier: chat.identifier,
authType: chat.authType,
})
.from(chat)
.where(eq(chat.id, chatId))
.limit(1)
if (!chatRecord) {
return { success: false, error: 'Chat not found' }
}
await db.delete(chat).where(eq(chat.id, chatId))
logger.info(`Chat "${chatId}" deleted successfully`)
recordAudit({
workspaceId: workspaceId || null,
actorId: userId,
action: AuditAction.CHAT_DELETED,
resourceType: AuditResourceType.CHAT,
resourceId: chatId,
resourceName: chatRecord.title || chatId,
description: `Deleted chat deployment "${chatRecord.title || chatId}"`,
metadata: {
workflowId: chatRecord.workflowId || undefined,
identifier: chatRecord.identifier || undefined,
authType: chatRecord.authType || undefined,
},
})
return { success: true }
}