From 7f1ff7fd86cf37b693fe734cbff305c06b4f62e0 Mon Sep 17 00:00:00 2001
From: Vikhyath Mondreti
Date: Sat, 25 Oct 2025 09:59:57 -1000
Subject: [PATCH 1/2] fix(billing): should allow restoring subscription (#1728)
* fix(already-cancelled-sub): UI should allow restoring subscription
* restore functionality fixed
* fix
---
apps/sim/app/api/billing/portal/route.ts | 7 +-
.../cancel-subscription.tsx | 106 +++++++++---------
.../components/subscription/subscription.tsx | 1 +
apps/sim/lib/billing/core/billing.ts | 12 ++
apps/sim/stores/subscription/types.ts | 1 +
5 files changed, 70 insertions(+), 57 deletions(-)
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 (
-
-
-
-
-
-
- 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
}
From 363cba01e9baefb89640cbc2f2add03f5c73c072 Mon Sep 17 00:00:00 2001
From: Differ <1467673018@qq.com>
Date: Tue, 11 Nov 2025 11:27:25 +0800
Subject: [PATCH 2/2] docs: add Chinese (Simplified) translation for README
- Add language switcher buttons to README.md
- Create README.zh-CN.md with complete Chinese translation
- Include all sections: quickstart, setup guides, tech stack, contributing
- Enable bilingual documentation support for Chinese users
---
README.md | 4 +
README.zh-CN.md | 203 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 207 insertions(+)
create mode 100644 README.zh-CN.md
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 @@
+
+
+
+
+
+
+
+ English | 简体中文
+
+
+几分钟内构建和部署 AI 智能体工作流
+
+
+
+
+
+
+
+
+
+
+
+
+## 快速开始
+
+### 云托管版本:[sim.ai](https://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 团队用 ❤️ 制作