chenpei d0dc94e36d feat: 将弹幕功能替换为AI聊天助手
新功能:
- 创建AI聊天助手组件(AIChatWidget)
- 支持文字+图片多模态对话
- 集成Gemini AI API
- 支持图片编辑和内容生成
- 实时对话历史管理
- 优雅的UI设计,支持黑夜模式

技术实现:
- 后端API代理Gemini API(/api/ai-chat)
- 支持最多5张图片上传
- 流式响应处理
- 对话历史管理
- Base64图片处理

配置:
- 在.env中添加Gemini API配置
- GEMINI_API_KEY
- GEMINI_API_BASE_URL
- GEMINI_MODEL

替换原弹幕功能,提供更强大的AI助手体验!
2025-10-27 17:45:37 +08:00

248 lines
5.1 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const validator = require('validator');
// 检查模型是否已经存在,如果存在则直接返回
if (mongoose.models.User) {
module.exports = mongoose.models.User;
return;
}
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
trim: true,
unique: true,
minlength: 2,
maxlength: 30,
validate: {
validator: function(v) {
// 允许中文、英文字母、数字和下划线
// 中文字符范围:\u4e00-\u9fa5
return /^[\u4e00-\u9fa5a-zA-Z0-9_]+$/.test(v);
},
message: props => `${props.value} 不是有效的用户名。用户名只能包含中文、英文字母、数字和下划线。`
}
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error('邮箱格式不正确');
}
}
},
password: {
type: String,
required: true,
minlength: 6,
trim: true
},
role: {
type: String,
enum: ['user', 'admin', 'vip', 'disabled'],
default: 'user'
},
status: {
type: String,
enum: ['pending', 'active', 'suspended'],
default: 'pending'
},
avatar: {
type: String,
default: '/images/default-avatar.png'
},
credits: {
type: Number,
default: 0
},
usageHistory: [{
feature: {
type: String,
required: true
},
timestamp: {
type: Date,
default: Date.now
},
creditsUsed: {
type: Number,
default: 1
}
}],
tokens: [{
token: {
type: String,
required: true
}
}],
createdAt: {
type: Date,
default: Date.now
},
lastLogin: {
type: Date,
default: Date.now
},
resetPasswordToken: String,
resetPasswordExpires: Date,
isEmailVerified: {
type: Boolean,
default: false
},
isManuallyAdded: {
type: Boolean,
default: false
},
emailVerificationToken: String,
emailVerificationExpires: Date,
settings: {
type: mongoose.Schema.Types.Mixed,
default: {}
},
// 钉钉登录相关字段
dingtalk_userid: {
type: String,
sparse: true, // 允许多个null值,但不允许重复的非null值
unique: true
},
loginType: {
type: String,
enum: ['normal', 'dingtalk'],
default: 'normal'
},
phone: {
type: String,
sparse: true,
unique: true,
validate: {
validator: function(v) {
// 如果提供了手机号,验证格式
return !v || /^1[3-9]\d{9}$/.test(v);
},
message: props => `${props.value} 不是有效的手机号格式`
}
},
realName: {
type: String,
trim: true
},
nickname: {
type: String,
trim: true
}
}, {
timestamps: true
});
// 隐藏敏感信息
userSchema.methods.toJSON = function() {
const user = this;
const userObject = user.toObject();
delete userObject.password;
delete userObject.tokens;
return userObject;
};
// 生成认证令牌
userSchema.methods.generateAuthToken = function() {
try {
const user = this;
const token = jwt.sign(
{
userId: user._id.toString(),
username: user.username,
email: user.email,
role: user.role
},
process.env.JWT_SECRET || 'your-secret-key',
{ expiresIn: '7d' }
);
// 保存令牌到用户的tokens数组
user.tokens = user.tokens || [];
user.tokens.push({ token });
return token;
} catch (error) {
console.error('生成令牌错误:', error);
throw error;
}
};
// 查找用户凭据
userSchema.statics.findByCredentials = async (email, password) => {
try {
const user = await User.findOne({ email });
if (!user) {
throw new Error('用户不存在');
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
throw new Error('密码错误');
}
return user;
} catch (error) {
console.error('查找用户凭据错误:', error);
throw error;
}
};
// 保存前加密密码
userSchema.pre('save', async function(next) {
try {
const user = this;
if (user.isModified('password')) {
user.password = await bcrypt.hash(user.password, 8);
}
next();
} catch (error) {
console.error('保存前加密密码错误:', error);
next(error);
}
});
// 记录功能使用
userSchema.methods.recordUsage = async function(feature, creditsUsed = 1) {
try {
const user = this;
// 检查积分是否足够
if (user.credits < creditsUsed) {
throw new Error('积分不足');
}
// 扣除积分并记录使用历史
user.credits -= creditsUsed;
user.usageHistory = user.usageHistory || [];
user.usageHistory.push({
feature,
creditsUsed,
timestamp: new Date()
});
await user.save();
return user;
} catch (error) {
console.error('记录功能使用错误:', error);
throw error;
}
};
const User = mongoose.model('User', userSchema);
module.exports = User;