Skip to content
Open

t #747

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"

# Authentication
BETTER_AUTH_SECRET="your-generated-secret-here"
BETTER_AUTH_URL="http://localhost:3000"
BETTER_AUTH_URL="http://localhost:5555"

# Redis
UPSTASH_REDIS_REST_URL="http://localhost:8079"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Before you begin, ensure you have the following installed on your system:

# Generate a secure secret for Better Auth
BETTER_AUTH_SECRET="your-generated-secret-here"
BETTER_AUTH_URL="http://localhost:3000"
BETTER_AUTH_URL="http://localhost:5555"

# Redis (matches docker-compose.yaml)
UPSTASH_REDIS_REST_URL="http://localhost:8079"
Expand Down Expand Up @@ -140,7 +140,7 @@ Before you begin, ensure you have the following installed on your system:
5. Run database migrations: `bun run db:migrate` from (inside apps/web)
6. Start the development server: `bun run dev` from (inside apps/web)

The application will be available at [http://localhost:3000](http://localhost:3000).
The application will be available at [http://localhost:5555](http://localhost:5555).

## Contributing

Expand Down
7 changes: 5 additions & 2 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"

# Better Auth
NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:5555
BETTER_AUTH_SECRET=your-secret-key-here

# Development Environment
Expand All @@ -30,4 +30,7 @@ R2_SECRET_ACCESS_KEY=your-secret-access-key
R2_BUCKET_NAME=opencut-transcription

# Modal transcription endpoint (from modal deploy transcription.py)
MODAL_TRANSCRIPTION_URL=https://your-username--opencut-transcription-transcribe-audio.modal.run
MODAL_TRANSCRIPTION_URL=https://your-username--opencut-transcription-transcribe-audio.modal.run

# ND Super Agent Integration
NEXT_PUBLIC_ND_SUPER_AGENT_API_URL=http://localhost:3001/api/base
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add the trailing newline.

dotenv-linter will keep flagging this example file until it ends with a trailing newline.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 36-36: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/.env.example` at line 36, The .env example is missing a trailing
newline which causes dotenv-linter to fail; open the apps web .env example and
add a single newline character after the LAST line containing
NEXT_PUBLIC_ND_SUPER_AGENT_API_URL (the env variable line) so the file ends with
a trailing newline; save the file and commit the change.

2 changes: 1 addition & 1 deletion apps/web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ ENV DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
ENV BETTER_AUTH_SECRET="build-time-secret"
ENV UPSTASH_REDIS_REST_URL="http://localhost:8079"
ENV UPSTASH_REDIS_REST_TOKEN="example_token"
ENV NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000"
ENV NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:5555"

ENV FREESOUND_CLIENT_ID=$FREESOUND_CLIENT_ID
ENV FREESOUND_API_KEY=$FREESOUND_API_KEY
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"packageManager": "bun@1.2.18",
"scripts": {
"dev": "next dev --turbopack",
"dev": "next dev --turbopack --port 5555",
"build": "next build",
"start": "next start",
"lint": "biome check src/",
Expand Down
131 changes: 118 additions & 13 deletions apps/web/src/app/editor/[project_id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
"use client";

import { useEffect, useRef } from "react";
import { useParams, useRouter } from "next/navigation";
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable";
import { EditorHeader } from "@/components/editor/editor-header";
import { MediaPanel } from "@/components/editor/media-panel";
import { Onboarding } from "@/components/editor/onboarding";
import { PreviewPanel } from "@/components/editor/preview-panel";
import { PropertiesPanel } from "@/components/editor/properties-panel";
import { Timeline } from "@/components/editor/timeline";
import { PreviewPanel } from "@/components/editor/preview-panel";
import { EditorHeader } from "@/components/editor/editor-header";
import { usePanelStore } from "@/stores/panel-store";
import { useProjectStore } from "@/stores/project-store";
import { EditorProvider } from "@/components/providers/editor-provider";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { usePlaybackControls } from "@/hooks/use-playback-controls";
import { Onboarding } from "@/components/editor/onboarding";
import { usePanelStore } from "@/stores/panel-store";
import { useProjectStore } from "@/stores/project-store";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useRef } from "react";

export default function Editor() {
function EditorContent() {
const {
toolsPanel,
previewPanel,
Expand All @@ -43,12 +43,100 @@ export default function Editor() {
} = useProjectStore();
const params = useParams();
const router = useRouter();
const searchParams = useSearchParams();
const projectId = params.project_id as string;
const handledProjectIds = useRef<Set<string>>(new Set());
const isInitializingRef = useRef<boolean>(false);

usePlaybackControls();

// 处理从 nd-super-agent 传递过来的参数
useEffect(() => {
const handleExternalParams = async () => {
console.log('🔍 开始检查外部参数...');

// 获取URL参数
const token = searchParams.get('token');
const userId = searchParams.get('user_id');
const tenantId = searchParams.get('tenant_id');
const materialIds = searchParams.get('material_ids');

// 从环境变量获取 API 基础 URL
const apiBaseUrl = process.env.NEXT_PUBLIC_ND_SUPER_AGENT_API_URL;

console.log('📋 外部参数检查:', {
hasToken: !!token,
hasUserId: !!userId,
hasTenantId: !!tenantId,
hasMaterialIds: !!materialIds,
hasApiBaseUrl: !!apiBaseUrl,
materialIds
});


// 如果有外部参数,处理素材加载
if (token && materialIds && apiBaseUrl) {
console.log('检测到外部参数,开始加载素材:', {
token: token.substring(0, 10) + '...',
userId,
tenantId,
materialIds,
apiBaseUrl: apiBaseUrl?.substring(0, 30) + '...'
});

try {
// 解析素材ID列表
const ids = materialIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));

if (ids.length > 0) {
console.log('🔗 调用素材接口:', { ids, apiUrl: `${apiBaseUrl}/gallery/batch-query` });

// 调用 nd-super-agent 的素材接口
const response = await fetch(`${apiBaseUrl}/gallery/batch-query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
...(tenantId && { 'X-Tenant-ID': tenantId }),
...(userId && { 'X-User-ID': userId })
},
body: JSON.stringify({ ids })
Comment on lines +59 to +103
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Keep auth context out of client-side query params and logs.

This effect reads token, user_id, and tenant_id from the URL, logs parts of that context, and then forwards it as request headers from the browser. That leaves credentials and tenant metadata in copied URLs, browser history, and any request logs that capture those URLs, and the new query-preservation redirects elsewhere in this PR keep replaying them through more routes. Move this exchange behind a server route or first-party session, then strip the sensitive params before rendering the editor.

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Don't use console.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/editor/`[project_id]/page.tsx around lines 59 - 103, The
page currently reads sensitive values (token, user_id, tenant_id) from
searchParams and logs/forwards them in the client-side fetch (see token, userId,
tenantId, materialIds, apiBaseUrl and the fetch to
`${apiBaseUrl}/gallery/batch-query` in page.tsx); move the exchange to a
server-side API route or use a first-party session: accept material_ids on the
client, call your new server endpoint which reads auth from secure server-side
storage (not from query), performs the POST to /gallery/batch-query, and returns
only non-sensitive results to the client; strip/remove the sensitive query
params before rendering the editor (do not expose token/user/tenant in URLs or
history) and remove all console.* usages per linting rules.

});

