diff --git a/.gitignore b/.gitignore index 0ac13c4..6406656 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ yarn-error.log* lerna-debug.log* .pnpm-debug.log* package-lock.json - +.wrangler/ # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/devrun.sh b/devrun.sh index 40cad01..c573936 100644 --- a/devrun.sh +++ b/devrun.sh @@ -1 +1,2 @@ +# 启动开发服务器 wrangler pages dev -- npm run dev \ No newline at end of file diff --git a/friends.sql b/friends.sql new file mode 100644 index 0000000..872897a --- /dev/null +++ b/friends.sql @@ -0,0 +1,13 @@ +PRAGMA defer_foreign_keys=TRUE; +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + phone VARCHAR(11) NOT NULL UNIQUE, + nickname VARCHAR(50), + avatar_url TEXT, + status INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO users VALUES(1,'13800138000','测试用户',NULL,1,'2025-03-26 08:39:15','2025-03-26 08:39:15','2025-03-26 08:39:15'); +INSERT INTO users VALUES(2,'13391881866','Hobby',NULL,1,'2025-03-26 09:42:30','2025-03-27 05:50:04','2025-03-27 03:21:40'); \ No newline at end of file diff --git a/functions/api/_middleware.js b/functions/api/_middleware.js new file mode 100644 index 0000000..170f1bd --- /dev/null +++ b/functions/api/_middleware.js @@ -0,0 +1,80 @@ +async function verifyToken(token, env) { + try { + const [headerB64, payloadB64, signature] = token.split('.'); + + // 验证签名 + const input = `${headerB64}.${payloadB64}`; + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(env.JWT_SECRET), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['verify'] + ); + + const signatureBytes = Uint8Array.from(atob( + signature.replace(/-/g, '+').replace(/_/g, '/') + ), c => c.charCodeAt(0)); + + const isValid = await crypto.subtle.verify( + 'HMAC', + key, + signatureBytes, + encoder.encode(input) + ); + + if (!isValid) { + throw new Error('Invalid signature'); + } + + // 解码 payload + const payload = JSON.parse(atob( + payloadB64.replace(/-/g, '+').replace(/_/g, '/') + )); + + // 检查过期时间 + if (payload.exp < Math.floor(Date.now() / 1000)) { + throw new Error('Token expired'); + } + + return payload; + } catch (error) { + throw new Error('Invalid token'); + } +} + +// 中间件函数 +export async function onRequest(context) { + try { + const authAccess = context.env.AUTH_ACCESS; + console.log('authAccess', authAccess); + + if (!authAccess || authAccess === '0' || context.request.url.includes('/login') || context.request.url.includes('/sendcode') || context.request.url.includes('/test-db')) { + console.log('跳过权限校验'); + context.data = { user: null }; + return context.next(); + } + + const authHeader = context.request.headers.get('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + throw new Error('No token provided'); + } + + const token = authHeader.split(' ')[1]; + const payload = await verifyToken(token, context.env); + + // 直接在原有的 request 对象上添加 context + context.data = { user: payload }; + console.log('context.request.user', context.data); + return context.next(); + } catch (error) { + console.error(error.message, context.request.url); + return new Response(JSON.stringify({ error: error.message }), { + status: 401, + headers: { + 'Content-Type': 'application/json' + } + }); + } +} diff --git a/functions/api/init.ts b/functions/api/init.ts index 68495b0..2aee297 100644 --- a/functions/api/init.ts +++ b/functions/api/init.ts @@ -1,13 +1,13 @@ import {generateAICharacters } from '../../src/config/aiCharacters'; import { groups } from '../../src/config/groups'; -export async function onRequestGet({ env, request }) { - console.log('init'); +export async function onRequestGet(context) { try { return Response.json({ code: 200, data: { groups: groups, characters: generateAICharacters('#groupName#', '#allTags#'), + user: context.data.user || null } }); } catch (error) { diff --git a/functions/api/login.ts b/functions/api/login.ts new file mode 100644 index 0000000..a5ced7b --- /dev/null +++ b/functions/api/login.ts @@ -0,0 +1,196 @@ +interface Env { + bgkv: KVNamespace; + JWT_SECRET: string; + bgdb: D1Database; +} + +export const onRequestPost: PagesFunction = async (context) => { + try { + const { request, env } = context; + + // 获取请求体 + const body = await request.json(); + const { phone, code } = body; + + // 验证手机号格式 + if (!phone || !/^1[3-9]\d{9}$/.test(phone)) { + return new Response( + JSON.stringify({ + success: false, + message: '无效的手机号码' + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // 验证码格式检查 + if (!code || !/^\d{6}$/.test(code)) { + return new Response( + JSON.stringify({ + success: false, + message: '验证码格式错误' + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // 从 KV 中获取存储的验证码 + const storedCode = await env.bgkv.get(`sms:${phone}`); + if (!storedCode || storedCode !== code) { + return new Response( + JSON.stringify({ + success: false, + message: '验证码错误或已过期' + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // 验证成功后,处理用户数据 + const db = env.bgdb; // 假设你的 D1 数据库实例名为 DB + // 查询用户是否存在 + const existingUser = await db.prepare( + "SELECT id, phone, nickname FROM users WHERE phone = ?" + ).bind(phone).first(); + + console.log('existingUser', existingUser); + let userId; + if (!existingUser) { + // 用户不存在,创建新用户 + const result = await db.prepare(` + INSERT INTO users (phone, nickname, status, created_at, updated_at, last_login_at) + VALUES (?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `).bind(phone, `用户${phone.substring(7)}`).run(); + + userId = result.lastRowId; + } else { + // 用户存在,更新登录时间 + await db.prepare(` + UPDATE users + SET last_login_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + WHERE phone = ? + `).bind(phone).run(); + + userId = existingUser.id; + } + + // 获取完整的用户信息 + const userInfo = await db.prepare(` + SELECT id, phone, nickname, avatar_url, status + FROM users + WHERE phone = ? + `).bind(phone).first(); + + userId = userInfo.id + + // 生成 token + const token = await generateToken(userId, env); + + // 删除验证码 + await env.bgkv.delete(`sms:${phone}`); + + // 返回用户信息和token + return new Response( + JSON.stringify({ + success: true, + message: '登录成功', + data: { + token, + user: userInfo + } + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + } catch (error) { + console.error(error); + return new Response( + JSON.stringify({ + success: false, + message: '服务器错误' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } +}; + +// 修改为 async 函数 +async function generateToken(userId: string, env: Env): Promise { + const header = { + alg: 'HS256', + typ: 'JWT' + }; + + const payload = { + userId, + exp: Math.floor(Date.now() / 1000) + (7 * 24 * 60 * 60), // 7天过期 + iat: Math.floor(Date.now() / 1000) + }; + + // Base64Url 编码 + const encodeBase64Url = (data: object) => { + return btoa(JSON.stringify(data)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + }; + + // 生成签名 + const generateSignature = async (input: string, secret: string) => { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(secret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + + const signature = await crypto.subtle.sign( + 'HMAC', + key, + encoder.encode(input) + ); + + return btoa(String.fromCharCode(...new Uint8Array(signature))) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + }; + + const headerEncoded = encodeBase64Url(header); + const payloadEncoded = encodeBase64Url(payload); + + const signature = await generateSignature( + `${headerEncoded}.${payloadEncoded}`, + env.JWT_SECRET + ); + + return `${headerEncoded}.${payloadEncoded}.${signature}`; +} \ No newline at end of file diff --git a/functions/api/sendCode.ts b/functions/api/sendCode.ts new file mode 100644 index 0000000..2cca299 --- /dev/null +++ b/functions/api/sendCode.ts @@ -0,0 +1,85 @@ +import { sendSMS } from '../utils/sms'; + +interface Env { + ALIYUN_ACCESS_KEY_ID: string; + ALIYUN_ACCESS_KEY_SECRET: string; + ALIYUN_SMS_SIGN_NAME: string; + ALIYUN_SMS_TEMPLATE_CODE: string; + bgkv: KVNamespace; +} + +export const onRequestPost: PagesFunction = async (context) => { + const { request, env } = context; + + try { + const { phone } = await request.json(); + + if (!phone || !/^1[3-9]\d{9}$/.test(phone)) { + return new Response( + JSON.stringify({ + success: false, + message: '请输入正确的手机号' + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + // 开发环境使用固定验证码 + const verificationCode = env.CF_PAGES_ENVIRONMENT !== 'production' + ? '123456' + : Math.random().toString().slice(-6); + if (env.CF_PAGES_ENVIRONMENT === 'production') { + try { + await sendSMS(phone, verificationCode, { + accessKeyId: env.ALIYUN_ACCESS_KEY_ID, + accessKeySecret: env.ALIYUN_ACCESS_KEY_SECRET, + signName: env.ALIYUN_SMS_SIGN_NAME, + templateCode: env.ALIYUN_SMS_TEMPLATE_CODE + }); + } catch (error) { + console.error('SMS Error:', error); + return new Response( + JSON.stringify({ + success: false, + message: error instanceof Error ? error.message : '发送验证码失败,请重试' + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ); + } + } + + // 存储验证码到 KV,设置 5 分钟过期 + await env.bgkv.put(`sms:${phone}`, verificationCode, { + expirationTtl: 5 * 60 // 5分钟 + }); + + return new Response( + JSON.stringify({ + success: true, + message: '验证码发送成功' + }), + { + headers: { 'Content-Type': 'application/json' }, + } + ); + + } catch (error) { + console.error('Request Error:', error); + return new Response( + JSON.stringify({ + success: false, + message: '请求格式错误' + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' }, + } + ); + } +}; diff --git a/functions/api/test-db.ts b/functions/api/test-db.ts new file mode 100644 index 0000000..e816d3f --- /dev/null +++ b/functions/api/test-db.ts @@ -0,0 +1,32 @@ +interface Env { + bgdb: D1Database; +} + +export const onRequestGet: PagesFunction = async (context) => { + try { + const { env } = context; + const db = env.bgdb; + + // 测试数据库连接 + const result = await db.prepare( + "SELECT * FROM users" + ).all(); + + return new Response(JSON.stringify({ + success: true, + tables: result, + env: Object.keys(env) + }), { + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + return new Response(JSON.stringify({ + success: false, + error: error.message, + stack: error.stack + }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } +}; \ No newline at end of file diff --git a/functions/api/user/info.ts b/functions/api/user/info.ts new file mode 100644 index 0000000..4b4697c --- /dev/null +++ b/functions/api/user/info.ts @@ -0,0 +1,64 @@ +interface Env { + bgdb: D1Database; +} + +export const onRequestGet: PagesFunction = async (context) => { + try { + const { env, data } = context; + + // 从数据库获取用户信息 + const db = env.bgdb; + const userInfo = await db.prepare(` + SELECT id, phone, nickname, avatar_url, status + FROM users + WHERE id = ? + `).bind(data.user.userId).first(); + + if (!userInfo) { + return new Response( + JSON.stringify({ + success: false, + message: '用户不存在' + }), + { + status: 404, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + //处理avatar_url + if (userInfo.avatar_url) { + userInfo.avatar_url = `${env.NEXT_PUBLIC_CF_IMAGES_DELIVERY_URL}/${userInfo.avatar_url}/public`; + } + + return new Response( + JSON.stringify({ + success: true, + data: userInfo + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + } catch (error) { + console.error(error); + return new Response( + JSON.stringify({ + success: false, + message: '服务器错误' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } +}; \ No newline at end of file diff --git a/functions/api/user/update.ts b/functions/api/user/update.ts new file mode 100644 index 0000000..dd5f999 --- /dev/null +++ b/functions/api/user/update.ts @@ -0,0 +1,107 @@ +interface Env { + bgdb: D1Database; +} + +export const onRequestPost: PagesFunction = async (context) => { + try { + const { env, data, request } = context; + + // 解析请求体 + const body = await request.json(); + const { nickname, avatar_url } = body; + + // 构建 SQL 更新语句和参数 + let sql = 'UPDATE users SET updated_at = DATETIME(\'now\')'; + const params = []; + + // 如果有昵称更新 + if (nickname !== undefined) { + if (typeof nickname !== 'string' || nickname.length > 32) { + return new Response( + JSON.stringify({ + success: false, + message: '昵称格式不正确' + }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + sql += ', nickname = ?'; + params.push(nickname); + } + + // 如果有头像更新 + if (avatar_url !== undefined) { + if (typeof avatar_url !== 'string') { + return new Response( + JSON.stringify({ + success: false, + message: '头像URL格式不正确' + }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + sql += ', avatar_url = ?'; + params.push(avatar_url); + } + + // 添加 WHERE 条件 + sql += ' WHERE id = ?'; + params.push(data.user.userId); + + // 执行更新 + const result = await env.bgdb.prepare(sql).bind(...params).run(); + + if (!result.success) { + return new Response( + JSON.stringify({ + success: false, + message: '更新失败' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // 获取更新后的用户信息 + const userInfo = await env.bgdb.prepare(` + SELECT id, phone, nickname, avatar_url, status + FROM users + WHERE id = ? + `).bind(data.user.userId).first(); + //处理avatar_url + if (userInfo.avatar_url) { + userInfo.avatar_url = `${env.NEXT_PUBLIC_CF_IMAGES_DELIVERY_URL}/${userInfo.avatar_url}/public`; + } + return new Response( + JSON.stringify({ + success: true, + data: userInfo + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + } catch (error) { + console.error(error); + return new Response( + JSON.stringify({ + success: false, + message: '服务器错误' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } +}; \ No newline at end of file diff --git a/functions/api/user/upload.ts b/functions/api/user/upload.ts new file mode 100644 index 0000000..698688d --- /dev/null +++ b/functions/api/user/upload.ts @@ -0,0 +1,24 @@ +export const onRequestPost: PagesFunction = async (context) =>{ + const { env, data } = context; + + // 创建 FormData 对象 + const formData = new FormData(); + formData.append('requireSignedURLs', 'false'); + formData.append('metadata', JSON.stringify({ + user: data.user.userId + })); + + // 调用 Cloudflare Images API 获取直接上传 URL + const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/images/v2/direct_upload`, { + method: "POST", + headers: { + "Authorization": `Bearer ${env.CF_API_TOKEN}`, + }, + body: formData + }); + + const data1 = await response.json(); + console.log('上传头像', data1); + //下发image前缀 + return Response.json(data1.result); + } \ No newline at end of file diff --git a/functions/utils/sms.ts b/functions/utils/sms.ts new file mode 100644 index 0000000..9e4e8a9 --- /dev/null +++ b/functions/utils/sms.ts @@ -0,0 +1,125 @@ +interface AliyunSMSConfig { + accessKeyId: string; + accessKeySecret: string; + signName: string; + templateCode: string; +} + +// 辅助函数:生成随机字符串 +function generateNonce(length: number): string { + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); +} + +// 辅助函数:计算 HMAC-SHA256(V3 使用 SHA256) +async function calculateHmacSha256(message: string, secret: string): Promise { + const encoder = new TextEncoder(); + const keyData = encoder.encode(secret); + const messageData = encoder.encode(message); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + + const signature = await crypto.subtle.sign( + 'HMAC', + key, + messageData + ); + + return btoa(String.fromCharCode(...new Uint8Array(signature))); +} + +export async function sendSMS(phone: string, code: string, config: AliyunSMSConfig) { + const API_URL = 'https://dysmsapi.aliyuncs.com/'; + const API_VERSION = '2017-05-25'; + + // 准备请求参数 + const params = { + AccessKeyId: config.accessKeyId, + Action: 'SendSms', + Format: 'JSON', // 明确指定返回 JSON 格式 + PhoneNumbers: phone, + SignName: config.signName, + TemplateCode: config.templateCode, + TemplateParam: JSON.stringify({ code }), + Version: API_VERSION, + SignatureMethod: 'HMAC-SHA1', + SignatureVersion: '1.0', + SignatureNonce: generateNonce(16), + Timestamp: new Date().toISOString() + }; + + // 参数排序 + const sortedParams = Object.keys(params) + .sort() + .reduce((acc, key) => ({ + ...acc, + [key]: params[key] + }), {}); + + // 构建签名字符串 + const canonicalizedQueryString = Object.entries(sortedParams) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value as string)}`) + .join('&'); + + const stringToSign = `GET&${encodeURIComponent('/')}&${encodeURIComponent(canonicalizedQueryString)}`; + + // 计算签名 + const signature = await calculateHmacSha1(stringToSign, `${config.accessKeySecret}&`); + + // 构建最终的 URL + const finalUrl = `${API_URL}?${canonicalizedQueryString}&Signature=${encodeURIComponent(signature)}`; + + // 发送请求 + const response = await fetch(finalUrl, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + + const responseText = await response.text(); + let responseData; + + try { + responseData = JSON.parse(responseText); + } catch (e) { + console.error('Response:', responseText); + throw new Error('Invalid response format from SMS service'); + } + //console.log(responseData, finalUrl) + if (!response.ok || responseData.Code !== 'OK') { + throw new Error(responseData.Message || 'SMS send failed'); + } + + return responseData; +} + +// 辅助函数:计算 HMAC-SHA1 +async function calculateHmacSha1(message: string, secret: string): Promise { + const encoder = new TextEncoder(); + const keyData = encoder.encode(secret); + const messageData = encoder.encode(message); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'HMAC', hash: 'SHA-1' }, + false, + ['sign'] + ); + + const signature = await crypto.subtle.sign( + 'HMAC', + key, + messageData + ); + + return btoa(String.fromCharCode(...new Uint8Array(signature))); +} \ No newline at end of file diff --git a/init-db.sh b/init-db.sh new file mode 100644 index 0000000..ef79f74 --- /dev/null +++ b/init-db.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# 文件名: init-db.sh + +rm -rf .local-db + +# 重新创建目录 +mkdir -p .local-db + +# 删除旧数据库(如果存在) +wrangler d1 delete friends --local + +# 创建新数据库 +wrangler d1 create friends --local --persist-to=.local-db + +# 创建表 +wrangler d1 execute friends --local --persist-to=.local-db --command=" +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + phone VARCHAR(11) NOT NULL UNIQUE, + nickname VARCHAR(50), + avatar_url TEXT, + status INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +);" + +# 插入测试数据 +wrangler d1 execute friends --local --persist-to=.local-db --command=" +INSERT OR IGNORE INTO users (phone, nickname) +VALUES ('13800138000', '测试用户');" + +# 验证表是否创建成功 +echo "验证表结构:" +wrangler d1 execute friends --local --persist-to=.local-db --command="PRAGMA table_info(users);" + +echo "验证数据:" +wrangler d1 execute friends --local --persist-to=.local-db --command="SELECT * FROM users;" + +echo "数据库初始化完成。使用以下命令启动开发服务器:" +echo "wrangler pages dev --d1 bgdb=friends --persist-to=.local-db -- npm run dev" diff --git a/migrations/0001_init_users.sql b/migrations/0001_init_users.sql new file mode 100644 index 0000000..cec8201 --- /dev/null +++ b/migrations/0001_init_users.sql @@ -0,0 +1 @@ +-- Migration number: 0001 2025-03-27T12:27:07.602Z diff --git a/migrations/0002_create_users.sql b/migrations/0002_create_users.sql new file mode 100644 index 0000000..c7a3979 --- /dev/null +++ b/migrations/0002_create_users.sql @@ -0,0 +1 @@ +-- Migration number: 0002 2025-03-28T06:06:16.353Z diff --git a/package-lock.json b/package-lock.json index c558cb6..72549f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "chat-ui", "version": "1.0.0", "dependencies": { + "@alicloud/credentials": "^2.4.2", + "@alicloud/dysmsapi20170525": "^3.1.1", "@fontsource/audiowide": "^5.1.1", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", @@ -30,6 +32,7 @@ "react-dom": "^18.2.0", "react-github-btn": "^1.4.0", "react-markdown": "^9.0.3", + "react-router-dom": "^7.4.0", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", @@ -37,7 +40,8 @@ "tailwind-merge": "^2.6.0", "tailwind-scrollbar-hide": "^2.0.0", "tailwindcss-animate": "^1.0.7", - "wrangler": "^3.112.0" + "wrangler": "^3.112.0", + "zustand": "^5.0.3" }, "devDependencies": { "@shadcn/ui": "^0.0.4", @@ -54,6 +58,114 @@ "vite": "^5.0.0" } }, + "node_modules/@alicloud/credentials": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/@alicloud/credentials/-/credentials-2.4.2.tgz", + "integrity": "sha512-UbqUYlwOWKNxOemXM545HzQyCaChhyrne9cab4f67EqAkgrTjeMiTA7QK6sHtmcmJYowYQxxXoKPSe5GZstvbA==", + "dependencies": { + "@alicloud/tea-typescript": "^1.8.0", + "httpx": "^2.3.3", + "ini": "^1.3.5", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/dysmsapi20170525": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@alicloud/dysmsapi20170525/-/dysmsapi20170525-3.1.1.tgz", + "integrity": "sha512-UvrQo9p1b7A/JH209jPFLdtuYGywMrn4vWl48LwGxgZOH21i/LQXJKGhIUkeN9/CbdWsW709lkJ9kWvzmQZ5gQ==", + "dependencies": { + "@alicloud/endpoint-util": "^0.0.1", + "@alicloud/openapi-client": "^0.4.12", + "@alicloud/openapi-util": "^0.3.2", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.4.9" + } + }, + "node_modules/@alicloud/endpoint-util": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz", + "integrity": "sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/gateway-spi": { + "version": "0.0.8", + "resolved": "https://registry.npmmirror.com/@alicloud/gateway-spi/-/gateway-spi-0.0.8.tgz", + "integrity": "sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g==", + "dependencies": { + "@alicloud/credentials": "^2", + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/openapi-client": { + "version": "0.4.13", + "resolved": "https://registry.npmmirror.com/@alicloud/openapi-client/-/openapi-client-0.4.13.tgz", + "integrity": "sha512-APi5NwzY6IMxGxM3bTehCzfFVsvY11ljbERhuO2Q4Lfemo4yF23ph38nXLmpv92exDmc4Xe93qKDhnVYMVEdiQ==", + "dependencies": { + "@alicloud/credentials": "^2.4.2", + "@alicloud/gateway-spi": "^0.0.8", + "@alicloud/openapi-util": "^0.3.2", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "1.4.9", + "@alicloud/tea-xml": "0.0.3" + } + }, + "node_modules/@alicloud/openapi-client/node_modules/@alicloud/tea-util": { + "version": "1.4.9", + "resolved": "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.9.tgz", + "integrity": "sha512-S0wz76rGtoPKskQtRTGqeuqBHFj8BqUn0Vh+glXKun2/9UpaaaWmuJwcmtImk6bJZfLYEShDF/kxDmDJoNYiTw==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/openapi-util": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@alicloud/openapi-util/-/openapi-util-0.3.2.tgz", + "integrity": "sha512-EC2JvxdcOgMlBAEG0+joOh2IB1um8CPz9EdYuRfTfd1uP8Yc9D8QRUWVGjP6scnj6fWSOaHFlit9H6PrJSyFow==", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.3.0", + "kitx": "^2.1.0", + "sm3": "^1.0.3" + } + }, + "node_modules/@alicloud/tea-typescript": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz", + "integrity": "sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==", + "dependencies": { + "@types/node": "^12.0.2", + "httpx": "^2.2.6" + } + }, + "node_modules/@alicloud/tea-typescript/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "node_modules/@alicloud/tea-util": { + "version": "1.4.10", + "resolved": "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.10.tgz", + "integrity": "sha512-VEsXWP2dlJLvsY2THj+sH++zwxQRz3Y5BQ8EkfnFems36RkngQKYOLsoto5nR6ej1Gf6I+0IOgBXrkRdpNCQ1g==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "@darabonba/typescript": "^1.0.0", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/tea-xml": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/@alicloud/tea-xml/-/tea-xml-0.0.3.tgz", + "integrity": "sha512-+/9GliugjrLglsXVrd1D80EqqKgGpyA0eQ6+1ZdUOYCaRguaSwz44trX3PaxPu/HhIPJg9PsGQQ3cSLXWZjbAA==", + "dependencies": { + "@alicloud/tea-typescript": "^1", + "@types/xml2js": "^0.4.5", + "xml2js": "^0.6.0" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -439,6 +551,19 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@darabonba/typescript": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@darabonba/typescript/-/typescript-1.0.3.tgz", + "integrity": "sha512-/y2y6wf5TsxD7pCPIm0OvTC+5qV0Tk7HQYxwpIuWRLXQLB0CRDvr6qk4bR6rTLO/JglJa8z2uCGZsaLYpQNqFQ==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "httpx": "^2.3.2", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "moment-timezone": "^0.5.45", + "xml2js": "^0.6.2" + } + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -2405,6 +2530,11 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz", @@ -2533,6 +2663,14 @@ "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmmirror.com/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -3988,6 +4126,28 @@ "node": ">=8.0.0" } }, + "node_modules/httpx": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/httpx/-/httpx-2.3.3.tgz", + "integrity": "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==", + "dependencies": { + "@types/node": "^20", + "debug": "^4.1.1" + } + }, + "node_modules/httpx/node_modules/@types/node": { + "version": "20.17.27", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/httpx/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, "node_modules/human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -4051,6 +4211,11 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmmirror.com/inline-style-parser/-/inline-style-parser-0.2.4.tgz", @@ -4309,6 +4474,14 @@ "node": ">= 12" } }, + "node_modules/kitx": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/kitx/-/kitx-2.2.0.tgz", + "integrity": "sha512-tBMwe6AALTBQJb0woQDD40734NKzb0Kzi3k7wQj9ar3AbP9oqhoVrdXPh7rk2r00/glIgd0YbToIUJsnxWMiIg==", + "dependencies": { + "@types/node": "^22.5.4" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -4334,6 +4507,11 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.castarray": { "version": "4.4.0", "resolved": "https://registry.npmmirror.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz", @@ -5386,6 +5564,25 @@ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6060,6 +6257,52 @@ } } }, + "node_modules/react-router": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.4.0.tgz", + "integrity": "sha512-Y2g5ObjkvX3VFeVt+0CIPuYd9PpgqCslG7ASSIdN73LwA1nNWzcMLaoMRJfP3prZFI92svxFwbn7XkLJ+UPQ6A==", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.4.0.tgz", + "integrity": "sha512-VlksBPf3n2bijPvnA7nkTsXxMAKOj+bWp4R9c3i+bnwlSOFAGOkJkKhzy/OsRkWaBMICqcAl1JDzh9ZSOze9CA==", + "dependencies": { + "react-router": "7.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -6432,6 +6675,11 @@ } ] }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -6449,6 +6697,11 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.33.5.tgz", @@ -6549,6 +6802,11 @@ "node": ">=8" } }, + "node_modules/sm3": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sm3/-/sm3-1.0.3.tgz", + "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==" + }, "node_modules/sonner": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/sonner/-/sonner-2.0.0.tgz", @@ -6918,6 +7176,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -7840,6 +8103,26 @@ } } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -7876,6 +8159,34 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 8fa2622..7ab24b1 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "preview": "vite preview" }, "dependencies": { + "@alicloud/credentials": "^2.4.2", + "@alicloud/dysmsapi20170525": "^3.1.1", "@fontsource/audiowide": "^5.1.1", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", @@ -30,6 +32,7 @@ "react-dom": "^18.2.0", "react-github-btn": "^1.4.0", "react-markdown": "^9.0.3", + "react-router-dom": "^7.4.0", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", @@ -37,7 +40,8 @@ "tailwind-merge": "^2.6.0", "tailwind-scrollbar-hide": "^2.0.0", "tailwindcss-animate": "^1.0.7", - "wrangler": "^3.112.0" + "wrangler": "^3.112.0", + "zustand": "^5.0.3" }, "devDependencies": { "@shadcn/ui": "^0.0.4", diff --git a/src/App.tsx b/src/App.tsx index 7154bbe..957ceec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,11 @@ -import ChatUI from './components/ChatUI' -import './App.css' -import Layout from './components/Layout' +import { RouterProvider } from 'react-router-dom'; +import { router } from './routes'; function App() { + console.log("App rendering"); // 添加日志 return ( - - - - ) + + ); } -export default App +export default App; \ No newline at end of file diff --git a/src/components/AuthGuard.jsx b/src/components/AuthGuard.jsx new file mode 100644 index 0000000..c85aeaf --- /dev/null +++ b/src/components/AuthGuard.jsx @@ -0,0 +1,21 @@ +import { Navigate, useLocation } from 'react-router-dom'; + +export default function AuthGuard({ children }) { + //判断环境变量中的AUTH_ACCESS是否为1开启权限校验 + const authAccess = import.meta.env.AUTH_ACCESS; + if (authAccess === '1') { + const location = useLocation(); + const token = localStorage.getItem('token'); + + if (!token && location.pathname !== '/login') { + return ; + } + + if (token && location.pathname === '/login') { + return ; + } + } + + + return children; +} \ No newline at end of file diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx new file mode 100644 index 0000000..12e32c0 --- /dev/null +++ b/src/layouts/BasicLayout.tsx @@ -0,0 +1,22 @@ +import { Outlet, useNavigate } from 'react-router-dom'; + +export default function BasicLayout() { + const navigate = useNavigate(); + + const handleLogout = () => { + localStorage.removeItem('token'); + navigate('/login'); + }; + + return ( +
+
+
AI Chat
+ +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/components/AdSection.tsx b/src/pages/chat/components/AdSection.tsx similarity index 99% rename from src/components/AdSection.tsx rename to src/pages/chat/components/AdSection.tsx index a36ce32..1bff3b7 100644 --- a/src/components/AdSection.tsx +++ b/src/pages/chat/components/AdSection.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { cn } from '../lib/utils'; +import { cn } from '@/lib/utils'; import { Popover, PopoverContent, diff --git a/src/components/ChatUI.tsx b/src/pages/chat/components/ChatUI.tsx similarity index 83% rename from src/components/ChatUI.tsx rename to src/pages/chat/components/ChatUI.tsx index 5e21286..6fbf749 100644 --- a/src/components/ChatUI.tsx +++ b/src/pages/chat/components/ChatUI.tsx @@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; - +import { request } from '@/utils/request'; import { Tooltip, TooltipContent, @@ -17,98 +17,13 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import rehypeKatex from 'rehype-katex' -import { SharePoster } from '@/components/SharePoster'; -import { MembersManagement } from '@/components/MembersManagement'; +import { SharePoster } from '@/pages/chat/components/SharePoster'; +import { MembersManagement } from '@/pages/chat/components/MembersManagement'; import Sidebar from './Sidebar'; import { AdBanner, AdBannerMobile } from './AdSection'; -// 使用本地头像数据,避免外部依赖 -const getAvatarData = (name: string) => { - const colors = ['#1abc9c', '#3498db', '#9b59b6', '#f1c40f', '#e67e22']; - const index = (name.charCodeAt(0) + (name.charCodeAt(1) || 0 )) % colors.length; - return { - backgroundColor: colors[index], - text: name[0], - }; -}; +import { useUserStore } from '@/store/userStore'; +import { getAvatarData } from '@/utils/avatar'; -// 单个完整头像 -const SingleAvatar = ({ user }: { user: User | AICharacter }) => { - // 如果有头像就使用头像,否则使用默认的文字头像 - if ('avatar' in user && user.avatar) { - return ( -
- {user.name} -
- ); - } - const avatarData = getAvatarData(user.name); - return ( -
- {avatarData.text} -
- ); -}; - -// 左右分半头像 -const HalfAvatar = ({ user, isFirst }: { user: User, isFirst: boolean }) => { - if ('avatar' in user && user.avatar) { - return ( -
- {user.name} -
- ); - } - const avatarData = getAvatarData(user.name); - return ( -
- {avatarData.text} -
- ); -}; - -// 四分之一头像 -const QuarterAvatar = ({ user, index }: { user: User, index: number }) => { - if ('avatar' in user && user.avatar) { - return ( -
- {user.name} -
- ); - } - const avatarData = getAvatarData(user.name); - return ( -
- {avatarData.text} -
- ); -}; // 修改 KaTeXStyle 组件 const KaTeXStyle = () => ( @@ -137,10 +52,10 @@ const KaTeXStyle = () => ( `}} /> ); -// Vite环境变量访问方式 -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; const ChatUI = () => { + const userStore = useUserStore(); + //获取url参数 const urlParams = new URLSearchParams(window.location.search); const id = urlParams.get('id')? parseInt(urlParams.get('id')!) : 0; @@ -182,7 +97,7 @@ const ChatUI = () => { const initData = async () => { try { - const response = await fetch(`${API_BASE_URL}/api/init`); + const response = await request(`/api/init`); if (!response.ok) { throw new Error('初始化数据失败'); } @@ -203,9 +118,28 @@ const ChatUI = () => { setGroupAiCharacters(groupAiCharacters); const allNames = groupAiCharacters.map(character => character.name); allNames.push('user'); + let avatar_url = null; + let nickname = '我'; setAllNames(allNames); + if (data.user && data.user != null) { + const response1 = await request('/api/user/info'); + const userInfo = await response1.json(); + //设置store + userStore.setUserInfo(userInfo.data); + avatar_url = userInfo.data.avatar_url; + nickname = userInfo.data.nickname; + } else { + // 设置空的用户信息 + userStore.setUserInfo({ + id: 0, + phone: '', + nickname: nickname, + avatar_url: null, + status: 0 + }); + } setUsers([ - { id: 1, name: "我" }, + { id: 1, name: nickname, avatar: avatar_url }, ...groupAiCharacters ]); } catch (error) { @@ -217,7 +151,7 @@ const ChatUI = () => { initData(); // 标记为已初始化 isInitialized.current = true; - }, []); // 依赖数组保持为空 + }, [userStore]); useEffect(() => { scrollToBottom(); @@ -237,6 +171,16 @@ const ChatUI = () => { }; }, []); + // 添加一个新的 useEffect 来监听 userStore.userInfo 的变化 + useEffect(() => { + if (userStore.userInfo && users.length > 0) { + setUsers(prev => [ + { id: 1, name: userStore.userInfo.nickname, avatar: userStore.userInfo.avatar_url? userStore.userInfo.avatar_url : null }, + ...prev.slice(1) // 保留其他 AI 角色 + ]); + } + }, [userStore.userInfo]); // 当 userInfo 变化时更新 users + // 4. 工具函数 const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); @@ -292,12 +236,12 @@ const ChatUI = () => { // 构建历史消息数组 let messageHistory = messages.map(msg => ({ role: 'user', - content: msg.sender.name == "我" ? 'user:' + msg.content : msg.sender.name + ':' + msg.content, + content: msg.sender.name == userStore.userInfo.nickname ? 'user:' + msg.content : msg.sender.name + ':' + msg.content, name: msg.sender.name })); let selectedGroupAiCharacters = groupAiCharacters; if (!isGroupDiscussionMode) { - const shedulerResponse = await fetch(`${API_BASE_URL}/api/scheduler`, { + const shedulerResponse = await request(`/api/scheduler`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -325,7 +269,7 @@ const ChatUI = () => { setMessages(prev => [...prev, aiMessage]); try { - const response = await fetch(`${API_BASE_URL}/api/chat`, { + const response = await request(`/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -466,34 +410,6 @@ const ChatUI = () => { //进行跳转到?id=index window.location.href = `?id=${index}`; return; - /* - //跳转后,关闭当前页面 - setSelectedGroupIndex(index); - const newGroup = groups[index]; - setGroup(newGroup); - - // 重新生成当前群组的 AI 角色,并按照 members 数组的顺序排序 - const newGroupAiCharacters = generateAICharacters(newGroup.name) - .filter(character => newGroup.members.includes(character.id)) - .sort((a, b) => { - return newGroup.members.indexOf(a.id) - newGroup.members.indexOf(b.id); - }); - - // 更新用户列表 - setUsers([ - { id: 1, name: "我" }, - ...newGroupAiCharacters - ]); - setIsGroupDiscussionMode(newGroup.isGroupDiscussionMode); - - // 重置消息 - setMessages([]); - - // 可选:关闭侧边栏(在移动设备上) - if (window.innerWidth < 768) { - setSidebarOpen(false); - } - */ }; return ( @@ -542,7 +458,7 @@ const ChatUI = () => { - {'avatar' in user && user.avatar ? ( + {'avatar' in user && user.avatar && user.avatar !== null ? ( ) : ( @@ -581,8 +497,8 @@ const ChatUI = () => {
{messages.map((message) => (
- {message.sender.name !== "我" && ( + className={`flex items-start gap-2 ${message.sender.name === userStore.userInfo.nickname ? "justify-end" : ""}`}> + {message.sender.name !== userStore.userInfo.nickname && ( {'avatar' in message.sender && message.sender.avatar ? ( @@ -593,16 +509,16 @@ const ChatUI = () => { )} )} -
+
{message.sender.name}
{ )}
- {message.sender.name === "我" && ( + {message.sender.name === userStore.userInfo.nickname && ( {'avatar' in message.sender && message.sender.avatar ? ( diff --git a/src/components/Header.tsx b/src/pages/chat/components/Header.tsx similarity index 100% rename from src/components/Header.tsx rename to src/pages/chat/components/Header.tsx diff --git a/src/components/Layout.tsx b/src/pages/chat/components/Layout.tsx similarity index 100% rename from src/components/Layout.tsx rename to src/pages/chat/components/Layout.tsx diff --git a/src/components/MembersManagement.tsx b/src/pages/chat/components/MembersManagement.tsx similarity index 100% rename from src/components/MembersManagement.tsx rename to src/pages/chat/components/MembersManagement.tsx diff --git a/src/components/SharePoster.tsx b/src/pages/chat/components/SharePoster.tsx similarity index 100% rename from src/components/SharePoster.tsx rename to src/pages/chat/components/SharePoster.tsx diff --git a/src/components/Sidebar.tsx b/src/pages/chat/components/Sidebar.tsx similarity index 97% rename from src/components/Sidebar.tsx rename to src/pages/chat/components/Sidebar.tsx index 9415f0a..887bc46 100644 --- a/src/components/Sidebar.tsx +++ b/src/pages/chat/components/Sidebar.tsx @@ -4,8 +4,8 @@ import { MessageSquareIcon, PlusCircleIcon, MenuIcon, PanelLeftCloseIcon } from import { cn } from "@/lib/utils"; import GitHubButton from 'react-github-btn'; import '@fontsource/audiowide'; -//import { groups } from "@/config/groups"; import { AdSection } from './AdSection'; +import { UserSection } from './UserSection'; import { Tooltip, TooltipContent, @@ -123,6 +123,9 @@ const Sidebar = ({ isOpen, toggleSidebar, selectedGroupIndex = 0, onSelectGroup, {/* 广告位 */} + {/* 用户信息模块 */} + + {/* GitHub Star Button - 只在侧边栏打开时显示,放在底部 */}
{/* 标题移至底部 */} @@ -141,7 +144,7 @@ const Sidebar = ({ isOpen, toggleSidebar, selectedGroupIndex = 0, onSelectGroup,
{isOpen && ( -
+
= ({ isOpen }) => { + const [isHovering, setIsHovering] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [newNickname, setNewNickname] = useState(''); + const [uploadingAvatar, setUploadingAvatar] = useState(false); + const fileInputRef = useRef(null); + const userStore = useUserStore(); + + // 添加更新昵称的函数 + const updateNickname = async () => { + if (!newNickname.trim()) return; + + try { + setIsLoading(true); + const response = await request('/api/user/update', { + method: 'POST', + body: JSON.stringify({ nickname: newNickname.trim() }) + }); + const { data } = await response.json(); + console.log('更新用户信息', data); + //更新用户信息 + userStore.setUserInfo(data); + + setIsEditing(false); + } catch (error) { + console.error('更新昵称失败:', error); + } finally { + setIsLoading(false); + } + }; + + // 添加上传头像的处理函数 + const handleAvatarUpload = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + try { + setUploadingAvatar(true); + + // 1. 首先从后端获取上传 URL + const response = await request('/api/user/upload', { + method: 'POST' + }); + const { uploadURL, id } = await response.json(); + + // 2. 上传图片到 Cloudflare Images + const formData = new FormData(); + formData.append('file', file); // 使用 'file' 作为字段名 + + await fetch(uploadURL, { + method: 'POST', + body: formData + }); + + // 3. 更新用户头像信息 + const updateResponse = await request('/api/user/update', { + method: 'POST', + body: JSON.stringify({ avatar_url: id }) + }); + + const { data } = await updateResponse.json(); + userStore.setUserInfo(data); + + } catch (error) { + console.error('上传头像失败:', error); + } finally { + setUploadingAvatar(false); + } + }; + + if (!isOpen || !userStore.userInfo || !userStore.userInfo.status) return null; + + return ( +
setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + > + {/* 头像区域 */} +
+ +
!uploadingAvatar && fileInputRef.current?.click()} + > + {uploadingAvatar ? ( +
+
+
+ ) : userStore.userInfo?.avatar_url ? ( + avatar + ) : ( + + {getAvatarData(userStore.userInfo?.nickname || '我').text} + + )} +
+ {/* 头像hover效果 */} +
!uploadingAvatar && fileInputRef.current?.click()} + > + +
+
+ + {/* 用户信息区域 */} +
+
+ {isEditing ? ( +
+ setNewNickname(e.target.value)} + className="text-sm px-2 border rounded-md w-full" + placeholder={userStore.userInfo?.nickname || '输入新昵称'} + onKeyDown={(e) => { + if (e.key === 'Enter') updateNickname(); + if (e.key === 'Escape') setIsEditing(false); + }} + autoFocus + /> +
+ + +
+
+ ) : ( + <> + + {isLoading ? '加载中...' : userStore.userInfo?.nickname || '游客用户'} + + { + setIsEditing(true); + setNewNickname(userStore.userInfo?.nickname || ''); + }} + /> + + )} +
+ + {/* 退出登录按钮 */} + {!isEditing && ( +
{ + localStorage.removeItem('token'); + window.location.href = '/login'; + }} + > + + + 退出登录 + +
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx new file mode 100644 index 0000000..e7d9216 --- /dev/null +++ b/src/pages/chat/index.tsx @@ -0,0 +1,7 @@ +import ChatUI from './components/ChatUI'; + +export default function Chat() { + return ( + + ); +} diff --git a/src/pages/login/comonents/PhoneLogin.tsx b/src/pages/login/comonents/PhoneLogin.tsx new file mode 100644 index 0000000..d599124 --- /dev/null +++ b/src/pages/login/comonents/PhoneLogin.tsx @@ -0,0 +1,157 @@ +import React, { useState } from 'react'; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { request } from '@/utils/request'; +interface PhoneLoginProps { + onLogin: (phone: string, code: string) => void; +} + +const PhoneLogin: React.FC = ({ handleLoginSuccess }) => { + const [phone, setPhone] = useState(''); + const [code, setCode] = useState(''); + const [countdown, setCountdown] = useState(0); + const [isLoading, setIsLoading] = useState(false); + + // 发送验证码 + const handleSendCode = async () => { + if (!phone || !/^1[3-9]\d{9}$/.test(phone)) { + alert('请输入正确的手机号'); + return; + } + + //setIsLoading(true); + try { + const response = await fetch(`/api/sendcode`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ phone }), + }); + + if (!response.ok) { + throw new Error('发送验证码失败'); + } + + // 开始倒计时 + setCountdown(60); + const timer = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { + clearInterval(timer); + return 0; + } + return prev - 1; + }); + }, 1000); + + } catch (error) { + console.error('发送验证码失败:', error); + alert('发送验证码失败,请重试'); + } finally { + setIsLoading(false); + } + }; + + // 提交登录 + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!phone || !code) { + alert('请输入手机号和验证码'); + return; + } + + setIsLoading(true); + try { + const response = await request(`/api/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ phone, code }), + }); + + const data = await response.json(); + console.log(data); + + if (!response.ok) { + throw new Error(data.message || '登录失败'); + } + + //执行登录成功回调 + handleLoginSuccess(data.data.token); + + } catch (error) { + console.error('登录失败:', error); + alert(error instanceof Error ? error.message : '登录失败,请重试'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ {/* Logo */} +
+ botgroup.chat +
+ + +
+ 仅支持中国大陆手机号登录 +
+ +
+
+
+ +86 + setPhone(e.target.value)} + maxLength={11} + className="border-none focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none p-0" + /> +
+
+ +
+
+ setCode(e.target.value)} + maxLength={6} + className="border rounded-lg p-3 h-[46px] focus:border-[#ff6600] focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none" + /> + +
+
+ + +
+
+
+ ); +}; + +export default PhoneLogin; \ No newline at end of file diff --git a/src/pages/login/index.jsx b/src/pages/login/index.jsx new file mode 100644 index 0000000..aa4a2a4 --- /dev/null +++ b/src/pages/login/index.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import PhoneLogin from './comonents/PhoneLogin'; + +export default function Login() { + const navigate = useNavigate(); + + const handleLoginSuccess = (token) => { + localStorage.setItem('token', token); + navigate('/'); + }; + + React.useEffect(() => { + const isLogin = localStorage.getItem('token'); + if (isLogin) { + window.location.href = '/'; // 由于是 Vite 多页面,这里使用 window.location.href + } + }, []); + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/src/routes.tsx b/src/routes.tsx new file mode 100644 index 0000000..162afa5 --- /dev/null +++ b/src/routes.tsx @@ -0,0 +1,26 @@ +import { createBrowserRouter } from 'react-router-dom'; +import Login from './pages/login'; +import Chat from './pages/chat'; +import BasicLayout from './layouts/BasicLayout'; +import AuthGuard from './components/AuthGuard'; + +export const router = createBrowserRouter([ + { + path: '/login', + element: , + }, + { + path: '/', + element: ( + + + + ), + children: [ + { + path: '', + element: , + }, + ], + }, +]); \ No newline at end of file diff --git a/src/store/userStore.ts b/src/store/userStore.ts new file mode 100644 index 0000000..ff7ce01 --- /dev/null +++ b/src/store/userStore.ts @@ -0,0 +1,25 @@ +import { create } from 'zustand' + +interface UserInfo { + id: number; + phone: string; + nickname: string; + avatar_url: string | null; + status: number; +} + +interface UserStore { + userInfo: UserInfo; + setUserInfo: (userInfo: UserInfo) => void; +} + +export const useUserStore = create((set) => ({ + userInfo: { + id: 0, + phone: '', + nickname: '', + avatar_url: null, + status: 0 + }, + setUserInfo: (userInfo: UserInfo) => set({ userInfo }) +})); \ No newline at end of file diff --git a/src/utils/avatar.ts b/src/utils/avatar.ts new file mode 100644 index 0000000..c0ab36a --- /dev/null +++ b/src/utils/avatar.ts @@ -0,0 +1,97 @@ +import React from 'react'; + +interface User { + id: number | string; + name: string; + avatar?: string; +} + +interface AICharacter { + id: string; + name: string; + personality: string; + model: string; + avatar?: string; + custom_prompt?: string; + tags?: string[]; +} + +export const getAvatarData = (name: string) => { + const colors = ['#1abc9c', '#3498db', '#9b59b6', '#f1c40f', '#e67e22']; + const index = (name.charCodeAt(0) + (name.charCodeAt(1) || 0)) % colors.length; + return { + backgroundColor: colors[index], + text: name[0], + }; +}; + +// 获取单个头像的样式和内容 +export const getSingleAvatarData = (user: User | AICharacter) => { + if ('avatar' in user && user.avatar) { + return { + type: 'image', + src: user.avatar, + alt: user.name, + className: 'w-full h-full object-cover' + }; + } + const avatarData = getAvatarData(user.name); + return { + type: 'text', + text: avatarData.text, + className: 'w-full h-full flex items-center justify-center text-xs text-white font-medium', + style: { backgroundColor: avatarData.backgroundColor } + }; +}; + +// 获取半头像的样式和内容 +export const getHalfAvatarData = (user: User, isFirst: boolean) => { + if ('avatar' in user && user.avatar) { + return { + type: 'image', + src: user.avatar, + alt: user.name, + className: 'w-full h-full object-cover', + containerStyle: { + borderRight: isFirst ? '1px solid white' : 'none' + } + }; + } + const avatarData = getAvatarData(user.name); + return { + type: 'text', + text: avatarData.text, + className: 'w-1/2 h-full flex items-center justify-center text-xs text-white font-medium', + style: { + backgroundColor: avatarData.backgroundColor, + borderRight: isFirst ? '1px solid white' : 'none' + } + }; +}; + +// 获取四分之一头像的样式和内容 +export const getQuarterAvatarData = (user: User, index: number) => { + if ('avatar' in user && user.avatar) { + return { + type: 'image', + src: user.avatar, + alt: user.name, + className: 'w-full h-full object-cover', + containerStyle: { + borderRight: index % 2 === 0 ? '1px solid white' : 'none', + borderBottom: index < 2 ? '1px solid white' : 'none' + } + }; + } + const avatarData = getAvatarData(user.name); + return { + type: 'text', + text: avatarData.text, + className: 'aspect-square flex items-center justify-center text-[8px] text-white font-medium', + style: { + backgroundColor: avatarData.backgroundColor, + borderRight: index % 2 === 0 ? '1px solid white' : 'none', + borderBottom: index < 2 ? '1px solid white' : 'none' + } + }; +}; \ No newline at end of file diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..90d542b --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,38 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; +export async function request(url: string, options: RequestInit = {}) { + const token = localStorage.getItem('token'); + + const headers = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + + try { + const response = await fetch(`${API_BASE_URL}${url}`, { + ...options, + headers, + }); + + // 如果返回 401,清除 token 并跳转到登录页 + if (response.status === 401) { + localStorage.removeItem('token'); + window.location.href = '/login'; + throw new Error('Unauthorized'); + } + + if (!response.ok) { + throw new Error('Request failed'); + } + + return response; + } catch (error) { + // 如果是网络错误或其他错误,也可以处理 + console.error('Request error:', error); + throw error; + } +} \ No newline at end of file diff --git a/warngler.toml b/warngler.toml deleted file mode 100644 index 829ef71..0000000 --- a/warngler.toml +++ /dev/null @@ -1,3 +0,0 @@ -[observability] -enabled = true -head_sampling_rate = 1 # optional. default = 1. \ No newline at end of file diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..4812b4f --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,9 @@ +[[kv_namespaces]] +binding = "bgkv" +id = "cbd11575c3504e6bb043c1c250d2f7ed" +preview_id = "cbd11575c3504e6bb043c1c250d2f7ed" # 本地开发环境使用 + +[[d1_databases]] +binding = "bgdb" # available in your Worker on env.DB +database_name = "friends" +database_id = "f68e9fa5-4aea-45db-9516-2d7052e936fa" \ No newline at end of file