diff --git a/README.md b/README.md index 99ee735cb8..ea1bdbc0c7 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@

+

+ English | 简体中文 +

+

Build and deploy AI agent workflows in minutes.

diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000000..70b25ed5fd --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,203 @@ +

+ + Sim Logo + +

+ +

+ English | 简体中文 +

+ +

几分钟内构建和部署 AI 智能体工作流

+ +

+ Sim.ai + Discord + Twitter + Documentation +

+ +

+ Sim Demo +

+ +## 快速开始 + +### 云托管版本:[sim.ai](https://sim.ai) + +Sim.ai + +### 自托管:NPM 包 + +```bash +npx simstudio +``` +→ http://localhost:3000 + +#### 注意事项 +需要在您的机器上安装并运行 Docker。 + +#### 选项 + +| 标志 | 描述 | +|------|-------------| +| `-p, --port ` | 运行 Sim 的端口(默认 `3000`) | +| `--no-pull` | 跳过拉取最新 Docker 镜像 | + +### 自托管:Docker Compose + +```bash +# 克隆仓库 +git clone https://github.com/simstudioai/sim.git + +# 进入项目目录 +cd sim + +# 启动 Sim +docker compose -f docker-compose.prod.yml up -d +``` + +在 [http://localhost:3000/](http://localhost:3000/) 访问应用程序 + +#### 使用 Ollama 本地模型 + +使用 [Ollama](https://ollama.ai) 运行 Sim 的本地 AI 模型 - 无需外部 API: + +```bash +# 使用 GPU 支持启动(自动下载 gemma3:4b 模型) +docker compose -f docker-compose.ollama.yml --profile setup up -d + +# 仅使用 CPU 的系统: +docker compose -f docker-compose.ollama.yml --profile cpu --profile setup up -d +``` + +等待模型下载完成,然后访问 [http://localhost:3000](http://localhost:3000)。使用以下命令添加更多模型: +```bash +docker compose -f docker-compose.ollama.yml exec ollama ollama pull llama3.1:8b +``` + +### 自托管:开发容器 + +1. 使用 [Remote - Containers 扩展](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)打开 VS Code +2. 打开项目,在提示时点击"在容器中重新打开" +3. 在终端运行 `bun run dev:full` 或使用 `sim-start` 别名 + - 这将同时启动主应用程序和实时 socket 服务器 + +### 自托管:手动设置 + +**环境要求:** +- [Bun](https://bun.sh/) 运行时 +- PostgreSQL 12+ 并安装 [pgvector 扩展](https://github.com/pgvector/pgvector)(AI 嵌入功能必需) + +**注意:** Sim 使用向量嵌入实现知识库和语义搜索等 AI 功能,这需要 PostgreSQL 的 `pgvector` 扩展。 + +1. 克隆并安装依赖: + +```bash +git clone https://github.com/simstudioai/sim.git +cd sim +bun install +``` + +2. 设置带 pgvector 的 PostgreSQL: + +您需要安装带有 `vector` 扩展的 PostgreSQL 以支持嵌入功能。选择一个选项: + +**选项 A:使用 Docker(推荐)** +```bash +# 启动带 pgvector 扩展的 PostgreSQL +docker run --name simstudio-db \ + -e POSTGRES_PASSWORD=your_password \ + -e POSTGRES_DB=simstudio \ + -p 5432:5432 -d \ + pgvector/pgvector:pg17 +``` + +**选项 B:手动安装** +- 安装 PostgreSQL 12+ 和 pgvector 扩展 +- 参见 [pgvector 安装指南](https://github.com/pgvector/pgvector#installation) + +3. 设置环境变量: + +```bash +cd apps/sim +cp .env.example .env # 配置必需的变量(DATABASE_URL、BETTER_AUTH_SECRET、BETTER_AUTH_URL) +``` + +在 `.env` 文件中更新数据库 URL: +```bash +DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio" +``` + +4. 设置数据库: + +首先,配置数据库包的环境变量: +```bash +cd packages/db +cp .env.example .env +``` + +在 `packages/db/.env` 文件中更新数据库 URL: +```bash +DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio" +``` + +然后运行迁移: +```bash +bunx drizzle-kit migrate --config=./drizzle.config.ts +``` + +5. 启动开发服务器: + +**推荐方式 - 同时运行两个服务器(从项目根目录):** + +```bash +bun run dev:full +``` + +这将同时启动主 Next.js 应用程序和完整功能所需的实时 socket 服务器。 + +**替代方式 - 分别运行服务器:** + +Next.js 应用(从项目根目录): +```bash +bun run dev +``` + +实时 socket 服务器(从 `apps/sim` 目录在单独的终端): +```bash +cd apps/sim +bun run dev:sockets +``` + +## Copilot API 密钥 + +Copilot 是由 Sim 管理的服务。要在自托管实例上使用 Copilot: + +- 访问 https://sim.ai → 设置 → Copilot 并生成 Copilot API 密钥 +- 在您的自托管 apps/sim/.env 文件中将 `COPILOT_API_KEY` 环境变量设置为该值 + +## 技术栈 + +- **框架**:[Next.js](https://nextjs.org/)(App Router) +- **运行时**:[Bun](https://bun.sh/) +- **数据库**:PostgreSQL + [Drizzle ORM](https://orm.drizzle.team) +- **身份认证**:[Better Auth](https://better-auth.com) +- **UI**:[Shadcn](https://ui.shadcn.com/)、[Tailwind CSS](https://tailwindcss.com) +- **状态管理**:[Zustand](https://zustand-demo.pmnd.rs/) +- **流程编辑器**:[ReactFlow](https://reactflow.dev/) +- **文档**:[Fumadocs](https://fumadocs.vercel.app/) +- **单体仓库**:[Turborepo](https://turborepo.org/) +- **实时通信**:[Socket.io](https://socket.io/) +- **后台任务**:[Trigger.dev](https://trigger.dev/) +- **远程代码执行**:[E2B](https://www.e2b.dev/) + +## 贡献 + +欢迎贡献!请查看我们的[贡献指南](.github/CONTRIBUTING.md)了解详情。 + +## 许可证 + +本项目采用 Apache License 2.0 许可证 - 详见 [LICENSE](LICENSE) 文件。 + +