if (response.ok) {
const result = await response.json();
console.log('✅ 素材加载成功:', {
找到数量: result.data?.found_count || 0,
请求数量: result.data?.requested_count || 0
});

if (result.success && result.data && result.data.items) {
console.log('📋 获取到素材:', result.data.items.map(item => ({
id: item.id,
name: item.name || item.title,
type: item.type
})));

// TODO: 将素材添加到 OpenCut 的媒体存储中
// 可能需要调用 useMediaStore 或相关的存储管理
}
Comment on lines +106 to +122
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The success path still drops the fetched assets.

After batch-query succeeds, the code only logs result.data.items and stops at a TODO, so none of the new env/query/API plumbing produces an editor-visible result. Please hand these items to the existing media/editor store before merging. If helpful, I can sketch that integration in a follow-up issue.

Based on learnings, utilize Zustand stores from apps/web/src/stores for state management.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/editor/`[project_id]/page.tsx around lines 106 - 122, The
fetched assets are only logged and not added to the app state; import and use
the existing Zustand media/editor store (e.g., useMediaStore or the editor/media
store in apps/web/src/stores) and, inside the success branch where
result.success && result.data && result.data.items, transform result.data.items
into the media store's expected shape (map id, name/title, type, url/preview
fields) and call the store's mutation (e.g., addMedia, addItems, addAssets) to
persist them so they appear in the editor; ensure this runs on the client-side
context where useMediaStore is usable and keep logging for success/failure
around the store update.

} else {
console.error('❌ 素材加载失败:', response.status, response.statusText);
}
} else {
console.warn('⚠️ 没有有效的素材ID');
}
} catch (error) {
console.error('💥 素材加载失败:', error.message);
}
}
};

// 延迟执行,确保组件已经初始化
const timer = setTimeout(handleExternalParams, 1000);
return () => clearTimeout(timer);
}, [searchParams]);

useEffect(() => {
let isCancelled = false;

Expand Down Expand Up @@ -457,3 +545,20 @@ export default function Editor() {
</EditorProvider>
);
}

export default function Editor() {
return (
<Suspense
fallback={
<div className="h-screen w-screen flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-4">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
<p className="text-sm text-muted-foreground">正在加载编辑器...</p>
</div>
</div>
}
>
<EditorContent />
</Suspense>
);
}
66 changes: 66 additions & 0 deletions apps/web/src/app/editor/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client";

import { useProjectStore } from "@/stores/project-store";
import { Loader2 } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect } from "react";

function EditorIndexContent() {
const { createNewProject } = useProjectStore();
const router = useRouter();
const searchParams = useSearchParams();

useEffect(() => {
const createAndRedirect = async () => {
try {
// 创建新项目
const projectId = await createNewProject("新项目");

// 保留查询参数
const queryString = searchParams.toString();
const redirectUrl = queryString
? `/editor/${projectId}?${queryString}`
: `/editor/${projectId}`;

// 重定向到编辑器页面
router.replace(redirectUrl);
} catch (error) {
console.error("创建项目失败:", error);
// 如果创建失败,重定向到项目列表页
const queryString = searchParams.toString();
const fallbackUrl = queryString
? `/projects?${queryString}`
: "/projects";
router.replace(fallbackUrl);
}
};

createAndRedirect();
}, [createNewProject, router, searchParams]);
Comment on lines +6 to +39
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Locate the file and verify content
fd "page.tsx" apps/web/src/app/editor/ --type f

Repository: OpenCut-app/OpenCut

Length of output: 141


🏁 Script executed:

# Search for createNewProject implementation
rg "createNewProject" --type ts --type tsx -A 5 -B 2

Repository: OpenCut-app/OpenCut

Length of output: 90


🏁 Script executed:

# Find the project store file to understand createNewProject
fd "project.*store" --type ts --type tsx

Repository: OpenCut-app/OpenCut

Length of output: 232


🏁 Script executed:

# Check for useRouter and useSearchParams imports in the file
rg "useRouter|useSearchParams" apps/web/src/app/editor/ --type tsx -B 2 -A 2

Repository: OpenCut-app/OpenCut

Length of output: 90


🏁 Script executed:

# Read the page.tsx file
cat -n apps/web/src/app/editor/page.tsx

Repository: OpenCut-app/OpenCut

Length of output: 2494


🏁 Script executed:

# Search for createNewProject across all files
rg "createNewProject" -A 5 -B 2

Repository: OpenCut-app/OpenCut

Length of output: 5381


🏁 Script executed:

# Find project store
fd "project" -type f -name "*store*"

Repository: OpenCut-app/OpenCut

Length of output: 233


🏁 Script executed:

# Read the project store file to see full createNewProject implementation
cat -n apps/web/src/stores/project-store.ts | head -200

Repository: OpenCut-app/OpenCut

Length of output: 7468


🏁 Script executed:

# See more of the project-store
wc -l apps/web/src/stores/project-store.ts

Repository: OpenCut-app/OpenCut

Length of output: 103


🏁 Script executed:

# Read lines 200-230 to see the rest of createNewProject
sed -n '200,230p' apps/web/src/stores/project-store.ts

Repository: OpenCut-app/OpenCut

Length of output: 950


🏁 Script executed:

# Also check how other files use createNewProject with guards
rg -B 10 -A 10 "createAndRedirect|hasStarted"

Repository: OpenCut-app/OpenCut

Length of output: 2792


🏁 Script executed:

# Verify if there are any other guards or mechanisms in related code
rg "useRef.*false|once.*true|completed" apps/web/src/app/editor/ -A 3

Repository: OpenCut-app/OpenCut

Length of output: 325


🏁 Script executed:

# See how isInitializingRef is used in the [project_id]/page.tsx
rg -B 5 -A 15 "isInitializingRef" apps/web/src/app/editor/

Repository: OpenCut-app/OpenCut

Length of output: 7788


Guard the mount effect to prevent duplicate project creation.

This route executes a non-idempotent write (createNewProject) on mount without any guard, causing React 18 strict mode or component remounts to create and persist multiple duplicate projects before the redirect completes. The issue is reproducible on first visit to /editor in development mode.

Fix using ref-based guard
-import { Suspense, useEffect } from "react";
+import { Suspense, useEffect, useRef } from "react";

 function EditorIndexContent() {
   const { createNewProject } = useProjectStore();
   const router = useRouter();
   const searchParams = useSearchParams();
+  const hasStartedCreationRef = useRef(false);

   useEffect(() => {
+    if (hasStartedCreationRef.current) {
+      return;
+    }
+    hasStartedCreationRef.current = true;
+
     const createAndRedirect = async () => {
       try {
         // 创建新项目
         const projectId = await createNewProject("新项目");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/editor/page.tsx` around lines 6 - 39, The effect in
EditorIndexContent calls the non-idempotent createNewProject on every mount,
which can run twice in React 18 Strict Mode; protect it with a ref-based guard:
create a local ref (e.g., hasStartedRef) checked at the start of the useEffect
(or inside createAndRedirect) to return early if true, set hasStartedRef.current
= true immediately before invoking createNewProject, and leave the rest of the
logic (building queryString, router.replace on success/failure) unchanged;
reference the functions/vars createNewProject, createAndRedirect, router, and
searchParams so you only add the guard and not alter the redirect flow.


return (
<div className="h-screen w-screen flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-4">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">正在创建新项目...</p>
</div>
</div>
);
}

