checkpoint: 重写 langchain agent 前的快照

包含未合入 main 的工作:
- CAS 单点登录接入 (CasSecurityConfig + 4 个 CAS 类)
- LoginController 改 POST + body, 修 URL 密码暴露
- chat_test.py 修 texts→text typo
- 前端外部系统侧边栏 + login API POST
- gateway-nginx.conf

下一步: 从这个分支拉 feat/langgraph-rewrite 做 langchain-chat agent 重写
回滚点: git checkout backup/pre-langgraph

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 14:58:53 +08:00
parent f8e8017434
commit 911f7adee6
17 changed files with 647 additions and 10 deletions

View File

@@ -191,6 +191,17 @@
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- CAS 单点登录 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>

View File

@@ -7,6 +7,7 @@ import com.inspur.llm.chat.base.security.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@@ -27,6 +28,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
* Copyright Ⓒ 2024 Inspur Corporation Limited All rights reserved.
*/
@Configuration
@Order(100)
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

View File

@@ -0,0 +1,37 @@
package com.inspur.llm.chat.base.security.cas;
import com.inspur.llm.chat.base.security.JwtTokenUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* CAS 登录成功 → 生成 JWT → 重定向前端登录页(带 cas_token
*/
@Slf4j
@Component
public class CasLoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private CasProperties casProperties;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
log.info("CAS 登录成功: {}", authentication.getName());
String token = JwtTokenUtils.generateToken(authentication);
String redirect = casProperties.getFrontEndUrl()
+ "/#/login?cas_token=" + URLEncoder.encode(token, StandardCharsets.UTF_8.name());
response.sendRedirect(redirect);
}
}

View File

@@ -0,0 +1,24 @@
package com.inspur.llm.chat.base.security.cas;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* CAS 单点登录配置(绑定 application.yml 中的 security.cas.*
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "security.cas")
public class CasProperties {
private boolean enabled = false;
private String serverHost;
private String serverLogin;
private String serverLogout;
private String appLogin;
private String appLogout;
private String appKey;
private String appSecret;
private boolean httpsFlag = false;
private String frontEndUrl;
}

View File

@@ -0,0 +1,101 @@
package com.inspur.llm.chat.base.security.cas;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
/**
* CAS 单点登录 Security 配置(仅处理 /cas/** 路径,与 WebSecurityConfig 共存)。
*
* 流程:
* /cas/login无 ticket → 触发 CasEntryPoint → 302 到 CAS 登录页
* /cas/login?ticket=xxx → CasAuthenticationFilter 验证 → 成功 → CasLoginSuccessHandler 生成 JWT → 302 前端
*/
@Configuration
@Order(1)
public class CasSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CasProperties casProperties;
@Autowired
private AuthenticationUserDetailsService<CasAssertionAuthenticationToken> casUserDetailsService;
@Autowired
private AuthenticationSuccessHandler casLoginSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/cas/**")
.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(casAuthenticationEntryPoint())
.and()
.addFilter(casAuthenticationFilter());
http.headers().frameOptions().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(casAuthenticationProvider());
}
@Bean
public ServiceProperties casServiceProperties() {
ServiceProperties sp = new ServiceProperties();
sp.setService(casProperties.getFrontEndUrl() + "/chat_web_backend" + casProperties.getAppLogin());
sp.setSendRenew(false);
sp.setAuthenticateAllArtifacts(true);
return sp;
}
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint();
ep.setLoginUrl(casProperties.getServerLogin());
ep.setServiceProperties(casServiceProperties());
return ep;
}
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
filter.setFilterProcessesUrl(casProperties.getAppLogin());
filter.setAuthenticationSuccessHandler(casLoginSuccessHandler);
filter.setServiceProperties(casServiceProperties());
// 仅在 URL 含 ticket 时才尝试认证;无 ticket → 让 ExceptionTranslationFilter 触发 EntryPoint 重定向 CAS
final String appLogin = casProperties.getAppLogin();
filter.setRequiresAuthenticationRequestMatcher(req ->
appLogin.equals(req.getServletPath()) && req.getParameter("ticket") != null
);
return filter;
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setAuthenticationUserDetailsService(casUserDetailsService);
provider.setServiceProperties(casServiceProperties());
provider.setTicketValidator(new Cas20ServiceTicketValidator(casProperties.getServerHost()));
provider.setKey("casAuthProviderKey");
return provider;
}
}

View File

@@ -0,0 +1,74 @@
package com.inspur.llm.chat.base.security.cas;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.inspur.llm.chat.base.security.UserDetail;
import com.inspur.llm.chat.gpt.mapper.SysUserMapper;
import com.inspur.llm.chat.gpt.mapper.UserMapper;
import com.inspur.llm.chat.gpt.pojo.entity.SysUser;
import com.inspur.llm.chat.gpt.pojo.entity.User;
import com.inspur.llm.chat.gpt.pojo.vo.SysUserVO;
import com.inspur.llm.chat.gpt.pojo.vo.UserVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.HashSet;
/**
* 把 CAS 返回的 principal 映射到本地用户:
* 1) sys_user.cas_username
* 2) gpt_user.cas_username
* 3) gpt_user.telfallback
* 4) 都没找到 → 抛 UsernameNotFoundException
*/
@Slf4j
@Service
public class CasUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) {
String casPrincipal = token.getName();
log.info("CAS 用户加载: principal={}", casPrincipal);
SysUser sysUser = sysUserMapper.selectOne(
Wrappers.<SysUser>lambdaQuery().eq(SysUser::getCasUsername, casPrincipal).last("limit 1"));
if (sysUser != null) {
log.info("CAS 命中 sys_user id={}", sysUser.getId());
SysUserVO vo = new SysUserVO();
BeanUtils.copyProperties(sysUser, vo);
return new UserDetail(vo, new HashSet<>());
}
User user = userMapper.selectOne(
Wrappers.<User>lambdaQuery().eq(User::getCasUsername, casPrincipal).last("limit 1"));
if (user != null) {
log.info("CAS 命中 gpt_user (cas_username) id={}", user.getId());
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo);
return new UserDetail(vo, new HashSet<>());
}
user = userMapper.selectOne(
Wrappers.<User>lambdaQuery().eq(User::getTel, casPrincipal).last("limit 1"));
if (user != null) {
log.info("CAS 命中 gpt_user (tel fallback) id={}", user.getId());
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo);
return new UserDetail(vo, new HashSet<>());
}
log.warn("CAS 用户未在战知库中找到: {}", casPrincipal);
throw new UsernameNotFoundException("CAS 用户未绑定战知账号: " + casPrincipal);
}
}