由 Sim 团队用 ❤️ 制作

diff --git a/apps/sim/app/api/billing/portal/route.ts b/apps/sim/app/api/billing/portal/route.ts index 017fbb8bd7..959a83cd7f 100644 --- a/apps/sim/app/api/billing/portal/route.ts +++ b/apps/sim/app/api/billing/portal/route.ts @@ -1,6 +1,6 @@ import { db } from '@sim/db' import { subscription as subscriptionTable, user } from '@sim/db/schema' -import { and, eq } from 'drizzle-orm' +import { and, eq, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { requireStripeClient } from '@/lib/billing/stripe-client' @@ -38,7 +38,10 @@ export async function POST(request: NextRequest) { .where( and( eq(subscriptionTable.referenceId, organizationId), - eq(subscriptionTable.status, 'active') + or( + eq(subscriptionTable.status, 'active'), + eq(subscriptionTable.cancelAtPeriodEnd, true) + ) ) ) .limit(1) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx index 1f5ea569aa..fd81cec55e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx @@ -12,7 +12,6 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { useSession, useSubscription } from '@/lib/auth-client' import { createLogger } from '@/lib/logs/console/logger' import { getBaseUrl } from '@/lib/urls/utils' @@ -30,6 +29,7 @@ interface CancelSubscriptionProps { } subscriptionData?: { periodEnd?: Date | null + cancelAtPeriodEnd?: boolean } } @@ -127,35 +127,48 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub const subscriptionStatus = getSubscriptionStatus() const activeOrgId = activeOrganization?.id - // For team/enterprise plans, get the subscription ID from organization store - if ((subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) && activeOrgId) { - const orgSubscription = useOrganizationStore.getState().subscriptionData + if (isCancelAtPeriodEnd) { + if (!betterAuthSubscription.restore) { + throw new Error('Subscription restore not available') + } + + let referenceId: string + let subscriptionId: string | undefined + + if ((subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) && activeOrgId) { + const orgSubscription = useOrganizationStore.getState().subscriptionData + referenceId = activeOrgId + subscriptionId = orgSubscription?.id + } else { + // For personal subscriptions, use user ID and let better-auth find the subscription + referenceId = session.user.id + subscriptionId = undefined + } + + logger.info('Restoring subscription', { referenceId, subscriptionId }) - if (orgSubscription?.id && orgSubscription?.cancelAtPeriodEnd) { - // Restore the organization subscription - if (!betterAuthSubscription.restore) { - throw new Error('Subscription restore not available') - } - - const result = await betterAuthSubscription.restore({ - referenceId: activeOrgId, - subscriptionId: orgSubscription.id, - }) - logger.info('Organization subscription restored successfully', result) + // Build restore params - only include subscriptionId if we have one (team/enterprise) + const restoreParams: any = { referenceId } + if (subscriptionId) { + restoreParams.subscriptionId = subscriptionId } + + const result = await betterAuthSubscription.restore(restoreParams) + + logger.info('Subscription restored successfully', result) } - // Refresh state and close await refresh() if (activeOrgId) { await loadOrganizationSubscription(activeOrgId) await refreshOrganization().catch(() => {}) } + setIsDialogOpen(false) } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to keep subscription' + const errorMessage = error instanceof Error ? error.message : 'Failed to restore subscription' setError(errorMessage) - logger.error('Failed to keep subscription', { error }) + logger.error('Failed to restore subscription', { error }) } finally { setIsLoading(false) } @@ -190,19 +203,15 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub const periodEndDate = getPeriodEndDate() // Check if subscription is set to cancel at period end - const isCancelAtPeriodEnd = (() => { - const subscriptionStatus = getSubscriptionStatus() - if (subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) { - return useOrganizationStore.getState().subscriptionData?.cancelAtPeriodEnd === true - } - return false - })() + const isCancelAtPeriodEnd = subscriptionData?.cancelAtPeriodEnd === true return ( <>
- Manage Subscription + + {isCancelAtPeriodEnd ? 'Restore Subscription' : 'Manage Subscription'} + {isCancelAtPeriodEnd && (

You'll keep access until {formatDate(periodEndDate)} @@ -217,10 +226,12 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub 'h-8 rounded-[8px] font-medium text-xs transition-all duration-200', error ? 'border-red-500 text-red-500 dark:border-red-500 dark:text-red-500' - : 'text-muted-foreground hover:border-red-500 hover:bg-red-500 hover:text-white dark:hover:border-red-500 dark:hover:bg-red-500' + : isCancelAtPeriodEnd + ? 'text-muted-foreground hover:border-green-500 hover:bg-green-500 hover:text-white dark:hover:border-green-500 dark:hover:bg-green-500' + : 'text-muted-foreground hover:border-red-500 hover:bg-red-500 hover:text-white dark:hover:border-red-500 dark:hover:bg-red-500' )} > - {error ? 'Error' : 'Manage'} + {error ? 'Error' : isCancelAtPeriodEnd ? 'Restore' : 'Manage'}

@@ -228,11 +239,11 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub - {isCancelAtPeriodEnd ? 'Manage' : 'Cancel'} {subscription.plan} subscription? + {isCancelAtPeriodEnd ? 'Restore' : 'Cancel'} {subscription.plan} subscription? {isCancelAtPeriodEnd - ? 'Your subscription is set to cancel at the end of the billing period. You can reactivate it or manage other settings.' + ? 'Your subscription is set to cancel at the end of the billing period. Would you like to keep your subscription active?' : `You'll be redirected to Stripe to manage your subscription. You'll keep access until ${formatDate( periodEndDate )}, then downgrade to free plan.`}{' '} @@ -260,38 +271,23 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub setIsDialogOpen(false) : handleKeep} disabled={isLoading} > - Keep Subscription + {isCancelAtPeriodEnd ? 'Cancel' : 'Keep Subscription'} {(() => { const subscriptionStatus = getSubscriptionStatus() - if ( - subscriptionStatus.isPaid && - (activeOrganization?.id - ? useOrganizationStore.getState().subscriptionData?.cancelAtPeriodEnd - : false) - ) { + if (subscriptionStatus.isPaid && isCancelAtPeriodEnd) { return ( - - - -
- - Continue - -
-
- -

Subscription will be cancelled at end of billing period

-
-
-
+ + {isLoading ? 'Restoring...' : 'Restore Subscription'} + ) } return ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx index 9ac78581ef..b69b499aff 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx @@ -523,6 +523,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) { }} subscriptionData={{ periodEnd: subscriptionData?.periodEnd || null, + cancelAtPeriodEnd: subscriptionData?.cancelAtPeriodEnd, }} />
diff --git a/apps/sim/lib/billing/core/billing.ts b/apps/sim/lib/billing/core/billing.ts index 48085eb821..55b6a207f7 100644 --- a/apps/sim/lib/billing/core/billing.ts +++ b/apps/sim/lib/billing/core/billing.ts @@ -220,6 +220,7 @@ export async function getSimplifiedBillingSummary( metadata: any stripeSubscriptionId: string | null periodEnd: Date | string | null + cancelAtPeriodEnd?: boolean // Usage details usage: { current: number @@ -318,6 +319,7 @@ export async function getSimplifiedBillingSummary( metadata: subscription.metadata || null, stripeSubscriptionId: subscription.stripeSubscriptionId || null, periodEnd: subscription.periodEnd || null, + cancelAtPeriodEnd: subscription.cancelAtPeriodEnd || undefined, // Usage details usage: { current: usageData.currentUsage, @@ -393,6 +395,7 @@ export async function getSimplifiedBillingSummary( metadata: subscription?.metadata || null, stripeSubscriptionId: subscription?.stripeSubscriptionId || null, periodEnd: subscription?.periodEnd || null, + cancelAtPeriodEnd: subscription?.cancelAtPeriodEnd || undefined, // Usage details usage: { current: currentUsage, @@ -450,5 +453,14 @@ function getDefaultBillingSummary(type: 'individual' | 'organization') { lastPeriodCost: 0, daysRemaining: 0, }, + ...(type === 'organization' && { + organizationData: { + seatCount: 0, + memberCount: 0, + totalBasePrice: 0, + totalCurrentUsage: 0, + totalOverage: 0, + }, + }), } } diff --git a/apps/sim/stores/subscription/types.ts b/apps/sim/stores/subscription/types.ts index c0de147d45..643694b795 100644 --- a/apps/sim/stores/subscription/types.ts +++ b/apps/sim/stores/subscription/types.ts @@ -29,6 +29,7 @@ export interface SubscriptionData { metadata: any | null stripeSubscriptionId: string | null periodEnd: Date | null + cancelAtPeriodEnd?: boolean usage: UsageData billingBlocked?: boolean }