Compare commits
10 Commits
80ee3ca5a5
...
74c09c1d81
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74c09c1d81 | ||
|
|
f8702e80f2 | ||
|
|
7b96b16bbf | ||
|
|
b155df6567 | ||
|
|
8c85fe943c | ||
|
|
3be454ce38 | ||
|
|
9a7549d18c | ||
|
|
0d4a938c00 | ||
|
|
c782dea322 | ||
|
|
ddc4ac26bc |
65
AGENTS.md
Normal file
65
AGENTS.md
Normal file
@@ -0,0 +1,65 @@
|
||||
## 核心目标
|
||||
|
||||
基于提供的工程模板架构,智能解析用户的JSON格式需求,自动生成完整的**node应用及移动端Web应用**。确保生成的代码具备以下特性:
|
||||
- **完整性**: 包含前后端完整功能实现 (也可能没有前端,是纯后端项目)
|
||||
- **可运行性**: 代码即拆即用,无需额外修改
|
||||
- **规范性**: 严格遵循模板的技术栈和编码规范
|
||||
- **部署就绪**: 自动构建生成可直接部署的工程产物
|
||||
|
||||
## 实现策略
|
||||
|
||||
### 1. 前端实现策略
|
||||
- 基于响应式设计的单页应用(SPA)
|
||||
- 专为移动端优化,**采用单一入口点架构**,所有功能模块通过组件化方式在同一页面内动态加载和切换,确保用户体验的连贯性和加载性能的优化
|
||||
- **主题适配**: 确保组件支持亮色/暗色主题
|
||||
- **错误展示**:前端页面需要有统一的错误展示,用于显示API请求失败或其他操作错误的信息
|
||||
- 不需要实现登录页,默认访问应用的用户都是已登录状态
|
||||
- 所有请求统一使用 `/src/api/index` 中的 `api` 方法进行调用,因为已经内置了必要的请求头封装
|
||||
- 如果有需要,使用 `/src/api/user` 中的 `getUserInfo` 方法获取当前用户信息
|
||||
- 如果有需要,使用 `/src/api/user` 中的 `getGroupUsers` 方法获取全部用户信息列表
|
||||
|
||||
### 2. 后端实现策略
|
||||
- 所有接口以 `/api` 为前缀
|
||||
- 已经为项目封装了当前用户信息获取的逻辑,提供了中间件,`userInfoMiddleware`,见: `src/middleware/auth.ts`
|
||||
- 已经为项目封装了当前群组所有用户信息列表的获取逻辑,提供了中间件,`groupInfoMiddleware`,见: `src/middleware/auth.ts`
|
||||
|
||||
### 3. 开发规范
|
||||
- **文件命名**: 使用kebab-case命名文件和文件夹
|
||||
- **组件命名**: React组件使用PascalCase
|
||||
- **API设计**: 遵循RESTful设计原则,返回统一的JSON格式
|
||||
- **类型定义**: 为所有数据结构定义TypeScript类型
|
||||
- **代码组织**: 保持清晰的目录结构和模块化设计
|
||||
|
||||
## 任务执行流程
|
||||
|
||||
1. **需求分析**: 仔细解析JSON需求,识别核心功能点
|
||||
2. **架构设计**: 基于模板结构规划实现方案
|
||||
3. **数据建模**: 根据data_models设计数据库表结构
|
||||
4. **API设计**: 根据api_endpoints设计RESTful接口
|
||||
5. **前端开发**: 实现页面组件、路由、状态管理
|
||||
6. **后端开发**: 实现数据模型、API端点、业务逻辑
|
||||
7. **build**: 调用`build`命令生成可部署的工程产物
|
||||
* 如果报错则修正错误后再次build,直到成功
|
||||
* 禁止修改build命令
|
||||
|
||||
## 用户需求修改
|
||||
|
||||
请注意,用户可能会多次修改需求,**每次修改后用户会重新给你最新的完整的JSON需求描述**。
|
||||
|
||||
用户每次修改后,请在现有代码基础上**针对JSON中较上一次有变动的地方重点编辑实现**,确保所有变更都被正确反映。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 严格遵循现有的项目结构和编码规范
|
||||
- 确保前端页面适配移动端访问(响应式)
|
||||
- 保持API响应格式的一致性
|
||||
- 及时更新相关的TypeScript类型定义
|
||||
- 确保所有新增的页面都能正确路由和访问
|
||||
|
||||
---
|
||||
|
||||
请基于以上规范和用户JSON需求,生成完整的、可运行的、符合输出格式要求的代码实现。
|
||||
|
||||
接下来我会输出用户JSON需求,其中前端需求放在 `webapp_requirements`, 后端需求放在 `node_backend_requirements`。
|
||||
|
||||
> 如果 webapp_requirements 中 is_needed = false,代表不需要前端,只需要生成后端代码
|
||||
29
packages/client/src/api/user.ts
Normal file
29
packages/client/src/api/user.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import api from "@/api/index.ts";
|
||||
import {getGroupId} from "@/utils/auth.ts";
|
||||
|
||||
export type UserInfo = {
|
||||
userId: number;
|
||||
nickname: string;
|
||||
avatarUrl: string;
|
||||
gender: 'MALE' | 'FEMALE' | 'UNKNOWN';
|
||||
nimToken: string; // NetEase Cloud Communication token
|
||||
nimAccountId: string; // NetEase Cloud Communication account ID
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的信息
|
||||
*/
|
||||
export const getUserInfo = async (): Promise<UserInfo> => {
|
||||
return await api.post<UserInfo>('https://egret.byteawake.com/api/user/info');
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取群组内所有用户的信息
|
||||
*/
|
||||
export const getGroupUsers = async (): Promise<UserInfo[]> => {
|
||||
return await api.post<UserInfo[]>('https://egret.byteawake.com/api/group/members', {
|
||||
groupId: Number(getGroupId()),
|
||||
});
|
||||
};
|
||||
@@ -67,14 +67,22 @@ export const clearGroupId = (): void => {
|
||||
export const handleAuthTokenAndGroupIdFromUrl = (): void => {
|
||||
const tokenFromUrl = getAuthTokenFromUrl();
|
||||
const groupIdFromUrl = getGroupIdFromUrl();
|
||||
if (tokenFromUrl && groupIdFromUrl) {
|
||||
saveAuthToken(tokenFromUrl);
|
||||
saveGroupId(groupIdFromUrl);
|
||||
|
||||
// 从URL中移除authToken参数
|
||||
let updated = false;
|
||||
if (tokenFromUrl) {
|
||||
saveAuthToken(tokenFromUrl);
|
||||
updated = true;
|
||||
}
|
||||
if (groupIdFromUrl) {
|
||||
saveGroupId(groupIdFromUrl);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// Clean URL if we consumed any param
|
||||
if (updated) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('authToken');
|
||||
url.searchParams.delete('groupId');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
window.history.replaceState(null, '', url.toString());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.11.0",
|
||||
"compression": "^1.7.5",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2",
|
||||
@@ -21,6 +22,7 @@
|
||||
"sqlite3": "^5.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.9.36",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.13",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Express } from "express";
|
||||
import {userInfoMiddleware, groupInfoMiddleware} from "@/middleware/auth";
|
||||
|
||||
export const createApis = (app: Express) => {
|
||||
app.get("/api/test", async (req, res) => {
|
||||
@@ -8,5 +9,16 @@ export const createApis = (app: Express) => {
|
||||
});
|
||||
});
|
||||
|
||||
// 同时使用用户信息和群组信息中间件
|
||||
app.get("/api/user-group/info", userInfoMiddleware, groupInfoMiddleware, async (req, res) => {
|
||||
res.status(200).json({
|
||||
message: "success",
|
||||
data: {
|
||||
user: req.userInfo,
|
||||
group: req.groupInfo
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/** add apis here */
|
||||
};
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import path from "path";
|
||||
import { Sequelize } from "sequelize";
|
||||
|
||||
export const sequelize = new Sequelize({
|
||||
dialect: "sqlite",
|
||||
storage: path.resolve(__dirname, process.env.DB_PATH || "./data.sqlite"),
|
||||
});
|
||||
import { sequelize } from "./instance";
|
||||
|
||||
async function initializeDatabase() {
|
||||
try {
|
||||
|
||||
7
packages/server/src/database/instance.ts
Normal file
7
packages/server/src/database/instance.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import path from "path";
|
||||
import { Sequelize } from "sequelize";
|
||||
|
||||
export const sequelize = new Sequelize({
|
||||
dialect: "sqlite",
|
||||
storage: path.resolve(__dirname, process.env.DB_PATH || "./data.sqlite"),
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import expressOasGenerator, {
|
||||
dotenv.config({ path: path.resolve(__dirname, "../../../.env") });
|
||||
|
||||
import "./database";
|
||||
import { sequelize } from "@/database/instance";
|
||||
import { createApis } from "./api";
|
||||
|
||||
const port = process.env.PORT || 3005;
|
||||
@@ -50,6 +51,12 @@ app.get("/v3/api-docs", async (req, res) => {
|
||||
});
|
||||
|
||||
const host = "0.0.0.0";
|
||||
app.listen(Number(port), host, () => {
|
||||
app.listen(Number(port), host, async () => {
|
||||
try {
|
||||
await sequelize.sync({ alter: true });
|
||||
console.log("[server]: sequelize.sync() executed");
|
||||
} catch (e) {
|
||||
console.error("Failed to sync database:", e);
|
||||
}
|
||||
console.log(`[server]: Server is running at http://${host}:${port}`);
|
||||
});
|
||||
|
||||
178
packages/server/src/middleware/auth.ts
Normal file
178
packages/server/src/middleware/auth.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import {Request, Response, NextFunction} from "express";
|
||||
import axios from "axios";
|
||||
|
||||
export type UserInfo = {
|
||||
userId: number;
|
||||
nickname: string;
|
||||
avatarUrl: string;
|
||||
gender: 'MALE' | 'FEMALE' | 'UNKNOWN';
|
||||
nimToken: string; // NetEase Cloud Communication token
|
||||
nimAccountId: string; // NetEase Cloud Communication account ID
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
userInfo?: UserInfo | null;
|
||||
groupInfo?: {
|
||||
groupId: number;
|
||||
users: UserInfo[];
|
||||
} | null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Bearer token from request header
|
||||
*/
|
||||
export const extractTokenFromHeader = (authHeader: string | undefined): string | null => {
|
||||
if (!authHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = authHeader.split(" ");
|
||||
if (parts.length !== 2 || parts[0] !== "Bearer") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parts[1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user information by token
|
||||
*/
|
||||
export const getUserInfoByToken = async (token: string): Promise<any> => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
"https://egret.byteawake.com/api/user/info",
|
||||
{}, // 请求体数据,这里为空对象
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
timeout: 10000, // 10 second timeout
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
// API returned error response
|
||||
throw new Error(`Failed to get user information: ${error.response.status} ${error.response.statusText}`);
|
||||
} else if (error.request) {
|
||||
// Request was sent but no response received
|
||||
throw new Error("Failed to get user information: timeout or network error");
|
||||
} else {
|
||||
// Other errors
|
||||
throw new Error(`Failed to get user information: ${error.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* User information middleware
|
||||
* Automatically extract token from request header and get user information, store result in req.userInfo
|
||||
*/
|
||||
export const userInfoMiddleware = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = extractTokenFromHeader(authHeader);
|
||||
|
||||
if (!token) {
|
||||
res.status(401).json({
|
||||
error: "No valid access token provided",
|
||||
message: "Missing Bearer token in Authorization header",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get user information
|
||||
const userInfoRes = await getUserInfoByToken(token);
|
||||
req.userInfo = userInfoRes.code === 200 ? userInfoRes.data : null;
|
||||
|
||||
next();
|
||||
} catch (error: any) {
|
||||
res.status(401).json({
|
||||
error: "User authentication failed",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//////// group
|
||||
|
||||
/**
|
||||
* Get group users
|
||||
*/
|
||||
export const getGroupUsers = async (groupId: number, token: string): Promise<any> => {
|
||||
try {
|
||||
const response = await axios.post<UserInfo[]>(
|
||||
"https://egret.byteawake.com/api/group/members",
|
||||
{groupId},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
timeout: 10000, // 10 second timeout
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
// API returned error response
|
||||
throw new Error(`Failed to get user information: ${error.response.status} ${error.response.statusText}`);
|
||||
} else if (error.request) {
|
||||
// Request was sent but no response received
|
||||
throw new Error("Failed to get user information: timeout or network error");
|
||||
} else {
|
||||
// Other errors
|
||||
throw new Error(`Failed to get user information: ${error.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* group information middleware
|
||||
*/
|
||||
export const groupInfoMiddleware = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = extractTokenFromHeader(authHeader);
|
||||
const groupId = req.headers.groupid;
|
||||
|
||||
if (token && groupId) {
|
||||
try {
|
||||
const usersRes = await getGroupUsers(Number(groupId), token);
|
||||
const users: UserInfo[] = usersRes.code === 200 ? usersRes.data : [];
|
||||
req.groupInfo = {
|
||||
groupId: Number(groupId),
|
||||
users,
|
||||
}
|
||||
} catch (error) {
|
||||
// If getting user information fails, do not block the request from continuing, but do not set userInfo
|
||||
console.warn("Failed to get group user information:", error);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error: any) {
|
||||
res.status(400).json({
|
||||
error: "Get Group Users failed",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
0
packages/server/src/models/.gitkeep
Normal file
0
packages/server/src/models/.gitkeep
Normal file
0
packages/server/src/services/.gitkeep
Normal file
0
packages/server/src/services/.gitkeep
Normal file
Reference in New Issue
Block a user