View File

@@ -22,7 +22,8 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
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;
@@ -54,8 +55,8 @@ public class LoginController {
* @date: 2024/1/9
* @version: 1.0.0
*/
@GetMapping("/api/oauth/token")
public ResponseInfo<Oauth2Token> login(LoginCommand command) {
@PostMapping("/api/oauth/token")
public ResponseInfo<Oauth2Token> login(@RequestBody LoginCommand command) {
UsernamePasswordAuthenticationToken authenticationToken;
if (UserTypeEnum.TEL.getValue().equals(command.getLoginType())) {
UserVO user = userService.loginByTel(command.getTel(), command.getPassword(), command.getCode()).getData();

View File

@@ -70,6 +70,11 @@ public class SysUser extends BaseEntity {
*/
private String email;
/**
* CAS 单点登录用户名(钢研统一身份)
*/
private String casUsername;
/**
* 性别 0->女;1->男
*/

View File

@@ -74,6 +74,11 @@ public class User extends BaseEntity {
*/
private String unionid;
/**
* CAS 单点登录用户名(钢研统一身份)
*/
private String casUsername;
/**
* 登录ip
*/

View File

@@ -92,4 +92,17 @@ xss:
# 词向量模型名称
embedding-model-name: bge_m3
# CAS 单点登录配置
security:
cas:
enabled: true
server-host: http://192.168.203.20:8180
server-login: ${security.cas.server-host}/login
server-logout: ${security.cas.server-host}/logout
app-login: /cas/login
app-logout: /cas/logout
app-key: 8UyEQdr6L6FfAH3D
app-secret: Ely1U6Uv2Nf8APQSfZdg1epSbQ6ORKwr
front-end-url: http://192.168.203.8:3000

View File

@@ -8,9 +8,9 @@ export function fetchChatConfig<T>() {
})
}
// 登录
// 登录POST + body避免密码出现在 URL/access log
export function fetchVerify<T>(data: object) {
return get<T>({
return post<T>({
url: '/app/api/oauth/token',
data,
})

View File

@@ -56,6 +56,14 @@
{{ loginTip }}
</el-button>
</el-form-item>
<el-form-item>
<el-button
style="width: 100%; background: #FF2500; color: #fff; border-color: #FF2500;"
@click="goCasLogin"
>
通过统一身份登录
</el-button>
</el-form-item>
</el-form>
</div>
</div>
@@ -139,6 +147,22 @@ const handleVerify = async () => {
}
};
// 跳后端 CAS 入口;后端 302 跳 CAS 服务器
const goCasLogin = () => {
window.location.href = `/chat_web_backend/cas/login`;
};
// 处理 CAS 回调URL 含 cas_token 参数 → 存 token 后跳首页
onMounted(() => {
const params = new URLSearchParams(window.location.search);
const casToken = params.get("cas_token");
if (casToken) {
authStore.setToken(casToken);
window.history.replaceState({}, document.title, window.location.pathname + window.location.hash.split("?")[0]);
router.push("/chat");
}
});
</script>
<style scoped lang="scss">
.loginPage {

13
images/1.svg Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 31.79 26.46">
<g id="_图层_1-2" data-name="图层 1">
<g>
<path d="M4.91,6.25c.18,0,.74-.6,.87-.71,.17,0,.65-.53,.79-.65,.32-.26,.63-.52,.95-.78,.56-.46,1.12-.92,1.69-1.38,.15,0,.72,1.25,.84,1.45,.45,.78,.9,1.56,1.35,2.35l-1.69,1.36c-1.6-.55-3.19-1.1-4.79-1.65Z"/>
<path d="M13.06,5.43c-.5-1.29,.38-3.57,1.27-4.51C15.49-.3,17.51,0,19.03,.14c0,0-2.48,2.93-3.55,5.08-.82-.01-1.63,.02-2.42,.21Z"/>
<path d="M17.66,4.96L25.6,.8s2.68,1.49,3.63,2.72c-1.4,.41-9.1,2.6-9.37,2.77-.88-.55-2.2-1.33-2.2-1.33Z"/>
<polygon points="20.85 7.78 31.75 7.86 31.79 12.24 20.74 9.98 20.85 7.78"/>
<path d="M19.56,13.33c-.17,0-.29,.12-.57,.33-.62,.47-1.24,.94-1.86,1.41,1.64,1.94,3.21,3.94,4.81,5.91,.62,.77,1.23,1.55,1.87,2.3,.97-.59,1.7-1.47,2.6-2.15,.32-.25,1.43-.76,1.45-1.22,0-.13-.86-.71-.99-.82-.42-.34-.84-.69-1.26-1.03-1.49-1.22-2.97-2.43-4.47-3.63-.4-.32-.81-.71-1.25-.98-.14-.08-.24-.12-.32-.13Z"/>
<path d="M14.11,26.46l-.16-17.73c0-.06-2.36,.1-2.45,.11-1.79,.15-3.65,.33-5.45,.55-1.43,.18-3.03,.26-4.27,1.12C.46,11.43-.13,13.17,.02,14.73l.17,.55s3.27-1.55,3.58-1.7c.63-.3,1.26-.58,1.9-.84,.54-.22,1.32-.65,1.9-.64-.17,.37-.57,.73-.84,1.05l-1.51,1.81-3.24,3.89c-.48,.57-1.02,.92-.79,1.69,.43,1.47,1.54,2.63,2.77,3.5,1.21-1.91,4.39-7.62,6.15-10.35l-.89,12.76h4.89Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

52
images/2.svg Normal file
View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 162.4 26.46">
<g id="_图层_1-2" data-name="图层 1">
<g>
<g>
<path d="M119.14,11.98h-17.36v-1.49h18.11v.73c0,.42-.34,.75-.75,.75Z"/>
<path d="M109.69,12.44s3.91,7.74,7.82,10.54c.39,.28,.92,.27,1.31-.01,1.96-1.45,2.1-1.86,2.1-1.86,0,0-3.8-1.82-9.5-9.58-1.73,.41-1.73,.91-1.73,.91Z"/>
<path d="M109.23,5.22h3.14s.24,10.69-8.85,17.49c-.4,.3-.93,.37-1.39,.17-.52-.23-.99-.57-.99-.57,0,0,7.02-4.62,8.09-17.09Z"/>
</g>
<g>
<path d="M4.91,6.25c.18,0,.74-.6,.87-.71,.17,0,.65-.53,.79-.65,.32-.26,.63-.52,.95-.78,.56-.46,1.12-.92,1.69-1.38,.15,0,.72,1.25,.84,1.45,.45,.78,.9,1.56,1.35,2.35l-1.69,1.36c-1.6-.55-3.19-1.1-4.79-1.65Z"/>
<path d="M13.06,5.43c-.5-1.29,.38-3.57,1.27-4.51C15.49-.3,17.51,0,19.03,.14c0,0-2.48,2.93-3.55,5.08-.82-.01-1.63,.02-2.42,.21Z"/>
<path d="M17.66,4.96L25.6,.8s2.68,1.49,3.63,2.72c-1.4,.41-9.1,2.6-9.37,2.77-.88-.55-2.2-1.33-2.2-1.33Z"/>
<polygon points="20.85 7.78 31.75 7.86 31.79 12.24 20.74 9.98 20.85 7.78"/>
<path d="M19.56,13.33c-.17,0-.29,.12-.57,.33-.62,.47-1.24,.94-1.86,1.41,1.64,1.94,3.21,3.94,4.81,5.91,.62,.77,1.23,1.55,1.87,2.3,.97-.59,1.7-1.47,2.6-2.15,.32-.25,1.43-.76,1.45-1.22,0-.13-.86-.71-.99-.82-.42-.34-.84-.69-1.26-1.03-1.49-1.22-2.97-2.43-4.47-3.63-.4-.32-.81-.71-1.25-.98-.14-.08-.24-.12-.32-.13Z"/>
<path d="M14.11,26.46l-.16-17.73c0-.06-2.36,.1-2.45,.11-1.79,.15-3.65,.33-5.45,.55-1.43,.18-3.03,.26-4.27,1.12C.46,11.43-.13,13.17,.02,14.73l.17,.55s3.27-1.55,3.58-1.7c.63-.3,1.26-.58,1.9-.84,.54-.22,1.32-.65,1.9-.64-.17,.37-.57,.73-.84,1.05l-1.51,1.81-3.24,3.89c-.48,.57-1.02,.92-.79,1.69,.43,1.47,1.54,2.63,2.77,3.5,1.21-1.91,4.39-7.62,6.15-10.35l-.89,12.76h4.89Z"/>
</g>
<path d="M46.69,9.69v-1.09h-3.34v-3.7h-2.81V13.08h-2.44v7.1h8.57v-5.93c0-.61-.5-1.11-1.11-1.11h-2.21v-3.45h3.34Zm-3.32,4.42c.26,0,.46,.21,.46,.47v4.22c0,.26-.2,.46-.46,.46h-1.89c-.26,0-.47-.2-.47-.46v-4.22c0-.26,.21-.47,.47-.47h1.89Z"/>
<path d="M47.63,4.92h2.83l3.11,16.59h3.2v1.19h-3.69c-.88,0-1.7-.42-2.22-1.13l-.05-.07c-.15-.21-.25-.44-.3-.69l-2.88-15.89Z"/>
<path d="M46.25,11.17s3.66,.11,8.06-1.4c.4-.01,.8-.01,1.2,0,.2,0,.4,0,.59,0,.1,0,.2,0,.3,0,.09,0,.18-.01,.27,0,.07,.01,.13,.05,.16,.12,.01,.04,0,.06-.03,.08-.55,.41-1.13,.76-1.78,1-.02,0-.04,.01-.06,.02-.23,.08-.46,.15-.69,.21-.32,.09-.64,.16-.96,.23-.42,.09-.85,.16-1.27,.23-.54,.08-1.07,.15-1.61,.21-.66,.07-1.32,.12-1.99,.17-.34,.02-.68,.04-1.03,.06-.1,0-1.1,.06-1.1,.04-.11-.83-.06-.96-.06-.96Z"/>
<polygon points="51.95 6.57 53.16 5.28 55.75 6.93 54.1 8.96 51.95 6.57"/>
<path d="M53.99,12.13l2.92,1.18s-3.85,7.29-10.4,9.3c-1.16-1.13-1.27-1.51-1.27-1.51,0,0,4.76-2.01,8.75-8.97Z"/>
<path d="M61.29,4.5l1.16,.36,1.99,.63s-3.52,5.89-3.99,6.11-1.84-.61-1.84-.61l2.68-6.5Z"/>
<path d="M62.9,8.85c-.07,.01-.05,2.52-.06,2.78-.11,3.63-.83,7.26-3.6,9.85-.04,.04-.31,.33-.39,.25l1.13,1.07s3.29-1.64,5.12-6.3c.85-2.17,1.49-5.04,1.18-7.98,0,0-3.39,.33-3.39,.33Z"/>
<path d="M67.33,21.71l2.48-1.6s-3.25-3.77-4.65-3.61-.77,1.32-.77,1.32l2.94,3.88Z"/>
<rect x="59.13" y="13.28" width="9.91" height="1.29"/>
<rect x="62.22" y="7.67" width="6.47" height="1.21"/>
<path d="M76.24,6.59h-6.73v14.76h8.45V8.31c0-.95-.77-1.72-1.72-1.72Zm-1.14,13.02c0,.23-.18,.42-.41,.42h-1.9c-.23,0-.41-.19-.41-.42V8.11c0-.23,.18-.41,.41-.41h1.9c.23,0,.41,.18,.41,.41v11.5Z"/>
<rect x="88.14" y="12.65" width="2.81" height="2.64"/>
<path d="M122.59,8.83h6.72v.4c0,.53-.43,.97-.97,.97h-5.75v-1.36h0Z"/>
<path d="M124.65,4.9h2.52V22.22c0,.33-.27,.6-.6,.6h-1.31c-.33,0-.6-.27-.6-.6V4.9h0Z"/>
<rect x="129.08" y="6.29" width="12.55" height="1.03"/>
<rect x="144.94" y="5.74" width="9.72" height="1.03"/>
<rect x="144.16" y="10.2" width="11.03" height="1.03"/>
<rect x="146.47" y="17.28" width="13.48" height="1.03"/>
<path d="M144.02,20.69h18.37v.45c0,.43-.35,.79-.79,.79h-17.58v-1.24h0Z"/>
<rect x="129.08" y="17.34" width="12.55" height="1.03"/>
<path d="M131.16,4.89l.58,4.47c0,.06,.07,.09,.12,.07l2.4-.88c.06-.02,.09-.09,.07-.15l-1.29-3.51h-1.87Z"/>
<path d="M136.51,4.9l-.07,1.44-.54,2.41,1.13,.7s2.31-1.43,2.3-4.56h-2.82Z"/>
<path d="M133.33,15.84s.72,3.52-4.57,6.06c.11,.83,.33,.83,1.1,.72s3.36-.83,5.28-3.14,1.27-3.74,1.27-3.74l-3.08,.11Z"/>
<path d="M135.92,17.06s1.27,3.19,5.39,3.69c.44,.11-.94,2.15-.94,2.15,0,0-3.69-1.05-5.61-4.46,.72-.61,1.16-1.38,1.16-1.38Z"/>
<path d="M124.71,10.09s-1.5,3.33-2.66,5.37c.22,2.53,.66,2.92,.66,2.92l2.59-3.52-.28-4.84-.32,.08Z"/>
<path d="M126.62,13.37l1.82,1.93,1.6-1.27s-2.97-3.08-3.36-2.81-.06,2.15-.06,2.15Z"/>
<path d="M139.53,9.76h-9.6v6.19h10.87v-4.92c0-.7-.57-1.27-1.27-1.27Zm-1.26,4.67c0,.31-.26,.56-.57,.56h-5.13c-.32-.01-.32-.25-.32-.56v-.75c0-.31,.01-.58,.33-.58h5.35c.31,0,.34,.27,.34,.58v.75Zm0-2.55c0,.31-.26,.57-.57,.57h-5.17c-.32,0-.28-.26-.28-.57v-.71c-.01-.32,0-.56,.31-.56l5.38-.02c.31,0,.32,.26,.32,.58v.71Z"/>
<path d="M146.55,6.43s.58,5.92-2.5,9.06c.63,.69,1.07,.99,1.07,.99,0,0,5.06-3.91,4.4-10.05-.77-.55-2.97,0-2.97,0Z"/>
<polygon points="150.87 6.41 150.87 15.51 153.67 15.51 153.67 6.35 150.87 6.41"/>
<path d="M162.4,4.92V13.59c0,1.28-1.04,2.31-2.31,2.31h-2.56v-1.03h2.07V4.99l2.8-.07Z"/>
<path d="M155.68,6.42v6.25c0,.25,.2,.45,.45,.45h1.91c.25,0,.45-.2,.45-.45V6.39c0-.25-.21-.45-.46-.45l-1.91,.03c-.25,0-.44,.2-.44,.45Z"/>
<rect x="151.77" y="15.47" width="3.01" height="5.33"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

28
images/3.svg Normal file
View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 31.79 46.77">
<g id="_图层_1-2" data-name="图层 1">
<g>
<g>
<path d="M9.21,2.74c.13,.23,.72,1.25,.84,1.45,.45,.78,.9,1.56,1.35,2.35l-1.69,1.36c-1.6-.55-3.19-1.1-4.79-1.65"/>
<path d="M13.06,5.43c-.5-1.29,.38-3.57,1.27-4.51C15.49-.3,17.51,0,19.03,.14c0,0-2.48,2.93-3.55,5.08-.82-.01-1.63,.02-2.42,.21Z"/>
<path d="M17.66,4.96L25.6,.8s2.68,1.49,3.63,2.72c-1.4,.41-9.1,2.6-9.37,2.77-.88-.55-2.2-1.33-2.2-1.33Z"/>
<polygon points="20.85 7.78 31.75 7.86 31.79 12.24 20.74 9.98 20.85 7.78"/>
<path d="M19.56,13.33c-.17,0-.29,.12-.57,.33-.62,.47-1.24,.94-1.86,1.41,1.64,1.94,3.21,3.94,4.81,5.91,.62,.77,1.23,1.55,1.87,2.3,.97-.59,1.7-1.47,2.6-2.15,.32-.25,1.43-.76,1.45-1.22,0-.13-.86-.71-.99-.82-.42-.34-.84-.69-1.26-1.03-1.49-1.22-2.97-2.43-4.47-3.63-.4-.32-.81-.71-1.25-.98-.14-.08-.24-.12-.32-.13Z"/>
<path d="M14.11,26.46s-.16-17.73-.16-17.73c0-.06-2.36,.1-2.45,.11-1.79,.15-3.65,.33-5.45,.55-1.43,.18-3.03,.26-4.27,1.12C.46,11.43-.13,13.17,.02,14.73l.17,.55s3.27-1.55,3.58-1.7c.63-.3,1.26-.58,1.9-.84,.54-.22,1.32-.65,1.9-.64-.17,.37-.57,.73-.84,1.05l-1.51,1.81-3.24,3.89c-.48,.57-1.02,.92-.79,1.69,.43,1.47,1.54,2.63,2.77,3.5,1.21-1.91,4.39-7.62,6.15-10.35l-.89,12.76h4.89Z"/>
</g>
<g>
<path d="M7.06,36.55v-.85h-2.6v-2.88H2.27v6.37H.37v5.53H7.04v-4.62c0-.48-.39-.86-.86-.86h-1.72v-2.69h2.6Zm-2.59,3.44c.2,0,.36,.16,.36,.37v3.29c0,.2-.16,.36-.36,.36h-1.47c-.2,0-.37-.16-.37-.36v-3.29c0-.2,.16-.37,.37-.37h1.47Z"/>
<path d="M7.79,32.83h2.21l2.42,12.92h2.49v.93h-2.88c-.68,0-1.33-.33-1.73-.88l-.04-.05c-.12-.16-.2-.35-.23-.54l-2.24-12.38Z"/>
<path d="M6.72,37.7s2.85,.09,6.28-1.09c.31,0,.62,0,.94,0,.15,0,.31,0,.46,0,.08,0,.15,0,.23,0,.07,0,.14,0,.21,0,.06,.01,.1,.04,.13,.09,.01,.03,0,.04-.03,.06-.43,.32-.88,.59-1.39,.78-.02,0-.03,.01-.05,.02-.18,.06-.36,.11-.54,.16-.25,.07-.5,.13-.75,.18-.33,.07-.66,.13-.99,.18-.42,.06-.84,.12-1.26,.16-.52,.05-1.03,.1-1.55,.13-.27,.02-.53,.03-.8,.05-.07,0-.85,.05-.86,.03-.09-.64-.04-.75-.04-.75Z"/>
<polygon points="11.16 34.12 12.1 33.11 14.12 34.39 12.83 35.98 11.16 34.12"/>
<path d="M12.75,38.45l2.27,.92s-3,5.68-8.11,7.25c-.9-.88-.99-1.18-.99-1.18,0,0,3.71-1.57,6.82-6.99Z"/>
<path d="M18.44,32.5l.91,.28,1.55,.49s-2.74,4.59-3.11,4.76-1.44-.47-1.44-.47l2.09-5.06Z"/>
<path d="M19.69,35.9c-.06,0-.04,1.97-.05,2.16-.09,2.83-.64,5.66-2.8,7.68-.03,.03-.24,.25-.3,.2l.88,.84s2.56-1.28,3.99-4.91c.66-1.69,1.16-3.92,.92-6.22,0,0-2.64,.26-2.64,.26Z"/>
<path d="M23.15,45.91l1.93-1.24s-2.53-2.94-3.62-2.81-.6,1.03-.6,1.03l2.29,3.02Z"/>
<rect x="16.76" y="39.35" width="7.72" height="1.01"/>
<rect x="19.16" y="34.97" width="5.04" height=".94"/>
<path d="M30.08,34.13h-5.24v11.5h6.58v-10.16c0-.74-.6-1.34-1.34-1.34Zm-.89,10.14c0,.18-.14,.33-.32,.33h-1.48c-.18,0-.32-.15-.32-.33v-8.96c0-.18,.14-.32,.32-.32h1.48c.18,0,.32,.14,.32,.32v8.96Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -55,11 +55,11 @@ async def get_image(file_name: str):
async def thinking_generator(content: str) -> AsyncIterable[str]:
"""思考过程的异步生成器"""
"""思考过程的异步生成器(打字机效果,整段约 0.3s 完成)"""
yield json.dumps({'think': '\n'}, ensure_ascii=False)
for i in content:
for i in content:
yield json.dumps({'think': i}, ensure_ascii=False)
await asyncio.sleep(0.1)
await asyncio.sleep(0.02)
yield json.dumps({'think': '\n'}, ensure_ascii=False)
@@ -296,7 +296,7 @@ async def chat_test(
elif "search_answer" in json.loads(response):
search_answer = json.loads(response)["search_answer"]
# history_summary+= search_answer
yield json.dumps({"texts": search_answer}, ensure_ascii=False)
yield json.dumps({"text": search_answer}, ensure_ascii=False)
elif "docs" in json.loads(response):
docs = json.loads(response)["docs"]
elif "detail" in json.loads(response):
@@ -308,7 +308,7 @@ async def chat_test(
# yield json.dumps({"text": image_name}, ensure_ascii=False)
else :
#history_summary += json.loads(response)["final_answer"]
yield json.dumps({"texts": json.loads(response)["final_answer"]}, ensure_ascii=False)
yield json.dumps({"text": json.loads(response)["final_answer"]}, ensure_ascii=False)
if stop == "":
continue
else:

247
scripts/gateway-nginx.conf Normal file
View File

@@ -0,0 +1,247 @@
server {
listen 3000;
server_name _;
client_max_body_size 500M;
# 前端 Vite dev server (3001)
location /metalinfo {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
# Java 后端
location /chat_web_backend {
proxy_pass http://127.0.0.1:8099;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# ===== Stirling PDF (相对路径,直接代理) =====
location /tool/pdf/ {
proxy_pass http://127.0.0.1:18080/;
proxy_set_header Host $host;
proxy_buffering off;
}
# ===== Excalidraw (/assets/ 绝对路径,需要 sub_filter) =====
location /tool/draw/ {
proxy_pass http://127.0.0.1:18081/;
proxy_set_header Host $host;
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types text/html application/javascript;
sub_filter 'href="/' 'href="/tool/draw/';
sub_filter 'src="/' 'src="/tool/draw/';
sub_filter '"/assets/' '"/tool/draw/assets/';
}
# Excalidraw 资源回落
location /assets/ {
proxy_pass http://127.0.0.1:18081/assets/;
}
# ===== TrWebOCR (SPA打包无外部资源) =====
location /tool/ocr/ {
proxy_pass http://127.0.0.1:18083/;
proxy_set_header Host $host;
}
# TrWebOCR API
location /tool/ocr/api/ {
proxy_pass http://127.0.0.1:18083/api/;
proxy_set_header Host $host;
}
# ===== LibreTranslate (/static/ 绝对路径) =====
location /tool/translate/ {
proxy_pass http://127.0.0.1:18084/;
proxy_set_header Host $host;
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types text/html;
sub_filter 'href="/static/' 'href="/tool/translate/static/';
sub_filter 'src="/static/' 'src="/tool/translate/static/';
sub_filter 'action="/' 'action="/tool/translate/';
sub_filter 'url: "/' 'url: "/tool/translate/';
}
location /tool/translate/static/ {
proxy_pass http://127.0.0.1:18084/static/;
}
location /tool/translate/translate {
proxy_pass http://127.0.0.1:18084/translate;
proxy_set_header Host $host;
}
location /tool/translate/detect {
proxy_pass http://127.0.0.1:18084/detect;
proxy_set_header Host $host;
}
location /tool/translate/languages {
proxy_pass http://127.0.0.1:18084/languages;
proxy_set_header Host $host;
}
location /tool/translate/frontend/settings {
proxy_pass http://127.0.0.1:18084/frontend/settings;
proxy_set_header Host $host;
}
# ===== PPTist (相对路径,直接代理) =====
location /tool/ppt/ {
proxy_pass http://127.0.0.1:18085/;
proxy_set_header Host $host;
proxy_buffering off;
}
# PPTist AI 后端 (容器内nginx已配 /pptapi/ -> 18086)
location /tool/ppt/pptapi/ {
proxy_pass http://127.0.0.1:18086/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
}
# ===== imgcompress (/_next/ 绝对路径) =====
location /tool/imgcompress/ {
proxy_pass http://127.0.0.1:18087/;
proxy_set_header Host $host;
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types text/html;
sub_filter 'href="/_next/' 'href="/tool/imgcompress/_next/';
sub_filter 'src="/_next/' 'src="/tool/imgcompress/_next/';
sub_filter 'href="/favicon' 'href="/tool/imgcompress/favicon';
}
location /tool/imgcompress/_next/ {
proxy_pass http://127.0.0.1:18087/_next/;
}
location /tool/imgcompress/favicon.ico {
proxy_pass http://127.0.0.1:18087/favicon.ico;
}
location /tool/imgcompress/api/ {
proxy_pass http://127.0.0.1:18087/api/;
proxy_set_header Host $host;
proxy_buffering off;
}
# ===== Lama Cleaner (/static/ 绝对路径) =====
location /tool/lama/ {
proxy_pass http://127.0.0.1:18088/;
proxy_set_header Host $host;
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types text/html;
sub_filter 'src="/static/' 'src="/tool/lama/static/';
sub_filter 'href="/static/' 'href="/tool/lama/static/';
proxy_buffering off;
}
location /tool/lama/static/ {
proxy_pass http://127.0.0.1:18088/static/;
}
location /tool/lama/api/ {
proxy_pass http://127.0.0.1:18088/api/;
proxy_set_header Host $host;
proxy_buffering off;
}
location /tool/lama/inpaint {
proxy_pass http://127.0.0.1:18088/inpaint;
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 300s;
}
# ===== webp2jpg (相对路径为主) =====
location /tool/webp2jpg/ {
proxy_pass http://127.0.0.1:18089/;
proxy_set_header Host $host;
}
# webp2jpg 的 cdn 资源
location /cdn/ {
proxy_pass http://127.0.0.1:18089/cdn/;
}
location /version/ {
proxy_pass http://127.0.0.1:18089/version/;
}
# ===== Overleaf (复杂路径) =====
location /tool/overleaf/ {
proxy_pass http://127.0.0.1:18090/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Accept-Encoding "";
proxy_buffering off;
sub_filter_once off;
sub_filter_types text/html application/javascript;
sub_filter 'href="/' 'href="/tool/overleaf/';
sub_filter 'src="/' 'src="/tool/overleaf/';
sub_filter 'action="/' 'action="/tool/overleaf/';
sub_filter '"/login' '"/tool/overleaf/login';
sub_filter '"/register' '"/tool/overleaf/register';
sub_filter '"/project' '"/tool/overleaf/project';
}
location /tool/overleaf/stylesheets/ {
proxy_pass http://127.0.0.1:18090/stylesheets/;
}
location /tool/overleaf/js/ {
proxy_pass http://127.0.0.1:18090/js/;
}
location /tool/overleaf/img/ {
proxy_pass http://127.0.0.1:18090/img/;
}
location /tool/overleaf/fonts/ {
proxy_pass http://127.0.0.1:18090/fonts/;
}
location /tool/overleaf/login {
proxy_pass http://127.0.0.1:18090/login;
proxy_set_header Host $host;
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types text/html;
sub_filter 'href="/' 'href="/tool/overleaf/';
sub_filter 'src="/' 'src="/tool/overleaf/';
sub_filter 'action="/' 'action="/tool/overleaf/';
}
location /tool/overleaf/register {
proxy_pass http://127.0.0.1:18090/register;
proxy_set_header Host $host;
}
location /tool/overleaf/project {
proxy_pass http://127.0.0.1:18090/project;
proxy_set_header Host $host;
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types text/html;
sub_filter 'href="/' 'href="/tool/overleaf/';
sub_filter 'src="/' 'src="/tool/overleaf/';
}
location /tool/overleaf/socket.io/ {
proxy_pass http://127.0.0.1:18090/socket.io/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /tool/overleaf/launchpad {
proxy_pass http://127.0.0.1:18090/launchpad;
proxy_set_header Host $host;
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types text/html;
sub_filter 'href="/' 'href="/tool/overleaf/';
sub_filter 'src="/' 'src="/tool/overleaf/';
sub_filter 'action="/' 'action="/tool/overleaf/';
}
# ===== LaTeX 公式编辑器 (CDN外链无问题) =====
location /tool/latex/ {
proxy_pass http://127.0.0.1:18091/;
proxy_set_header Host $host;
}
# 默认跳转前端
location / {
return 302 /metalinfo;
}
}