export default function EditorIndex() {
return (
<Suspense
fallback={
<div className="h-screen w-screen flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-4">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">正在加载...</p>
</div>
</div>
}
>
<EditorIndexContent />
</Suspense>
);
}
23 changes: 23 additions & 0 deletions apps/web/src/app/landing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Footer } from "@/components/footer";
import { Header } from "@/components/header";
import { Hero } from "@/components/landing/hero";
import { SITE_URL } from "@/constants/site";
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "OpenCut - 免费开源视频编辑器",
description: "一个免费、开源的网页、桌面和移动端视频编辑器。保护隐私,功能完整,无水印。",
alternates: {
canonical: `${SITE_URL}/landing`,
},
};
Comment on lines +7 to +13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded Chinese metadata may be inconsistent with the rest of the application.

The title and description are in Chinese while other parts of the application appear to use English. If this is intentional for a specific locale, consider using i18n/l10n patterns for consistency. Otherwise, update to English to match the rest of the site.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/landing/page.tsx` around lines 7 - 13, The page exports a
hardcoded Chinese metadata object (export const metadata: Metadata) which is
inconsistent with the app’s English content; update the title and description to
English or wire them to your localization system (i18n) instead of hardcoding
Chinese—either replace the strings in metadata (title/description) with their
English equivalents or read localized values (e.g., from a locale-aware config
or translation function) and keep the alternates canonical using
SITE_URL/landing so metadata remains consistent with the rest of the site.


export default async function LandingPage() {
return (
<div>
<Header />
<Hero />
<Footer />
</div>
);
}
45 changes: 27 additions & 18 deletions apps/web/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import { Hero } from "@/components/landing/hero";
import { Header } from "@/components/header";
import { Footer } from "@/components/footer";
import type { Metadata } from "next";
import { SITE_URL } from "@/constants/site";
import { redirect } from "next/navigation";

export const metadata: Metadata = {
alternates: {
canonical: SITE_URL,
},
};
interface HomeProps {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function Home() {
return (
<div>
<Header />
<Hero />
<Footer />
</div>
);
export default async function Home({ searchParams }: HomeProps) {
// 在 Next.js 15 中需要 await searchParams
const params = await searchParams;

// 构建查询字符串
const queryString = new URLSearchParams();

for (const [key, value] of Object.entries(params)) {
if (value !== undefined) {
if (Array.isArray(value)) {
for (const v of value) {
queryString.append(key, v);
}
} else {
queryString.set(key, value);
}
}
}

const queryStr = queryString.toString();
const redirectUrl = queryStr ? `/editor?${queryStr}` : "/editor";

redirect(redirectUrl);
}
Loading