Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ Data Agent 反其道而行——**不引入任何向量检索**。语义层把
**前置依赖**:JDK 17+、Maven 3.9+、Node 18+、pnpm 8+、MySQL 8+、Python 3+(需安装 pandas、numpy、scipy)。

```bash
# 1. 建元数据库并初始化表结构
# 1. 建元数据库并初始化表结构(sql/ 目录下所有 .sql 文件均需执行)
mysql -u root -p -e "CREATE DATABASE data_agent CHARACTER SET utf8mb4;"
mysql -u root -p data_agent < sql/data_source.sql
for f in sql/*.sql; do mysql -u root -p data_agent < "$f"; done

# 2. 启动后端(默认端口 8080)
cd data-agent-backend
Expand Down
19 changes: 19 additions & 0 deletions data-agent-backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,25 @@
<artifactId>jsqlparser</artifactId>
<version>5.0</version>
</dependency>

<!-- JWT 签发/校验(HS256);唯一为登录引入的依赖,密码哈希走 JDK 标准库无新依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public enum ErrorCode {
/** 请求参数格式或取值非法,但没有更细分的业务错误码。 */
BAD_REQUEST("BAD_REQUEST", HttpStatus.BAD_REQUEST, "Invalid request parameters."),

/** 未认证:缺少或无效的凭证(token 缺失/过期/非法/用户已禁用)。 */
UNAUTHORIZED("UNAUTHORIZED", HttpStatus.UNAUTHORIZED, "Authentication is required."),

/** Bean Validation、绑定校验等字段级参数校验失败。 */
VALIDATION_FAILED("VALIDATION_FAILED", HttpStatus.BAD_REQUEST, "Invalid request parameters."),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2026 github.com/MaloneTalk
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* limitations under the License.
*/
package io.github.malonetalk.common;

/**
* 当前登录用户的轻量投影,由拦截器在同步请求线程放入 ThreadLocal。
*
* <p>仅承载鉴权必要字段(不含 password_hash),供管理/会话等同步 API 取用。Agent 异步链路
* (Reactor 弹性线程)拿不到此 ThreadLocal——权限轮次会改为通过 ToolCallContext 显式传 userId。
*/
public record UserContext(Integer userId, String username, String displayName) {

private static final ThreadLocal<UserContext> HOLDER = new ThreadLocal<>();

public static void set(UserContext context) {
HOLDER.set(context);
}

public static UserContext get() {
return HOLDER.get();
}

public static void clear() {
HOLDER.remove();
}

public static UserContext require() {
UserContext context = HOLDER.get();
if (context == null) {
throw new IllegalStateException("No user context bound to current thread.");
}
return context;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2026 github.com/MaloneTalk
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* limitations under the License.
*/
package io.github.malonetalk.config;

import io.github.malonetalk.entity.SysUser;
import io.github.malonetalk.mapper.SysUserMapper;
import io.github.malonetalk.util.PasswordUtil;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

/**
* 启动引导:sys_user 为空时创建初始 admin 账号。
*
* <p>初始密码取自环境变量({@code ADMIN_INIT_PASSWORD},经 {@code admin.init-password} 注入),
* 不写死进代码/SQL,避免进 git 历史。登录后应立即用改密码接口换掉。
*
* <p>未配置初始密码时启动失败(fail-closed)——避免无密码 admin 账号被静默创建。
* role_id 暂为 0:本轮无任何权限检查,admin 仅作为首个登录账号;
* 「不受权限限制」语义随权限轮次 sys_role(id=1) + @AdminOnly 一起生效。
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class AdminBootstrapRunner implements CommandLineRunner {

private final SysUserMapper sysUserMapper;

@Value("${admin.init-password:}")
private String adminInitPassword;

@Override
public void run(String... args) {
if (sysUserMapper.countAll() > 0) {
return;
}
if (adminInitPassword == null || adminInitPassword.isBlank()) {
throw new IllegalStateException(
"No user exists and admin.init-password (env ADMIN_INIT_PASSWORD) is not set. "
+ "Configure it before first startup to bootstrap the admin account.");
}
LocalDateTime now = LocalDateTime.now();
SysUser admin = new SysUser();
admin.setUsername("admin");
admin.setPasswordHash(PasswordUtil.hash(adminInitPassword));
admin.setDisplayName("管理员");
admin.setRoleId(0);
admin.setIdpType("LOCAL");
admin.setIdpUserId(null);
admin.setStatus(1);
admin.setCreateTime(now);
admin.setUpdateTime(now);
sysUserMapper.insert(admin);
log.info(
"Bootstrapped initial admin account (id={}, username=admin). Change its password"
+ " ASAP.",
admin.getId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2026 github.com/MaloneTalk
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* limitations under the License.
*/
package io.github.malonetalk.config;

import io.github.malonetalk.interceptor.AuthInterceptor;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/** 注册鉴权拦截器:覆盖 /api/**,仅放行登录接口与 Spring 错误页。 */
@Configuration
@AllArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

private final AuthInterceptor authInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/login", "/error");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2026 github.com/MaloneTalk
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* limitations under the License.
*/
package io.github.malonetalk.controller;

import io.github.malonetalk.common.ErrorCode;
import io.github.malonetalk.common.Result;
import io.github.malonetalk.common.UserContext;
import io.github.malonetalk.dto.ChangePasswordRequest;
import io.github.malonetalk.dto.LoginRequest;
import io.github.malonetalk.dto.LoginResponse;
import io.github.malonetalk.dto.UserInfoResponse;
import io.github.malonetalk.entity.SysUser;
import io.github.malonetalk.exception.BusinessException;
import io.github.malonetalk.mapper.SysUserMapper;
import io.github.malonetalk.util.JwtUtil;
import io.github.malonetalk.util.PasswordUtil;
import jakarta.validation.Valid;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 认证接口:登录 / 当前用户 / 改密码。
*
* <p>登录失败统一返回 401 + "用户名或密码错误",避免用户名枚举;账号禁用单独提示。
*/
@RestController
@Slf4j
@AllArgsConstructor
@RequestMapping("/api/auth")
public class AuthController {

private static final String BAD_CREDENTIALS = "用户名或密码错误";

private final SysUserMapper sysUserMapper;
private final JwtUtil jwtUtil;

@PostMapping("/login")
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
SysUser user = sysUserMapper.selectByUsername(request.username());
// 用户不存在、外部身份源(password_hash 为空)、密码不匹配:统一文案,避免枚举用户名。
if (user == null
|| user.getPasswordHash() == null
|| !PasswordUtil.verify(request.password(), user.getPasswordHash())) {
throw BusinessException.of(ErrorCode.UNAUTHORIZED, BAD_CREDENTIALS);
}
if (user.getStatus() == null || user.getStatus() != 1) {
throw BusinessException.of(ErrorCode.UNAUTHORIZED, "账号已禁用,请联系管理员");
}
String token = jwtUtil.generate(user.getId());
UserInfoResponse info =
new UserInfoResponse(user.getId(), user.getUsername(), user.getDisplayName());
return Result.success(new LoginResponse(token, info));
}

@GetMapping("/me")
public Result<UserInfoResponse> me() {
UserContext context = UserContext.require();
return Result.success(
new UserInfoResponse(context.userId(), context.username(), context.displayName()));
}

@PostMapping("/change-password")
public Result<Boolean> changePassword(@Valid @RequestBody ChangePasswordRequest request) {
Integer userId = UserContext.require().userId();
SysUser user = sysUserMapper.selectById(userId);
if (user == null
|| user.getPasswordHash() == null
|| !PasswordUtil.verify(request.oldPassword(), user.getPasswordHash())) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "旧密码不正确");
}
sysUserMapper.updatePassword(
userId, PasswordUtil.hash(request.newPassword()), LocalDateTime.now());
return Result.success(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2026 github.com/MaloneTalk
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* limitations under the License.
*/
package io.github.malonetalk.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record ChangePasswordRequest(
@NotBlank(message = "oldPassword 不能为空") String oldPassword,
@NotBlank(message = "newPassword 不能为空")
@Size(min = 6, max = 64, message = "newPassword 长度需在 6-64 之间")
String newPassword) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2026 github.com/MaloneTalk
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* limitations under the License.
*/
package io.github.malonetalk.dto;

import jakarta.validation.constraints.NotBlank;

public record LoginRequest(
@NotBlank(message = "username 不能为空") String username,
@NotBlank(message = "password 不能为空") String password) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2026 github.com/MaloneTalk
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* limitations under the License.
*/
package io.github.malonetalk.dto;

/** 登录成功响应:token + 用户信息(一次往返,前端无需再调 me)。 */
public record LoginResponse(String token, UserInfoResponse user) {}
Loading
Loading