forked from 77media/video-flow
更新 API 常量以使用环境变量,移除不必要的日志输出,重构剧本服务以支持新的项目创建和剧本保存逻辑,增加故事细节的提取和更新功能。
This commit is contained in:
parent
96b8066f5c
commit
073bc6db13
@ -1,2 +1,2 @@
|
||||
export const BASE_URL = "https://77.smartvideo.py.qikongjian.com"
|
||||
// process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
export const BASE_URL = process.env.NEXT_PUBLIC_SMART_API
|
||||
//
|
||||
|
||||
@ -28,8 +28,6 @@ request.interceptors.request.use(
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 直接返回响应数据
|
||||
console.log('?????????????????????????',Object.keys(response.data));
|
||||
|
||||
return response.data;
|
||||
},
|
||||
(error) => {
|
||||
|
||||
@ -599,10 +599,12 @@ export const generateScriptStream = (
|
||||
|
||||
case 'completed':
|
||||
console.log('生成完成:', data.message);
|
||||
resolve()
|
||||
return;
|
||||
|
||||
case 'error':
|
||||
console.error('生成失败:', data.message);
|
||||
reject(data.message)
|
||||
return;
|
||||
}
|
||||
})
|
||||
@ -616,11 +618,11 @@ export const generateScriptStream = (
|
||||
*/
|
||||
export const applyScriptToShot = async (request: {
|
||||
/** 项目ID */
|
||||
projectId: string;
|
||||
/** 剧本*/
|
||||
scriptText: string;
|
||||
}): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>("/movie/apply_script_to_shot", request);
|
||||
project_id: string;
|
||||
/** 计划Id*/
|
||||
plan_id: string;
|
||||
})=> {
|
||||
return post<ApiResponse<any>>("/movie/create_movie_project_plan_v1", request);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -629,19 +631,14 @@ export const applyScriptToShot = async (request: {
|
||||
*/
|
||||
export const getProjectScript = async (request: {
|
||||
/** 项目ID */
|
||||
projectId: string;
|
||||
}): Promise<
|
||||
ApiResponse<{
|
||||
/** 用户提示词 */
|
||||
prompt: string;
|
||||
/** 生成的剧本文本 */
|
||||
scriptText: string;
|
||||
}>
|
||||
> => {
|
||||
project_id: string;
|
||||
})=> {
|
||||
return post<
|
||||
ApiResponse<{
|
||||
prompt: string;
|
||||
scriptText: string;
|
||||
/** 项目id */
|
||||
project_id: string;
|
||||
/** 生成的剧本文本 */
|
||||
generated_script: string;
|
||||
}>
|
||||
>("/movie/get_project_script", request);
|
||||
};
|
||||
@ -653,32 +650,54 @@ export const getProjectScript = async (request: {
|
||||
*/
|
||||
export const saveScript = async (request: {
|
||||
/** 项目ID */
|
||||
projectId: string;
|
||||
project_id: string;
|
||||
/** 剧本文本 */
|
||||
scriptText: string;
|
||||
generated_script: string;
|
||||
/** 剧情梗概 */
|
||||
synopsis: string;
|
||||
/** 剧情类型 */
|
||||
categories: string[];
|
||||
/** 主角 */
|
||||
protagonist: string;
|
||||
/** 框架 */
|
||||
framework: string;
|
||||
}): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>("/movie/save_script", request);
|
||||
return post<ApiResponse<any>>("/movie/update_generated_script", request);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 创建项目
|
||||
* @param request 创建项目请求参数
|
||||
* 创建电影项目V1版本
|
||||
* @param request 创建电影项目请求参数
|
||||
* @returns Promise<ApiResponse<{ projectId: string }>>
|
||||
*/
|
||||
export const createProject = async (request: {
|
||||
/** 用户提示词 */
|
||||
userPrompt: string;
|
||||
/** 剧本内容 */
|
||||
scriptContent: string;
|
||||
}): Promise<
|
||||
ApiResponse<{
|
||||
/** 项目ID */
|
||||
projectId: string;
|
||||
}>
|
||||
> => {
|
||||
return post<
|
||||
ApiResponse<{
|
||||
projectId: string;
|
||||
}>
|
||||
>("/movie/create_project", request);
|
||||
export const abortVideoTask = async (request: {
|
||||
/** 项目ID */
|
||||
project_id: string;
|
||||
/** 计划ID */
|
||||
plan_id: string;
|
||||
}): Promise<ApiResponse<any>> => {
|
||||
return post("/api/v1/video/abort", request);
|
||||
};
|
||||
|
||||
export const createMovieProjectV1 = async (request: {
|
||||
/** 剧本内容 */
|
||||
script: string;
|
||||
/** 用户ID */
|
||||
user_id: string;
|
||||
/** 模式:auto | manual */
|
||||
mode: "auto" | "manual";
|
||||
/** 分辨率:720p | 1080p | 4k */
|
||||
resolution: "720p" | "1080p" | "4k";
|
||||
}) => {
|
||||
return post<ApiResponse<{
|
||||
/** 项目ID */
|
||||
project_id: string;
|
||||
/** 计划ID */
|
||||
plan_id: string;
|
||||
/** 项目名称 */
|
||||
name: string;
|
||||
/** 项目状态 */
|
||||
status: string;
|
||||
}>>("/movie/create_movie_project_v1", request);
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { SceneEntity, TagEntity, AITextEntity, VideoSegmentEntity } from '../domain/Entities';
|
||||
import { SceneItem, TagItem, TextItem, ShotItem } from '../domain/Item';
|
||||
import { SceneItem, TagItem, TextItem } from '../domain/Item';
|
||||
import { SceneEditUseCase } from '../usecase/SceneEditUseCase';
|
||||
import { TagEditUseCase } from '../usecase/TagEditUseCase';
|
||||
import { TextEditUseCase } from '../usecase/TextEditUseCase';
|
||||
|
||||
@ -106,6 +106,230 @@ export class LensType {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 这是一个符合DDD概念的值对象,代表故事的核心细节。
|
||||
* 它封装了故事的四个关键元素:梗概、分类、主角和框架内容。
|
||||
* - 值对象是不可变的,一旦创建,其内部属性不会改变。
|
||||
* - 构造函数是私有的,强制使用工厂方法创建实例,以确保数据处理的封装性。
|
||||
*/
|
||||
export class StoryDetails {
|
||||
/** 故事梗概 */
|
||||
public synopsis: string = "";
|
||||
|
||||
/** 故事分类标签 */
|
||||
public categories: string[] = [];
|
||||
|
||||
/** 主角名称 */
|
||||
public protagonist: string = "";
|
||||
|
||||
/** 激励事件 */
|
||||
public incitingIncident: string = "";
|
||||
|
||||
/** 问题与新目标 */
|
||||
public problem: string = "";
|
||||
|
||||
/** 冲突与障碍 */
|
||||
public conflict: string = "";
|
||||
|
||||
/** 赌注 */
|
||||
public stakes: string = "";
|
||||
|
||||
/** 人物弧线完成 */
|
||||
public characterArc: string = "";
|
||||
|
||||
/**
|
||||
* 更新剧本
|
||||
* @param text 剧本文本
|
||||
*/
|
||||
updateScript(text: string) {
|
||||
const scriptObject = this.createFromText(text);
|
||||
this.synopsis = scriptObject.synopsis;
|
||||
this.categories = scriptObject.categories;
|
||||
this.protagonist = scriptObject.protagonist;
|
||||
this.incitingIncident = scriptObject.incitingIncident;
|
||||
this.problem = scriptObject.problem;
|
||||
this.conflict = scriptObject.conflict;
|
||||
this.stakes = scriptObject.stakes;
|
||||
this.characterArc = scriptObject.characterArc;
|
||||
return scriptObject;
|
||||
}
|
||||
mergeframework():string{
|
||||
return `
|
||||
${this.incitingIncident}\n
|
||||
${this.problem}
|
||||
${this.conflict}
|
||||
${this.stakes}
|
||||
${this.characterArc}
|
||||
`
|
||||
}
|
||||
constructor(text: string) {
|
||||
this.updateScript(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工厂方法:从原始英文文本中提取并创建 StoryDetails 值对象。
|
||||
* 该方法负责所有的解析逻辑,包括智能提取梗概和去除格式标签。
|
||||
* @param text 包含故事所有细节的原始Markdown或富文本。
|
||||
*/
|
||||
public createFromText(text: string) {
|
||||
// --- 智能解析梗概 ---
|
||||
// 梗概通常位于“Core Elements”部分,描述主角和初始事件。
|
||||
// 我们提取“Protagonist”和“The Inciting Incident”的描述,并组合起来。
|
||||
const protagonistText = this.extractContentByHeader(text, "Core Identity:");
|
||||
const incitingIncidentText = this.extractContentByHeader(
|
||||
text,
|
||||
"The Inciting Incident"
|
||||
);
|
||||
// --- 其他字段的提取保持不变,使用标题进行查找 ---
|
||||
const categories = this.extractContentByHeader(text, "GENRE")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
const protagonist = this.extractContentByHeader(text, "Core Identity:");
|
||||
|
||||
const problem = this.extractContentByHeader(text, "The Problem & New Goal");
|
||||
const conflict = this.extractContentByHeader(text, "Conflict & Obstacles");
|
||||
const stakes = this.extractContentByHeader(text, "The Stakes");
|
||||
const characterArc = this.extractContentByHeader(
|
||||
text,
|
||||
"Character Arc Accomplished"
|
||||
);
|
||||
|
||||
// 1. 梗概 - 从主角描述和激励事件组合生成
|
||||
const synopsis = `${protagonistText} ${incitingIncidentText}`.trim();
|
||||
|
||||
return {
|
||||
synopsis,
|
||||
categories,
|
||||
protagonist,
|
||||
incitingIncident: incitingIncidentText,
|
||||
problem,
|
||||
conflict,
|
||||
stakes,
|
||||
characterArc
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 辅助方法:根据标题提取其下的内容,并去除所有Markdown标签。
|
||||
* @param fullText 完整的源文本。
|
||||
* @param headerName 要提取内容所属的标题名(不区分大小写)。
|
||||
* @returns 提取出的纯文本内容。
|
||||
*/
|
||||
public extractContentByHeader(fullText: string, headerName: string): string {
|
||||
try {
|
||||
console.log(`正在查找标题: "${headerName}"`);
|
||||
|
||||
// 转义正则表达式中的特殊字符
|
||||
const escapeRegex = (text: string): string => {
|
||||
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
};
|
||||
|
||||
// 清理Markdown格式和多余的空白字符
|
||||
const cleanMarkdownContent = (content: string): string => {
|
||||
return content
|
||||
// 移除markdown的粗体标记
|
||||
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
||||
// 移除markdown的斜体标记
|
||||
.replace(/\*([^*]+)\*/g, '$1')
|
||||
// 移除列表标记(- 或 * 开头的行)
|
||||
.replace(/^\s*[-*]\s+/gm, '')
|
||||
// 移除代码块
|
||||
.replace(/```[\s\S]*?```/g, '')
|
||||
// 移除行内代码
|
||||
.replace(/`([^`]+)`/g, '$1')
|
||||
// 规范化空白字符:将多个连续空白(包括换行)替换为单个空格
|
||||
.replace(/\s+/g, ' ')
|
||||
// 移除引号
|
||||
.replace(/["""'']/g, '')
|
||||
// 最终清理首尾空白
|
||||
.trim();
|
||||
};
|
||||
|
||||
// 标准化headerName,移除末尾的冒号和空格
|
||||
const normalizedHeaderName = headerName.replace(/:?\s*$/, '').trim();
|
||||
const escapedHeaderName = escapeRegex(normalizedHeaderName);
|
||||
|
||||
let content = "";
|
||||
|
||||
// 定义多种匹配模式,按优先级排序
|
||||
const patterns = [
|
||||
// 1. 匹配带编号的主标题:数字. **标题:** 格式
|
||||
new RegExp(`\\d+\\.\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\*\\*[A-Z]+:\\*\\*|---|\$)`, "i"),
|
||||
|
||||
// 2. 匹配子标题:*标题:* 格式(在主标题下的子项)
|
||||
new RegExp(`\\*\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\*\\s*\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\*\\*[A-Z]+:\\*\\*|---|\$)`, "i"),
|
||||
|
||||
// 3. 匹配独立的粗体标题:**标题:** 格式
|
||||
new RegExp(`^\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=^\\s*\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|^\\s*\\*\\*[A-Z]+:\\*\\*|^---|\$)`, "im"),
|
||||
|
||||
// 4. 匹配简单的**标题:**格式(可能在段落中)
|
||||
new RegExp(`\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\n\\n|---|\$)`, "i"),
|
||||
|
||||
// 5. 匹配markdown标题:## 标题 格式
|
||||
new RegExp(`^#{1,6}\\s*${escapedHeaderName}:?\\s*\\n([\\s\\S]*?)(?=^#{1,6}\\s|^\\*\\*[^*]+:?\\*\\*|^---|\$)`, "im"),
|
||||
|
||||
// 6. 匹配冒号后的内容:标题: 内容(适用于简单的键值对格式)
|
||||
new RegExp(`^\\s*${escapedHeaderName}:?\\s*([\\s\\S]*?)(?=^\\s*[A-Za-z][^:]*:|^\\*\\*[^*]+:?\\*\\*|^---|\$)`, "im"),
|
||||
];
|
||||
|
||||
// 尝试每种模式
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
const regex = patterns[i];
|
||||
const match = fullText.match(regex);
|
||||
if (match && match[1] && match[1].trim()) {
|
||||
content = match[1].trim();
|
||||
console.log(`使用模式 ${i + 1} 匹配成功`);
|
||||
console.log(`匹配的完整内容: "${match[0].substring(0, 150)}..."`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
console.log(`没有找到标题 "${headerName}" 对应的内容`);
|
||||
// 尝试更宽松的匹配作为最后手段
|
||||
const loosePattern = new RegExp(`${escapedHeaderName}[:\\s]*([\\s\\S]{1,500}?)(?=\\n\\n|\\*\\*[^*]+\\*\\*|#{1,6}\\s|---|\$)`, "i");
|
||||
const looseMatch = fullText.match(loosePattern);
|
||||
if (looseMatch && looseMatch[1]) {
|
||||
content = looseMatch[1].trim();
|
||||
console.log("使用宽松匹配模式找到内容");
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`原始匹配内容: "${content.substring(0, 100)}..."`);
|
||||
|
||||
// 清理内容格式
|
||||
content = cleanMarkdownContent(content);
|
||||
|
||||
// 如果内容太短,可能是匹配错误,返回空
|
||||
if (content.length < 10) {
|
||||
console.log("匹配到的内容太短,可能匹配错误");
|
||||
return "";
|
||||
}
|
||||
|
||||
console.log(`清理后的内容: "${content.substring(0, 100)}..."`);
|
||||
return content;
|
||||
|
||||
} catch (error) {
|
||||
console.error("内容提取出错:", error);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
toObject() {
|
||||
return {
|
||||
synopsis: this.synopsis,
|
||||
categories: this.categories,
|
||||
protagonist: this.protagonist,
|
||||
incitingIncident: this.incitingIncident,
|
||||
problem: this.problem,
|
||||
conflict: this.conflict,
|
||||
stakes: this.stakes,
|
||||
characterArc: this.characterArc,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 剧本聚合根
|
||||
* @description 作为聚合根,封装ScriptSlice的管理与行为
|
||||
@ -120,12 +344,14 @@ export class ScriptValueObject {
|
||||
get scriptSlices(): readonly ScriptSlice[] {
|
||||
return [...this._scriptSlices];
|
||||
}
|
||||
|
||||
scriptText: string = "";
|
||||
storyDetails: StoryDetails;
|
||||
/**
|
||||
* @description: 构造函数,初始化剧本
|
||||
* @param scriptText 剧本文本字符串
|
||||
*/
|
||||
constructor(scriptText?: string) {
|
||||
this.storyDetails = new StoryDetails("");
|
||||
if (scriptText) {
|
||||
this.parseFromString(scriptText);
|
||||
}
|
||||
@ -135,30 +361,15 @@ export class ScriptValueObject {
|
||||
* @description: 从字符串解析剧本片段
|
||||
* @param scriptText 剧本文本字符串
|
||||
*/
|
||||
parseFromString(scriptText: string): void {
|
||||
console.log('scriptText', scriptText)
|
||||
parseFromString(scriptText: string) {
|
||||
this.scriptText += scriptText;
|
||||
return this.storyDetails.updateScript(this.scriptText);
|
||||
}
|
||||
/**
|
||||
* @description: 将剧本片段转换为字符串
|
||||
* @returns string
|
||||
*/
|
||||
toString(): string {
|
||||
return this._scriptSlices.map((slice) => slice.text).join("");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 聚合根相等性比较
|
||||
* @param other 另一个ScriptValueObject实例
|
||||
* @returns 是否相等
|
||||
*/
|
||||
equals(other: ScriptValueObject): boolean {
|
||||
if (this._scriptSlices.length !== other._scriptSlices.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._scriptSlices.every((slice, index) =>
|
||||
slice.equals(other._scriptSlices[index])
|
||||
);
|
||||
return this.scriptText;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
app/service/test/testScript.txt
Normal file
1
app/service/test/testScript.txt
Normal file
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
import { ScriptSlice, ScriptValueObject } from "../domain/valueObject";
|
||||
import { generateScriptStream, applyScriptToShot } from "@/api/video_flow";
|
||||
import { generateScriptStream, applyScriptToShot, createMovieProjectV1, saveScript } from "@/api/video_flow";
|
||||
|
||||
export class ScriptEditUseCase {
|
||||
loading: boolean = false;
|
||||
@ -30,6 +30,8 @@ export class ScriptEditUseCase {
|
||||
stream_callback?.(content)
|
||||
this.scriptValueObject.parseFromString(content)
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
if (this.abortController?.signal.aborted) {
|
||||
console.log("剧本生成被中断");
|
||||
@ -53,18 +55,84 @@ export class ScriptEditUseCase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 创建项目
|
||||
* @param prompt 用户提示词
|
||||
* @param userId 用户ID
|
||||
* @param mode 模式:auto | manual
|
||||
* @param resolution 分辨率:720p | 1080p | 4k
|
||||
* @returns Promise<string> 返回项目ID
|
||||
*/
|
||||
async createProject(
|
||||
prompt: string,
|
||||
userId: string = "user123",
|
||||
mode: "auto" | "manual" = "auto",
|
||||
resolution: "720p" | "1080p" | "4k" = "720p"
|
||||
) {
|
||||
try {
|
||||
// 调用创建项目API
|
||||
const response = await createMovieProjectV1({
|
||||
script: prompt,
|
||||
user_id: userId,
|
||||
mode,
|
||||
resolution
|
||||
});
|
||||
|
||||
if (!response.successful) {
|
||||
throw new Error(response.message || "创建项目失败");
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("创建项目失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 保存剧本
|
||||
* @param projectId 项目ID
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async saveScript(projectId: string): Promise<void> {
|
||||
try {
|
||||
this.loading = true;
|
||||
|
||||
// 获取当前剧本文本
|
||||
const scriptText = this.scriptValueObject.toString();
|
||||
|
||||
// 调用保存剧本API
|
||||
const response = await saveScript({
|
||||
project_id: projectId,
|
||||
generated_script: scriptText,
|
||||
synopsis: this.scriptValueObject.storyDetails.synopsis,
|
||||
categories: this.scriptValueObject.storyDetails.categories,
|
||||
protagonist: this.scriptValueObject.storyDetails.protagonist,
|
||||
framework: this.scriptValueObject.storyDetails.mergeframework()
|
||||
});
|
||||
|
||||
if (!response.successful) {
|
||||
throw new Error(response.message || "保存剧本失败");
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("保存剧本失败:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @description: 应用剧本方法
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async applyScript(projectId: string): Promise<void> {
|
||||
async applyScript(projectId: string,planId: string): Promise<void> {
|
||||
try {
|
||||
this.loading = true;
|
||||
|
||||
// 调用应用剧本接口
|
||||
const response = await applyScriptToShot({
|
||||
projectId,
|
||||
scriptText: this.scriptValueObject.toString(),
|
||||
project_id: projectId,
|
||||
plan_id: planId
|
||||
});
|
||||
|
||||
if (!response.successful) {
|
||||
@ -87,7 +155,13 @@ export class ScriptEditUseCase {
|
||||
getScriptSlices(): ScriptSlice[] {
|
||||
return [...this.scriptValueObject.scriptSlices];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取当前剧本故事详情
|
||||
* @returns StoryDetails
|
||||
*/
|
||||
getStoryDetails() {
|
||||
return this.scriptValueObject.storyDetails.toObject();
|
||||
}
|
||||
/**
|
||||
* @description: 获取加载状态
|
||||
* @returns boolean
|
||||
|
||||
30
lib/auth.ts
30
lib/auth.ts
@ -1,7 +1,7 @@
|
||||
import { BASE_URL } from "@/api/constants";
|
||||
|
||||
// API配置
|
||||
const API_BASE_URL = 'https://77.api.qikongjian.com';
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || '';
|
||||
|
||||
// Token存储键
|
||||
const TOKEN_KEY = 'X-EASE-ADMIN-TOKEN';
|
||||
@ -29,7 +29,7 @@ export const loginUser = async (email: string, password: string) => {
|
||||
if (data.code === '200' && data.status === 200) {
|
||||
// 保存token到本地存储
|
||||
setToken(data.body.token);
|
||||
|
||||
|
||||
// 保存用户信息
|
||||
const user = {
|
||||
id: data.body.userId,
|
||||
@ -38,7 +38,7 @@ export const loginUser = async (email: string, password: string) => {
|
||||
token: data.body.token,
|
||||
};
|
||||
setUser(user);
|
||||
|
||||
|
||||
return user;
|
||||
} else {
|
||||
throw new Error(data.msg || '登录失败');
|
||||
@ -104,10 +104,10 @@ export const setToken = (token: string) => {
|
||||
*/
|
||||
export const getCurrentUser = () => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
|
||||
const userJson = localStorage.getItem(USER_KEY);
|
||||
if (!userJson) return null;
|
||||
|
||||
|
||||
try {
|
||||
return JSON.parse(userJson);
|
||||
} catch (error) {
|
||||
@ -165,7 +165,7 @@ export const getAuthHeaders = () => {
|
||||
*/
|
||||
export const authFetch = async (url: string, options: RequestInit = {}) => {
|
||||
const token = getToken();
|
||||
|
||||
|
||||
if (!token) {
|
||||
throw new Error('No token available');
|
||||
}
|
||||
@ -198,7 +198,7 @@ export const authFetch = async (url: string, options: RequestInit = {}) => {
|
||||
|
||||
// Google OAuth相关函数保持不变
|
||||
const GOOGLE_CLIENT_ID = '1016208801816-qtvcvki2jobmcin1g4e7u4sotr0p8g3u.apps.googleusercontent.com';
|
||||
const GOOGLE_REDIRECT_URI = typeof window !== 'undefined'
|
||||
const GOOGLE_REDIRECT_URI = typeof window !== 'undefined'
|
||||
? BASE_URL+'/users/oauth/callback'
|
||||
: '';
|
||||
|
||||
@ -207,7 +207,7 @@ const GOOGLE_REDIRECT_URI = typeof window !== 'undefined'
|
||||
*/
|
||||
export const signInWithGoogle = () => {
|
||||
const state = generateOAuthState();
|
||||
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: GOOGLE_CLIENT_ID,
|
||||
redirect_uri: GOOGLE_REDIRECT_URI,
|
||||
@ -226,13 +226,13 @@ export const signInWithGoogle = () => {
|
||||
*/
|
||||
export const generateOAuthState = () => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
|
||||
|
||||
// Generate a random string for state
|
||||
const state = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
|
||||
// Store the state in session storage to validate later
|
||||
sessionStorage.setItem('oauthState', state);
|
||||
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
@ -241,12 +241,12 @@ export const generateOAuthState = () => {
|
||||
*/
|
||||
export const validateOAuthState = (state: string): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
|
||||
|
||||
const storedState = sessionStorage.getItem('oauthState');
|
||||
|
||||
|
||||
// Clean up the stored state regardless of validity
|
||||
sessionStorage.removeItem('oauthState');
|
||||
|
||||
|
||||
// Validate that the returned state matches what we stored
|
||||
return state === storedState;
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user