Compare commits

11 Commits

Author SHA1 Message Date
f8e8017434 feat: 顶部 title + logo 配色 + 侧栏外部系统快捷入口
- index.html: 浏览器 title 改为"中国产业基础能力发展战略研究院战知大模型"
- properties.ts: 加 fullProjectName 字段,projectName 短名保留用于 footer
- title.svg / chatLogo.svg: 米字图标左下主笔染 #FF2500 红,其余 5 笔染 #636E77 灰,"战知"汉字保留默认黑色
- Operates.vue: 侧栏底部 user 头像上方新增 3 个浅蓝按钮风格外链入口(钢研门户/项目管理/数据中心),点击新窗口打开内网系统

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:51:09 +08:00
0c3a393d04 [运维] 接入内网 searxng + 清理启动脚本 + 修 log-trim 权限
搜索接口:

- duckduckgo_search.py / ZhipuSearchAPI.py 切换到内网 searxng (原 43.251.225.121 / 134.122.191.214 已失效)

启动脚本清理:

- 删除废弃 backend/ 目录 (与 chat_web_backend/ 编译产物 jar MD5 相同,仅是改名副本)

- 删除 start_all.sh 与 langchain-chat/{start,stop,stop_quick,shutdown_all,restart}.sh (被 scripts/*-restart.sh 覆盖)

- 删除 chat_web_backend/{start,test_mysql}.sh

修复:

- scripts/backend-restart.sh 对齐当前实际在跑的 chat_web_backend.jar (profile=dev)

- scripts/log-trim-daemon.sh 把 LOCK 移到 /tmp 按用户命名,修复非首次用户跑时的 Permission denied

新增:

- scripts/start-all.sh:一键启动入口,串联 mysql/redis/milvus/langchain/backend/frontend,含端口自检

- chat_web_backend/application-local.yml.archived:原 backend/ 下 yj profile 覆盖配置的归档备份

其他:

- .gitignore 忽略 scripts/pptist-deploy/PPTist/ (323M 第三方源码树)
2026-04-20 15:59:11 +08:00
279b104434 Merge pull request 'fix: 修改KGO搜索接口地址' (#1) from dev_albert into main
Reviewed-on: #1
2026-04-19 16:23:20 +00:00
8bb98dc2e1 fix: 修改KGO搜索接口地址 2026-04-19 23:46:08 +08:00
5eebcb5e83 fix: 工具反代路径冲突修复 + OCR改独立端口
- Stirling PDF: 用 context-path=/pdf 从应用层解决路径问题
- OCR: Vue Router history模式不兼容子路径,改回独立端口18083
- Lama Cleaner: 精确location匹配所有API(/model /inpaint等)
- PDF前端卡片路径从/spdf/改为/pdf/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:20:55 +08:00
5850d37c48 fix: 修复工具nginx反代路径冲突
- Stirling PDF: 使用 SERVER_SERVLET_CONTEXT_PATH=/pdf 从根本解决 base href 问题
- TrWebOCR: 用精确文件名 location 代替 sub_filter(后端 gzip 导致 sub_filter 不生效)
- Lama Cleaner: 同样用精确 location 匹配所有 API 路径(/model, /inpaint 等)
- PDF API 用 /api/v1/ 精确匹配,和 imgcompress 的 /api/ 不冲突
- OCR API 用 /api/tr-run/ 精确匹配

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:54:33 +08:00
570c0f3d61 fix: 工具广场统一18000端口 + 去除电量限制 + bot头像更新
nginx反代(tools-nginx.conf):
- 10个工具统一通过18000端口路径分发
- 解决/static/冲突(Lama精确匹配,其余给LibreTranslate)
- 解决/api/冲突(PDF用sub_filter改为/pdf-api/,/api/给imgcompress)
- Overleaf兜底处理所有未匹配的绝对路径
- 前端工具卡片统一走18000端口+路径

后端:
- 去除对话电量限制(validateUser中的num扣减逻辑)

前端:
- bot头像更新为战知logo(蓝底白字)
- LaTeX公式编辑器"复制为图片"改为"下载为图片"(解决跨域问题)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:28:23 +08:00
00a50858f8 fix: 调整侧边栏导航间距
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:39:13 +08:00
e1c3e550c3 feat: 用户管理模块
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:24:16 +08:00
6337af9481 feat: 新增4个工具(imgcompress/LamaCleaner/webp2jpg/Overleaf/LaTeX公式编辑器) + 应用广场布局优化
新增工具部署:
- imgcompress (端口18087) - 图片压缩、格式转换、AI抠图
- Lama Cleaner (端口18088) - AI图像擦除/去水印
- webp2jpg-online (端口18089) - 图片格式批量互转
- Overleaf (端口18090) - 在线LaTeX论文编辑器(docker-compose + MongoDB 8.0)
- LaTeX公式编辑器 (端口18091) - 纯前端KaTeX公式编辑

应用广场优化:
- 去掉tab切换,所有分类平铺展示
- CSS Grid自适应布局,一行可排3-4个卡片
- 重新分为4个分类:文档处理、图片处理、创作绘图、科研写作

其他:
- 更新 CLAUDE.md 项目配置文档
- PPTist AI后端优化prompt和流式输出格式

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:23:06 +08:00
108022cebd feat: 品牌升级(知冶→战知) + 应用工具广场重构 + 新增工具集成
品牌升级:
- 全站品牌从"知冶"更名为"战知"
- 更换 favicon、侧边栏 logo、登录页 logo
- 更新登录页标语和首页欢迎语

应用广场重构:
- 从后端数据库驱动改为前端静态配置,按分类 tab 展示
- 新增工具卡片 UI,支持 logo 图片和 emoji 图标

新增工具部署:
- Stirling PDF (端口18080) - PDF 处理工具箱
- Excalidraw (端口18081) - 手绘风格白板,集成 AI 绘图
- TrWebOCR (端口18083) - 中文离线 OCR
- LibreTranslate (端口18084) - 中英翻译引擎
- PPTist (端口18085) - 在线 PPT 编辑器
- PPTist AI 后端 (端口18086) - 对接 deepseek-v3 生成大纲/PPT/写作
- Excalidraw AI 代理 (端口18082) - 对接 deepseek-v3 生成 Mermaid 图

其他:
- 智能场景仅保留"选题推荐"
- vite 代理配置增加 /pdf/ 和 /draw/ 路由

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 17:24:51 +08:00
64 changed files with 2432 additions and 1113 deletions

3
.gitignore vendored
View File

@@ -57,3 +57,6 @@ Thumbs.db
# Claude
.claude/
# PPTist 源码树pptist-deploy 期间 git clone 下来的 323M 依赖,含 node_modules + .git
scripts/pptist-deploy/PPTist/

View File

@@ -1,6 +1,6 @@
# 钢研院智能问答平台(gangyan
# 钢研院智能问答平台(战知
面向冶金行业的 LLM 智能问答系统,提供智能对话、文档撰写、知识库管理、文献检索、翻译等功能。
面向科研单位的 LLM 智能问答系统,提供智能对话、文档撰写、知识库管理、文献研读、实用工具广场等功能。
## 项目结构
@@ -20,14 +20,104 @@
| MinIO | 9002(console)/9003(API) | 对象存储Milvus 依赖) |
| Embedding API | 10.102.24.75:3000 | bge-m3 向量模型 + LLM 网关 |
## LLM 配置
- 网关: `http://10.102.24.75:3000/v1/chat/completions`
- API Key: `sk-BlQIGRrotbVDWE5mXCPBFjVWIvJ83hldzz67xInNwzVo7pPb`
- 主力模型: `deepseek-v3`所有场景统一使用r1 已弃用)
- Embedding: `bge-m3`
- 上下文限制: 32K tokens
## 服务器访问
- SSH 直连: `ssh target-203-8`(已配公钥,自动通过 jump-203-17 跳转)
- 跳板机: `huawei@192.168.203.17`
- 目标机: `hawei@192.168.203.8`
- 项目路径: `/opt/download/oss_files/gangyan-deploy/gangyan/`
- Python: `/opt/software/miniconda3/envs/langchain-chat/bin/python`
- 网络代理: `http://219.234.197.247:53128`
- Docker 镜像源: `docker.1ms.run`Docker Hub 直连不通,必须用镜像源)
## 工具广场 - 已部署工具
### 文档处理
| 工具 | 端口 | 容器名 | 镜像 |
|------|------|--------|------|
| Stirling PDF | 18080 | stirling-pdf | `docker.1ms.run/frooodle/s-pdf` |
| TrWebOCR (中文OCR) | 18083 | trwebocr | `docker.1ms.run/mmmz/trwebocr` |
| LibreTranslate (翻译) | 18084 | libretranslate | `docker.1ms.run/libretranslate/libretranslate` |
LibreTranslate 配置: `LT_LOAD_ONLY=en,zh``LT_HIDE_API=true`,需设 HTTP_PROXY 代理下载语言包。
### 图片处理
| 工具 | 端口 | 容器名 | 镜像 |
|------|------|--------|------|
| imgcompress (压缩/转换/抠图) | 18087 | imgcompress | `imgcompress-nofooter:latest`commit 版,隐藏了 footer |
| Lama Cleaner (AI擦除) | 18088 | lama-cleaner | `docker.1ms.run/cwq1913/lama-cleaner:cpu-0.33.0` |
| webp2jpg-online (格式转换) | 18089 | webp2jpg | `docker.1ms.run/wbsu2003/webp2jpg-online:v1` |
Lama Cleaner 启动命令: `lama-cleaner --model=lama --device=cpu --host=0.0.0.0 --port=8080`需代理下载模型约196MB模型缓存在 volume `lama-models`
### 创作绘图
| 工具 | 端口 | 容器名 | 镜像 |
|------|------|--------|------|
| Excalidraw (白板) | 18081 | excalidraw | `docker.1ms.run/excalidraw/excalidraw` |
| PPTist (AI PPT) | 18085 | pptist | `pptist:latest`(自建镜像) |
PPTist 自建镜像: Dockerfile 在 `scripts/pptist-deploy/`,构建时需代理 + npm 淘宝镜像源。容器内 nginx 反代 `/pptapi/` 到宿主机 18086 端口的 AI 后端。
### 科研写作
| 工具 | 端口 | 容器名 | 镜像 |
|------|------|--------|------|
| Overleaf (LaTeX论文) | 18090 | overleaf | `docker.1ms.run/sharelatex/sharelatex` |
| LaTeX 公式编辑器 | 18091 | latex-editor | `docker.1ms.run/nginx:alpine`(纯前端) |
Overleaf 配置:
- docker-compose 在 `scripts/overleaf-deploy/docker-compose.yml`
- 依赖 MongoDB 8.0(需 replica set+ Redis
- 管理员: `admin@company.com` / `qwerQWER1234`
- 注册: 管理员在 `/admin/register` 手动添加,不支持自助注册
- 没有邮件服务,新用户通过管理员生成的链接设置密码
## AI 代理服务
以 screen 会话运行,重启服务器后需手动恢复。
| 服务 | 端口 | screen 名 | 文件 | 功能 |
|------|------|-----------|------|------|
| Excalidraw AI | 18082 | aiproxy | `scripts/excalidraw-ai-proxy.py` | text-to-diagramMermaid、wireframe-to-code |
| PPTist AI | 18086 | pptist-ai | `scripts/pptist-ai-backend.py` | 大纲生成、PPT 生成JSONL流式、AI 写作 |
启动方式:
```bash
PYTHON=/opt/software/miniconda3/envs/langchain-chat/bin/python
screen -dmS aiproxy $PYTHON scripts/excalidraw-ai-proxy.py
screen -dmS pptist-ai $PYTHON scripts/pptist-ai-backend.py
```
## 前端应用广场
- 文件: `chat_web_front/src/views/applications/index.vue`
- 布局: 所有分类平铺展示CSS Grid 自适应 `repeat(auto-fill, minmax(280px, 1fr))`
- 工具配置是前端静态数据,不走后端数据库
- 点击工具卡片 `window.open` 新标签打开 `${protocol}//${hostname}:${port}`
## 品牌
- 名称: 战知(原"知冶"
- 标语: 聚尖端之力,创多维平台
- 副标语: 聚合科技动能,扩展创新疆界,引领行业跃迁升级
## 项目位置
- 服务器路径:`/opt/download/oss_files/gangyan-deploy/gangyan`
- Git 仓库:`http://123.57.146.97:3000/liuguancen/gangyan.git`
- 分支:`main`
- Git 仓库: `http://123.57.146.97:3000/liuguancen/gangyan.git`
- 分支: `main`
## 详细文档索引
## 开发注意事项
- [系统架构与运行配置](architecture.md) — 服务拓扑、各模块详细配置、包结构、API 分组
- [开发构建指南](development-guide.md) — 启动顺序、构建命令、环境要求、日志与健康检查
- [Agent 协同规则](agent-coordination.md) — 多 Agent 并行开发的分支、接口变更、提交规范
- 功能未完全验证通过前不要 commit避免无用提交
- 前端用 Vite dev server 跑scp 文件到服务器后自动热更新
- Docker Hub 不通,拉镜像必须加 `docker.1ms.run/` 前缀
- 容器内需要网络时(下载模型/语言包)必须设 `HTTP_PROXY``HTTPS_PROXY` 环境变量
- 跳板机 SSH 经常断连,保持命令简短,避免长时间占用连接
- screen 会话管理 AI 代理服务,`screen -ls` 查看,`screen -r 名称` 进入

View File

@@ -1,49 +0,0 @@
#!/bin/bash
# 启动 Java 后端。profile=yj。
# MySQL 33306 / Redis 等统一见同目录 application-local.yml与 scripts/backend-restart.sh 一致,勿再写 -D 数据源以免和 yml 冲突)。
set -euo pipefail
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
export PATH="$JAVA_HOME/bin:$PATH"
PROJECT_DIR=/opt/download/oss_files/gangyan-deploy/gangyan/backend
JAR_FILE="$PROJECT_DIR/chat_web_yj.jar"
LOCAL_CFG="$PROJECT_DIR/application-local.yml"
if [[ ! -f "$JAR_FILE" ]]; then
echo "Error: JAR file not found at $JAR_FILE"
exit 1
fi
pkill -f "chat_web_yj.jar" 2>/dev/null || true
sleep 2
# 日志目录logback 写 /opt/apps/...);非 root 启动时需可写
mkdir -p "$PROJECT_DIR/logs" /opt/apps/logs/chat-server-backend-cast 2>/dev/null || true
LOG_OUT="$PROJECT_DIR/nohup.out"
if ! ( umask 022; : >>"$LOG_OUT" ) 2>/dev/null; then
LOG_OUT="/tmp/chat_web_yj_nohup.log"
fi
EXTRA_JAVA=()
[[ -f "$LOCAL_CFG" ]] && EXTRA_JAVA=(-Dspring.config.additional-location="file:${LOCAL_CFG}")
nohup java \
-Djava.net.useSystemProxies=false \
-Dhttp.nonProxyHosts="localhost|127.*|[::1]|*.local" \
-Xms512m \
-Xmx2048m \
"${EXTRA_JAVA[@]}" \
-Dspring.profiles.active=yj \
-jar "$JAR_FILE" >>"$LOG_OUT" 2>&1 &
sleep 10
if pgrep -f "chat_web_yj.jar" > /dev/null; then
echo "OK PID $(pgrep -n -f 'java .*chat_web_yj\.jar' | head -1) log:$LOG_OUT"
else
tail -50 "$LOG_OUT" 2>/dev/null || true
exit 1
fi

View File

@@ -1 +0,0 @@
nohup /home/gc/gangyan/backend/jdk1.8.0_161/bin/java -jar -Xms8g -Xmx8g -DserverUrlPrefix=http://localhost:3000 -DserverFrontUrlPrefix=/chat_web -Dspring.profiles.active=yj -Dspring.redis.database=3 -Dspring.redis.password=Redis_897653 -Dchat.modelName=zhipu-api /home/gc/gangyan/backend/chat_web_yj.jar > /home/gc/gangyan/backend/nohup.out 2>&1 &

View File

@@ -1 +0,0 @@
ps -ef|grep chat_web_yj | grep -v grep | awk '{print $2}' | xargs kill -9

View File

@@ -1,3 +1,7 @@
# 原始位置:/opt/download/oss_files/gangyan-deploy/gangyan/backend/application-local.yml
# 归档时间2026-04-20
# 用途yj profile 的本地覆盖mysql 33306 Docker 映射),启动时通过 -Dspring.config.additional-location 叠加
# 覆盖 jar 内过期的 application-yj.yml当前 jar 仍含远程库 123.57.146.97,不配本会长时间卡 Druid
# 完整 spring.redis + spring.datasource 与源码 application-yj.yml 对齐(本地 Docker 33306

View File

@@ -4,6 +4,7 @@ import com.inspur.llm.chat.base.base.BaseController;
import com.inspur.llm.chat.gpt.constant.SysLogTypeConstant;
import com.inspur.llm.chat.gpt.enums.BusinessTypeEnum;
import com.inspur.llm.chat.gpt.pojo.annotation.Log;
import com.inspur.llm.chat.gpt.pojo.command.SysUserPasswordCommand;
import com.inspur.llm.chat.gpt.pojo.command.UserCommand;
import com.inspur.llm.chat.gpt.pojo.entity.IPageInfo;
import com.inspur.llm.chat.gpt.pojo.entity.Query;
@@ -105,6 +106,19 @@ public class UserController extends BaseController {
return userService.updateUser(command);
}
/**
* 管理员重置用户密码
*
* @author: Auto
* @date: 2026-04-08
* @version: 1.0.0
*/
@PutMapping("/password/reset")
@Log(type = SysLogTypeConstant.DEFAULT, businessType = BusinessTypeEnum.UPDATE)
public ResponseInfo resetPassword(@RequestBody SysUserPasswordCommand command) {
return userService.resetPassword(command);
}
/**
* 批量删除会员用户
*

View File

@@ -104,6 +104,11 @@ public class User extends BaseEntity {
*/
private Integer status;
/**
* 是否管理员 0否 1是
*/
private Boolean admind;
/**
* 是否删除 0->未删除;1->已删除
*/

View File

@@ -108,4 +108,9 @@ public class UserVO implements Serializable {
*/
private Integer status;
/**
* 是否管理员 0否 1是
*/
private Boolean admind;
}

View File

@@ -119,4 +119,12 @@ public interface IUserService extends IService<User> {
*/
ResponseInfo removeUserById(Long id);
/**
* 管理员重置用户密码
*
* @param command 包含用户id和新密码
* @return 结果
*/
ResponseInfo resetPassword(SysUserPasswordCommand command);
}

View File

@@ -51,14 +51,6 @@ public class GptServiceImpl implements IGptService {
if (ValidatorUtil.isNull(user)) {
throw new ProhibitVisitException();
}
if (user.getNum() < 1) {
throw new BusinessException("电量不足,请分享好友获取电量或开通会员");
}
// 扣电量
UpdateWrapper<User> uw = new UpdateWrapper<>();
uw.lambda().set(User::getNum, user.getNum() - 1).eq(BaseEntity::getId, user.getId());
userMapper.update(null, uw);
}
@Override

View File

@@ -108,10 +108,17 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
}
user = DozerUtil.convertor(command, User.class);
user.setCreateUser(command.getOperater());
String name = "手机用户" + RandomUtil.randomString(6);
user.setUid(UUID.fastUUID().toString());
if (ValidatorUtil.isNull(user.getName()) || user.getName().isEmpty()) {
String name = "手机用户" + RandomUtil.randomString(6);
user.setName(name);
user.setNickName(name);
} else if (ValidatorUtil.isNull(user.getNickName()) || user.getNickName().isEmpty()) {
user.setNickName(user.getName());
}
if (ValidatorUtil.isNotNull(command.getPassword()) && !command.getPassword().isEmpty()) {
user.setPassword(JWTPasswordEncoder.bcryptEncode(command.getPassword()));
}
user.setType(UserTypeEnum.TEL.getValue());
userMapper.insert(user);
return ResponseInfo.success();
@@ -185,4 +192,13 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
return ResponseInfo.success();
}
@Override
@Transactional(rollbackFor = Exception.class, transactionManager = "masterTransactionManager")
public ResponseInfo resetPassword(SysUserPasswordCommand command) {
User user = getUser(command.getId());
user.setPassword(JWTPasswordEncoder.bcryptEncode(command.getNewPassword()));
userMapper.updateById(user);
return ResponseInfo.success();
}
}

View File

@@ -92,3 +92,4 @@ xss:
# 词向量模型名称
embedding-model-name: bge_m3

View File

@@ -24,13 +24,14 @@
<result column="share_id" property="shareId"/>
<result column="type" property="type"/>
<result column="status" property="status"/>
<result column="admind" property="admind"/>
<result column="deleted" property="deleted"/>
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
t.id, t.create_user, t.create_time, t.update_user, t.update_time, t.login_time, t.uid, t.name, t.nick_name, t.tel, t.password,
t.avatar, t.openid, t.unionid, t.ip, t.context, t.num, t.share_id, t.type, t.status, t.deleted
t.avatar, t.openid, t.unionid, t.ip, t.context, t.num, t.share_id, t.type, t.status, t.admind, t.deleted
</sql>
<!-- 通用查询条件 -->

View File

@@ -1,25 +0,0 @@
#!/bin/bash
# Java后端启动脚本
# 使用 Java 11 运行(兼容性更好)
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH
cd "$(dirname "$0")"
echo "=========================================="
echo "启动 Chat Web Backend"
echo "=========================================="
echo ""
echo "Java版本:"
java -version
echo ""
echo "启动端口: 8099"
echo "访问地址: http://localhost:8099/chat_web_backend"
echo ""
echo "按 Ctrl+C 停止服务"
echo "=========================================="
echo ""
# 启动应用
nohup java -jar target/chat_web_backend.jar --spring.profiles.active=dev > ./nohup.out 2>&1 &

View File

@@ -1,4 +0,0 @@
#!/bin/bash
echo "测试MySQL连接..."
echo "密码: 1234567890"
mysql -uroot -p1234567890 -h127.0.0.1 -e "SHOW DATABASES;" 2>&1 | head -20

View File

@@ -1,6 +1,6 @@
# 知冶大模型
#
知冶大模型
战知智能问答平台
# chat_web_front_new

View File

@@ -3,10 +3,10 @@
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.png">
<link rel="apple-touch-icon" href="/favicon.png">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="apple-touch-icon" href="/favicon.svg">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>冶·大模型</title>
<title>中国产业基础能力发展战略研究院战知大模型</title>
</head>
<body>

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

View File

@@ -23,17 +23,25 @@ watch(
.content {
width: 100vw;
height: 100vh;
// background: linear-gradient(180deg, #edf2ff 0%, #f7f9ff 100%),
// url("./assets/images/chat/chatLogo.png") no-repeat;
background-color: #edf2ffcc;
background-image: url("./assets/images/chat/chatLogo.png");
position: relative;
display: flex;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url("./assets/images/chat/chatLogo.svg");
background-size: 45%;
background-position: right bottom;
background-blend-mode: unset;
background-repeat: no-repeat;
// background-size: 45%;
// background-position: right bottom;
display: flex;
opacity: 0.04;
pointer-events: none;
z-index: 0;
}
.chatLogo {
width: 764px;
height: 700px;

View File

@@ -997,3 +997,44 @@ export function fetchLoginStatus<T>() {
url: '/app/api/auth/loginStatus'
})
}
// ****************************用户管理 - 开始********************************
// 用户分页列表
export function fetchUserPage<T>(data: { current: number; size: number; keyword?: string }) {
return get<T>({
url: '/gpt/user/page',
data,
})
}
// 新增用户
export function createUser<T>(data: object) {
return post<T>({
url: '/gpt/user',
data,
})
}
// 修改用户
export function editUser<T>(data: object) {
return put<T>({
url: '/gpt/user',
data,
})
}
// 删除用户
export function deleteUser<T>(ids: string) {
return post<T>({
url: `/gpt/user/${ids}`,
})
}
// 管理员重置用户密码
export function resetUserPassword<T>(data: { id: number; newPassword: string }) {
return put<T>({
url: '/gpt/user/password/reset',
data,
})
}
// ****************************用户管理 - 结束********************************

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000">
<rect width="1000" height="1000" rx="200" ry="200" fill="#fff" />
<svg viewBox="0 0 107 101" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2">
<path style="fill:none" d="M24 17h121v121H24z" transform="matrix(.8843 0 0 .83471 -21.223 -14.19)" />
<path d="M119.81 105.98a.549.549 0 0 0-.53-.12c-4.19-6.19-9.52-12.06-14.68-17.73l-.85-.93c0-.11-.05-.21-.12-.3a.548.548 0 0 0-.34-.2l-.17-.18-.12-.09c-.15-.32-.53-.56-.95-.35-1.58.81-3 1.97-4.4 3.04-1.87 1.43-3.7 2.92-5.42 4.52-.7.65-1.39 1.33-1.97 2.09-.28.37-.07.72.27.87-1.22 1.2-2.45 2.45-3.68 3.74-.11.12-.17.28-.16.44.01.16.09.31.22.41l2.16 1.65s.01.03.03.04c3.09 3.05 8.51 7.28 14.25 11.76.85.67 1.71 1.34 2.57 2.01.39.47.76.94 1.12 1.4.19.25.55.3.8.11.13.1.26.21.39.31a.57.57 0 0 0 .8-.1c.07-.09.1-.2.11-.31.04 0 .07.03.1.03.15 0 .31-.06.42-.18l10.18-11.12a.56.56 0 0 0-.04-.8l.01-.01Zm-29.23-3.85c.07.09.14.17.21.25 1.16.98 2.4 2.04 3.66 3.12l-5.12-3.91s-.32-.22-.52-.36c-.11-.08-.21-.16-.31-.24l-.38-.32s.07-.07.1-.11l.35-.35c1.72-1.74 4.67-4.64 6.19-6.06-1.61 1.62-4.87 6.37-4.17 7.98h-.01Zm17.53 13.81-4.22-3.22c-1.65-1.71-3.43-3.4-5.24-5.03 2.28 1.76 4.23 3.25 4.52 3.51 2.21 1.97 2.11 1.61 3.63 2.91l1.83 1.33c-.18.16-.36.33-.53.49l.01.01Zm1.06.81-.08-.06c.16-.13.33-.25.49-.38l-.4.44h-.01ZM42.24 51.45c.14.72.27 1.43.4 2.11.69 3.7 1.33 7.03 2.55 9.56l.48 1.92c.19.73.46 1.64.71 1.83 2.85 2.52 7.22 6.28 11.89 9.82.21.16.5.15.7-.01.01.02.03.03.04.04.11.1.24.15.38.15.16 0 .31-.06.42-.19 5.98-6.65 10.43-12.12 13.6-16.7.2-.25.3-.54.29-.84.2-.24.41-.48.6-.68a.558.558 0 0 0-.1-.86.578.578 0 0 0-.17-.36c-1.39-1.34-2.42-2.31-3.46-3.28-1.84-1.72-3.74-3.5-7.77-7.51-.02-.02-.05-.04-.07-.06a.555.555 0 0 0-.22-.14c-1.11-.39-3.39-.78-6.26-1.28-4.22-.72-10-1.72-15.2-3.27h-.04v-.01s-.02 0-.03.02h-.01l.04-.02s-.31.01-.37.04c-.08.04-.14.09-.19.15-.05.06-.09.12-.47.2-.38.08.08 0 .11 0h-.11v.03c.07.34.05.58.16.97-.02.1.21 1.02.24 1.11l1.83 7.26h.03Zm30.95 6.54s-.03.04-.04.05l-.64-.71c.22.21.44.42.68.66Zm-7.09 9.39s-.07.08-.1.12l-.02-.02c.04-.03.08-.07.13-.1h-.01Zm-7.07 8.47Zm3.02-28.57c.35.35 1.74 1.65 2.06 1.97-1.45-.66-5.06-2.34-6.74-2.88 1.65.29 3.93.66 4.68.91Zm-19.18-2.77c.84 1.44 1.5 6.49 2.16 11.4-.37-1.58-.69-3.12-.99-4.6-.52-2.56-1-4.85-1.67-6.88.14.01.31.03.49.05 0 .01 0 .02.02.03h-.01Zm-.29-1.21c-.23-.02-.44-.04-.62-.05-.02-.04-.03-.08-.04-.12l.66.18v-.01Zm-2.22.45v-.02.02ZM118.9 42.57c.04-.23-1.1-1.24-.74-1.26.85-.04.86-1.35 0-1.31-1.13.06-2.27.32-3.37.53-1.98.37-3.95.78-5.92 1.21-4.39.94-8.77 1.93-13.1 3.11-1.36.37-2.86.7-4.11 1.36-.42.22-.4.67-.17.95-.09.05-.18.08-.28.09-.37.07-.74.13-1.11.19a.566.566 0 0 0-.39.86c-2.32 3.1-4.96 6.44-7.82 9.95-2.81 3.21-5.73 6.63-8.72 10.14-9.41 11.06-20.08 23.6-31.9 34.64-.23.21-.24.57-.03.8.05.06.12.1.19.13-.16.15-.32.3-.48.44-.1.09-.14.2-.16.32-.08.08-.16.17-.23.25-.21.23-.2.59.03.8.23.21.59.2.8-.03.04-.04.08-.09.12-.13a.84.84 0 0 1 1.22 0c.69.74 1.34 1.44 1.95 2.09l-1.38-1.15a.57.57 0 0 0-.8.07c-.2.24-.17.6.07.8l14.82 12.43c.11.09.24.13.37.13.15 0 .29-.06.4-.17l.36-.36a.56.56 0 0 0 .63-.12c20.09-20.18 36.27-35.43 54.8-49.06.17-.12.25-.32.23-.51a.57.57 0 0 0 .48-.39c3.42-10.46 4.08-19.72 4.28-24.27 0-.03.01-.05.02-.07.02-.05.03-.1.04-.14.03-.11.05-.19.05-.19.26-.78.17-1.53-.15-2.15v.02ZM82.98 58.94c.9-1.03 1.79-2.04 2.67-3.02-5.76 7.58-15.3 19.26-28.81 33.14 9.2-10.18 18.47-20.73 26.14-30.12Zm-32.55 52.81-.03-.03c.11.02.19.04.2.04a.47.47 0 0 0-.17 0v-.01Zm6.9 6.42-.05-.04.03-.03c.02 0 .03.02.04.02 0 .02-.02.03-.03.05h.01Zm8.36-7.21 1.38-1.44c.01.01.02.03.03.05-.47.46-.94.93-1.42 1.39h.01Zm2.24-2.21c.26-.3.56-.65.87-1.02.01-.01.02-.03.04-.04 3.29-3.39 6.68-6.82 10.18-10.25.02-.02.05-.04.07-.06.86-.66 1.82-1.39 2.72-2.08-4.52 4.32-9.11 8.78-13.88 13.46v-.01Zm21.65-55.88c-1.86 2.42-3.9 5.56-5.63 8.07-5.46 7.91-23.04 27.28-23.43 27.65-2.71 2.62-10.88 10.46-16.09 15.37-.14.13-.25.24-.34.35a.794.794 0 0 1 .03-1.13c24.82-23.4 39.88-42.89 46-51.38-.13.33-.24.69-.55 1.09l.01-.02Zm16.51 7.1-.01.02c0-.02-.02-.07.01-.02Zm-.91-5.13Zm-5.89 9.45c-2.26-1.31-3.32-3.27-2.71-5.25l.19-.66c.08-.19.17-.38.28-.57.59-.98 1.49-1.85 2.52-2.36.05-.02.1-.03.15-.04a.795.795 0 0 1-.04-.43c.05-.31.25-.58.66-.58.67 0 2.75.62 3.54 1.3.24.19.47.4.68.63.3.35.74.92.96 1.33.13.06.23.62.38.91.14.46.2.93.18 1.4 0 .02 0 .02.01.03-.03.07 0 .37-.04.4-.1.72-.36 1.43-.75 2.05-.04.05-.07.11-.11.16 0 .01-.02.02-.03.04-.3.43-.65.83-1.08 1.13-1.26.89-2.73 1.16-4.2.79a6.33 6.33 0 0 1-.57-.25l-.02-.03Zm16.27-1.63c-.49 2.05-1.09 4.19-1.8 6.38-.03.08-.03.16-.03.23-.1.01-.19.05-.27.11-4.44 3.26-8.73 6.62-12.98 10.11 3.67-3.32 7.39-6.62 11.23-9.95a6.409 6.409 0 0 0 2.11-3.74l.56-3.37.03-.1c.25-.71 1.34-.4 1.17.33h-.02Z" style="fill:#6965db;fill-rule:nonzero" transform="matrix(1 0 0 1 -26.41 -29.49)" />
</svg>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<rect x="2" y="2" width="44" height="44" rx="8" fill="#1976d2"/>
<text x="24" y="20" text-anchor="middle" font-size="14" font-weight="bold" fill="#ffffff" font-family="Arial, sans-serif">EN</text>
<line x1="12" y1="25" x2="36" y2="25" stroke="#ffffff" stroke-width="1.5" opacity="0.5"/>
<text x="24" y="39" text-anchor="middle" font-size="14" font-weight="bold" fill="#ffffff" font-family="Arial, sans-serif"></text>
</svg>

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 48 48"><g fill="none" stroke-linejoin="round" stroke-width="4"><path fill="#d14424" stroke="#333333" d="M8 6C8 4.89543 8.89543 4 10 4H30L40 14V42C40 43.1046 39.1046 44 38 44H10C8.89543 44 8 43.1046 8 42V6Z"/><path stroke="#ffffff" stroke-linecap="round" d="M16 20H32"/><path stroke="#ffffff" stroke-linecap="round" d="M16 28H32"/></g></svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@@ -0,0 +1,34 @@
<svg width="82" height="92" viewBox="0 0 82 92" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_dddd_3933_98709)">
<path d="M56.3678 0.557543L9.07973 10.0315C4.2844 10.989 0.875 14.8441 0.875 19.3039V73.9053C0.875 77.7856 4.06265 81.0359 8.27589 81.4895L33.0841 84.4375L54.9327 64.6642L77.1014 44.0219L76.7133 12.778C76.6579 8.97326 72.7773 6.15122 68.7027 6.95752L63.2143 8.04098L63.2421 5.67248C63.2975 2.34651 59.9158 -0.147965 56.3678 0.557543Z" fill="#FF4B4B"/>
<path d="M63.269 8.04043V57.5772L77.1561 43.9962L76.7681 11.7192C76.7403 8.44358 73.3864 6.02469 69.8661 6.7302L63.269 8.04043Z" fill="#545454"/>
<path d="M33.0873 84.4375L70.2189 75.6383L77.1569 43.9963L33.0873 84.4375Z" fill="#D0DBDC"/>
<path d="M44.1432 35.3049C44.1432 35.3049 39.9022 30.2907 36.77 28.9301C34.7743 28.0734 31.8084 27.7459 29.48 28.8041C26.0429 30.3663 25.6826 34.7506 28.8425 36.4891C29.5909 36.9175 30.5888 37.2702 31.8915 37.5474C38.8767 39.0088 50.4076 42.0324 48.994 54.2024C48.994 54.2024 48.1901 65.6166 34.9683 66.9268C32.6399 67.1536 30.2839 67.0024 28.0109 66.5236C24.8787 65.8937 19.335 64.6087 16.6185 63.0717L16.1196 50.045H17.4501C17.4501 50.045 20.0834 55.8402 27.5674 58.6371C28.9534 59.141 30.4779 59.3426 31.947 59.141C33.8041 58.889 35.9939 57.982 36.8809 55.2355C36.8809 55.2355 38.3777 51.2292 30.6165 49.0371C24.4075 47.2733 18.5589 45.4592 16.1473 39.5127C15.0109 36.6907 14.8168 33.5663 15.593 30.5679C16.6185 26.6624 19.5567 21.1695 27.8723 18.801C27.8723 18.801 38.7935 16.105 46.2776 19.7585L45.9449 35.3049H44.1432Z" fill="white"/>
</g>
<defs>
<filter id="filter0_dddd_3933_98709" x="-8.34465e-07" y="-4.17233e-07" width="81.0944" height="91.875" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.4375"/>
<feGaussianBlur stdDeviation="0.4375"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3933_98709"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="0.4375" dy="1.3125"/>
<feGaussianBlur stdDeviation="0.656251"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.09 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_3933_98709" result="effect2_dropShadow_3933_98709"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="0.875001" dy="3.0625"/>
<feGaussianBlur stdDeviation="0.875001"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="effect2_dropShadow_3933_98709" result="effect3_dropShadow_3933_98709"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="1.75" dy="5.25"/>
<feGaussianBlur stdDeviation="1.09375"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.01 0"/>
<feBlend mode="normal" in2="effect3_dropShadow_3933_98709" result="effect4_dropShadow_3933_98709"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow_3933_98709" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

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 fill="#636E77">
<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 fill="#FF2500" 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

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" fill="white" 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

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

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 fill="#636E77">
<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 fill="#FF2500" 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.1 KiB

View File

@@ -3,7 +3,7 @@
<div class="operates">
<img
@click="goChat"
src="../assets/images/operates/title.png"
src="../assets/images/operates/title.svg"
class="titleImg pointer"
alt=""
/>
@@ -14,14 +14,39 @@
<span :style="{ fontFamily:item.operateItemed ? 'PingFangSC-Blod' : '' }">{{item.name}}</span>
</div>
<el-divider />
<div class="externalLinksBlock">
<div
v-for="link in externalLinks"
:key="link.name"
class="extItem"
:title="link.name + ' (新窗口打开)'"
@click="openExtLink(link.url)"
>
<span class="extName">{{ link.name }}</span>
</div>
</div>
<el-divider />
<div class="operateBottom">
<img src="../assets/images/operates/user.png" class="userImg" alt="" />
<img
src="../assets/images/operates/quit.png"
@click="quit"
class="quit"
alt=""
/>
<el-popover placement="right" :width="140" trigger="click">
<template #reference>
<img src="../assets/images/operates/user.png" class="userImg pointer" alt="" />
</template>
<div class="userMenu">
<div class="userMenuItem" @click="goProfile">
<el-icon><User /></el-icon>
<span>个人中心</span>
</div>
<div v-if="isAdmin" class="userMenuItem" @click="goUserManage">
<el-icon><Setting /></el-icon>
<span>用户管理</span>
</div>
<el-divider style="margin: 6px 0" />
<div class="userMenuItem danger" @click="quit">
<el-icon><SwitchButton /></el-icon>
<span>退出登录</span>
</div>
</div>
</el-popover>
</div>
</div>
</div>
@@ -29,10 +54,12 @@
<script setup lang='ts'>
import { useAuthStore } from "@/store";
import { reactive, watch } from "vue";
import { reactive, watch, computed } from "vue";
import { useRouter, useRoute } from "vue-router";
import { User, Setting, SwitchButton, TopRight } from "@element-plus/icons-vue";
const authStore = useAuthStore();
const isAdmin = computed(() => authStore.session?.admind === true);
const router = useRouter();
const route = useRoute();
const menuList = reactive([
@@ -74,11 +101,30 @@ const menuList = reactive([
]);
// 外部系统快捷入口(侧栏底部)
const externalLinks = [
{ name: '钢研门户', url: 'http://192.168.203.15:8030/zk/index' },
{ name: '项目管理', url: 'http://192.168.203.23:8080/' },
{ name: '数据中心', url: 'http://192.168.203.21:8600/kd' },
];
const openExtLink = (url: string) => {
window.open(url, '_blank');
};
const quit = () => {
authStore.removeToken();
router.push("/login");
};
const goProfile = () => {
router.push("/profile");
};
const goUserManage = () => {
router.push("/userManage");
};
/**
* 返回首页
*/
@@ -135,8 +181,9 @@ watch(
margin-left: 20px;
.operates {
width: 76px;
height: 657px;
max-height: 100vh;
min-height: 600px;
max-height: calc(100vh - 40px);
overflow-y: auto;
background: #fafbff;
box-shadow: 0px 0px 8px 1px rgba(180, 189, 221, 0.56);
position: absolute;
@@ -145,7 +192,8 @@ watch(
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
justify-content: flex-start;
gap: 55px;
z-index: 999;
.titleImg {
width: 44px;
@@ -245,6 +293,29 @@ watch(
background-size: 100%;
}
}
.externalLinksBlock {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
.extItem {
cursor: pointer;
color: #606771;
padding: 6px 6px;
border-radius: 6px;
text-align: center;
white-space: nowrap;
transition: all 0.2s;
background: rgba(0, 78, 160, 0.05);
.extName {
font-size: 12px;
}
&:hover {
background: rgba(0, 78, 160, 0.14);
color: #004ea0;
}
}
}
.operateBottom {
display: flex;
flex-direction: column;
@@ -253,17 +324,33 @@ watch(
width: 42px;
height: 42px;
}
.quit {
margin-top: 36px;
width: 18.85px;
height: 20px;
cursor: pointer;
}
}
.el-divider--horizontal {
margin: 0px;
margin: -40px;
width: 28px;
}
}
}
</style>
<style lang="scss">
.userMenu {
.userMenuItem {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
color: #333;
&:hover {
background: #f0f2f5;
color: #004ea0;
}
&.danger:hover {
color: #f56c6c;
}
}
}
</style>

View File

@@ -1,3 +1,4 @@
export const PROJECT_PROPERTIES = {
projectName: '知冶模型'
projectName: '战知',
fullProjectName: '中国产业基础能力发展战略研究院战知大模型'
}

View File

@@ -28,8 +28,8 @@ export default {
success: '操作成功',
failed: '操作失败',
unauthorizedTips: '未经授权,请先进行验证。',
logo: '知冶大模型',
siteInfo: '© 2023 知冶模型 _ 浪潮软件科技有限公司',
logo: '知',
siteInfo: '© 2026 战知 _ 浪潮软件科技有限公司',
userXieyi: '用户协议',
privacyZhengce: '隐私政策',
cueWord1: '您好,我是你的报告撰写智能小助手,快来告诉我你的需要吧~',
@@ -46,9 +46,9 @@ export default {
noChatFlow: '无对话历史'
},
login: {
title: '知海无涯搜万象',
title2:'冶技卓越析微尘',
subTitle: '支持多轮对话,集成冶金专业搜索,具备自建知识库、内容创作、信息归纳总结等能力',
title: '聚尖端之力,创多维平台',
title2:'',
subTitle: '聚合科技动能,扩展创新疆界,引领行业跃迁升级',
quickStart: '快速开始',
telPlaceholder: '请输入手机号',
passwordPlaceholder: '请输入密码',

View File

@@ -39,6 +39,16 @@ const routes: RouteRecordRaw[] = [
name: 'Application',
component: () => import('@/views/applications/index.vue'),
},
{
path: '/profile',
name: 'Profile',
component: () => import('@/views/profile/index.vue'),
},
{
path: '/userManage',
name: 'UserManage',
component: () => import('@/views/userManage/index.vue'),
},
{
path: '/translate',
name: 'Translate',

View File

@@ -12,6 +12,7 @@ interface SessionResponse {
password: string | null
context: boolean | false
num: number | 0
admind: boolean | false
}
export interface AuthState {

View File

@@ -1,422 +1,221 @@
<template>
<div class="tools-page">
<div class="tools-container">
<div class="top-level" ref="topLevel">
<template v-for="(item, index) in topSpecies">
<div class="species-name" :class="activeTopSpecieIndex === index ? 'active' : ''"
@click="changeTopSpecies(index)">{{ item.name }}
</div>
</template>
<div class="tools-header">
<span class="tools-title">实用工具</span>
</div>
<div class="second-level" ref="secondLevel">
<template v-for="(item, index) in secondSpecies">
<div class="species-name" :class="activeSecondSpecieIndex === index ? 'active' : ''"
@click="changeSecondSpecies(index)">{{ item.name }}
<div v-for="category in toolCategories" :key="category.name" class="category-section">
<div class="category-name">{{ category.name }}</div>
<div class="tools-list">
<div v-for="tool in category.tools" :key="tool.id" class="tool-item"
:class="{ disabled: !tool.enabled }" @click="openTool(tool)">
<div class="tool-icon">
<img v-if="tool.logoImg" :src="tool.logoImg" class="tool-logo-img" alt="">
<span v-else>{{ tool.icon }}</span>
</div>
</template>
</div>
<div class="tools-info" :style="toolsHeight != '' ? 'max-height:'+toolsHeight:''">
<template v-for="tool in tools">
<div class="level-name" v-if="tool.topLevel">{{ tool.topLevel}}</div>
<div class="tools">
<template v-for="item in tool.data">
<div class="tool-item" @click="goToLink(item)">
<img :src="getToolsImg(item.imgUrl)" alt="">
<div class="tool-info">
<div class="top">
<div class="name">{{ item.name }}</div>
<div class="type" v-if="item.tag && activeTopSpecieIndex === 0">{{ item.tag }}</div>
<div class="tool-top">
<div class="tool-name">{{ tool.name }}</div>
<div class="tool-badge" v-if="!tool.enabled">即将上线</div>
</div>
<div class="bottom">
<div class="desc" :title="item.description"> {{ item.description }}</div>
<img src="@/assets/images/applications/right.png" alt="">
<div class="tool-desc">{{ tool.description }}</div>
</div>
<img v-if="tool.enabled" src="@/assets/images/applications/right.png" class="tool-arrow" alt="">
</div>
</div>
</div>
</template>
</div>
</template>
</div>
</div>
<page-footer></page-footer>
</template>
<script setup lang="ts">
import {onMounted, onUnmounted, reactive, ref} from "vue";
import {withLoading} from "@/utils/loading";
import {getConfig, getTagConfig, viewUrl} from "@/api";
import PageFooter from "@/components/pageFooter.vue";
import {ElMessage} from "element-plus";
import { reactive } from "vue";
import stirlingLogo from "@/assets/images/applications/stirling-pdf.svg";
import excalidrawLogo from "@/assets/images/applications/excalidraw.svg";
import trwebocrLogo from "@/assets/images/applications/trwebocr.png";
import pptistLogo from "@/assets/images/applications/pptist.png";
import libretranslateLogo from "@/assets/images/applications/libretranslate.svg";
// 后端接口的上下文
const baseURL = import.meta.env.VITE_GLOB_API_CTX;
interface Tool {
id: string;
name: string;
icon: string;
description: string;
url?: string;
port?: number;
path?: string;
logoImg?: string;
enabled: boolean;
type: 'newtab' | 'route';
}
// 请求参数类型
type RequestParams = {
type?: string;
sort?: string;
tag?: string;
className?: string;
interface Category {
name: string;
tools: Tool[];
}
const toolCategories = reactive<Category[]>([
{
name: '📄 文档处理',
tools: [
{ id: 'stirling-pdf', name: 'Stirling PDF', icon: '', logoImg: stirlingLogo, description: 'PDF 合并、拆分、压缩、转换、加水印、OCR 识别等 30+ 功能', path: '/pdf/', enabled: true, type: 'newtab' },
{ id: 'trwebocr', name: '图片转文字 OCR', icon: '', logoImg: trwebocrLogo, description: '中文离线 OCR识别图片中的文字和表格', port: 18083, enabled: true, type: 'newtab' },
{ id: 'libretranslate', name: '智能翻译', icon: '', logoImg: libretranslateLogo, description: '中英互译,支持文本和文件翻译,数据不出内网', path: '/translate/', enabled: true, type: 'newtab' },
]
},
{
name: '🖼️ 图片处理',
tools: [
{ id: 'imgcompress', name: '图片压缩转换', icon: '🗜️', description: '图片压缩、格式转换、批量处理、AI 智能抠图去背景', path: '/imgcompress/', enabled: true, type: 'newtab' },
{ id: 'lama-cleaner', name: 'AI 图像擦除', icon: '🧹', description: 'AI 智能擦除图片中的水印、路人、瑕疵,自动修复画面', path: '/lama/', enabled: true, type: 'newtab' },
{ id: 'webp2jpg', name: '图片格式转换', icon: '🔄', description: '支持 PSD/HEIC/WebP/PNG/JPG 等格式批量互转,纯浏览器处理不上传', path: '/webp2jpg/', enabled: true, type: 'newtab' },
]
},
{
name: '🎨 创作绘图',
tools: [
{ id: 'excalidraw', name: 'Excalidraw', icon: '', logoImg: excalidrawLogo, description: '手绘风格白板,绘制流程图、架构图、示意图', path: '/draw/', enabled: true, type: 'newtab' },
{ id: 'pptist', name: 'AI PPT 编辑器', icon: '', logoImg: pptistLogo, description: 'AI 生成 PPT 大纲和演示文稿,在线编辑演示', path: '/ppt/', enabled: true, type: 'newtab' },
]
},
{
name: '📝 科研写作',
tools: [
{ id: 'overleaf', name: 'LaTeX 论文编辑器', icon: '📐', description: '在线 LaTeX 编辑器,支持多人协作撰写论文,实时编译预览', path: '/overleaf/', enabled: true, type: 'newtab' },
{ id: 'latex-formula', name: 'LaTeX 公式编辑器', icon: '∑', description: '在线编辑数学公式,支持希腊字母、矩阵、积分等,可复制公式或图片', path: '/latex/', enabled: true, type: 'newtab' },
]
},
]);
const TOOL_PORT = 18000;
const openTool = (tool: Tool) => {
if (!tool.enabled) return;
let url = '';
if (tool.path) {
url = `${window.location.protocol}//${window.location.hostname}:${TOOL_PORT}${tool.path}`;
} else if (tool.port) {
url = `${window.location.protocol}//${window.location.hostname}:${tool.port}`;
}
if (url) {
window.open(url, '_blank');
}
};
// 一级导航
const topSpecies = reactive<any[]>([]);
const activeTopSpecieIndex = ref<number>(0);
// 二级导航
const secondSpecies = reactive<any[]>([]);
const activeSecondSpecieIndex = ref<number>(-1);
// 工具
type ToolInfo = {
imgUrl: string,
name: string,
description: string,
tag: string
}
type Tool = {
topLevel?: string;
data: ToolInfo[];
}
const tools = reactive<Tool[]>([]);
// 工具模块高度自适应相关
const topLevel = ref<any>(null);
const secondLevel = ref<any>(null);
const toolsHeight = ref('');
/**
* 获取工具广场信息
* 适用于获取一级导航,二级导航,以及实用工具下除项目管理、知识管理、研究实施、运营推广外的内容
* @param params
*/
const getConfigList = async (params: RequestParams) => {
try {
let res = await withLoading(getConfig)(params);
return res.code === 200 && res.data ? res.data : [];
} catch (error: any) {
ElMessage.error(error && error.message ? error.message : '未知错误');
return [];
}
}
/**
* 获取工具广场信息
* 适用于获取实用工具下除项目管理、知识管理、研究实施、运营推广的内容
* @param params
*/
const getTagConfigList = async (params: RequestParams) => {
try {
let res = await withLoading(getTagConfig)(params);
return res.code === 200 && res.data ? res.data : [];
} catch (error: any) {
ElMessage.error(error && error.message ? error.message : '未知错误');
}
}
/**
* 获取一级导航
*/
const getTopSpecies = async () => {
topSpecies.length = 0;
let params: RequestParams = {
type: "广场",
sort: '广场',
tag: ''
}
topSpecies.push(...await getConfigList(params));
}
/**
* 获取二级导航
*/
const getSecondSpecies = async () => {
secondSpecies.length = 0;
if (activeTopSpecieIndex.value === 0) {
let params: RequestParams = {
type: "广场",
sort: topSpecies[activeTopSpecieIndex.value].value,
tag: 'tag'
}
secondSpecies.push(...await getConfigList(params));
}
}
/**
* 获取工具
*/
const getTools = async () => {
tools.length = 0;
if (activeTopSpecieIndex.value === 0 && activeSecondSpecieIndex.value === 0) {
await getTagToolList();
return;
}
await getToolList();
}
const getTagToolList = async () => {
let params: RequestParams = {
className: secondSpecies[activeSecondSpecieIndex.value].value,
}
tools.push(...await getTagConfigList(params));
}
const getToolList = async () => {
let tag = '';
if (activeSecondSpecieIndex.value === 0) {
tag = "NoTag";
} else if (activeSecondSpecieIndex.value != -1) {
tag = secondSpecies[activeSecondSpecieIndex.value].value;
}
let params: RequestParams = {
type: "广场",
sort: topSpecies[activeTopSpecieIndex.value].value,
tag: tag
}
let data = await getConfigList(params);
tools.push({ data: data})
}
/**
* 切换一级导航
*/
const changeTopSpecies = async (index: number) => {
activeTopSpecieIndex.value = index;
if (activeTopSpecieIndex.value === 0) {
activeSecondSpecieIndex.value = 0;
} else {
activeSecondSpecieIndex.value = -1;
}
await getSecondSpecies();
await getTools();
getToolsHeight();
}
/**
* 切换二级导航
* @param index
*/
const changeSecondSpecies = (index: number) => {
if (activeTopSpecieIndex.value !== 0) {
return;
}
activeSecondSpecieIndex.value = index;
getTools();
}
/**
* 拼接工具图片地址
*/
const getToolsImg = (imgUrl: string) => {
return new URL(baseURL + '/file/' + imgUrl, import.meta.url).href
}
/**
* 工具模块高度自适应
*/
const getToolsHeight = () => {
if (activeTopSpecieIndex.value !== 0) {
if (topLevel.value && topLevel.value.offsetHeight !== 0) {
let otherHeight: number = topLevel.value.offsetHeight + 146;
toolsHeight.value = "calc(100vh - " + otherHeight + "px)";
}
} else {
if (topLevel.value && topLevel.value.offsetHeight !== 0 && secondLevel.value && secondLevel.value.offsetHeight !== 0) {
let otherHeight: number = topLevel.value.offsetHeight + secondLevel.value.offsetHeight + 176;
toolsHeight.value = "calc(100vh - " + otherHeight + "px)";
}
}
}
/**
* 链接跳转
* @param item
*/
const goToLink = async (item: any) => {
try {
let res = await withLoading(viewUrl)(Number(item.id));
if (res.code === 200) {
window.open(item.jumpUrl, '_blank');
} else {
ElMessage.error(res.msg);
}
} catch (error) {
ElMessage.error(error && error.message ? error.message : '未知错误');
}
}
onMounted(async () => {
await getTopSpecies();
await changeTopSpecies(0);
getToolsHeight();
window.addEventListener("resize", getToolsHeight);
})
onUnmounted(() => {
window.removeEventListener("resize", getToolsHeight);
})
</script>
<style scoped lang="scss">
.tools-container {
.tools-page {
width: 100%;
padding: 60px 160px 20px 236px;
position: relative;
height: 100%;
.top-level {
display: flex;
margin-bottom: 20px;
overflow: hidden;
}
.species-name {
font-weight: bold;
font-size: 20px;
color: #858A94;
margin-right: 60px;
cursor: pointer;
}
.active {
color: #004EA0;
}
}
.second-level {
display: flex;
flex-wrap: wrap;
margin-bottom: 8px;
.species-name {
width: 89px;
height: 36px;
text-align: center;
line-height: 36px;
background: rgba(133, 138, 150, 0.1);
border-radius: 18px;
border: 1px solid #E6EDFF;
margin-right: 16px;
font-size: 16px;
margin-bottom: 12px;
cursor: pointer;
}
.active {
background: #004EA0;
color: #FAFBFF;
}
}
.tools {
display: flex;
flex-wrap: wrap;
.tools-container {
padding: 24px 40px;
height: 100%;
overflow-y: auto;
.tool-item {
.tools-header {
margin-bottom: 20px;
.tools-title { font-size: 20px; font-weight: bold; color: #000; }
}
}
.category-section {
margin-bottom: 24px;
.category-name {
font-size: 15px;
font-weight: bold;
color: #333;
margin-bottom: 12px;
padding-left: 4px;
}
}
.tools-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.tool-item {
display: flex;
width: 325px;
height: 105px;
align-items: center;
padding: 14px;
background: #E6EDFF;
border-radius: 8px;
border: 1px solid #E6EDFF;
margin-right: 17px;
margin-bottom: 20px;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
img {
width: 73px;
height: 73px;
&:hover:not(.disabled) {
border-color: #004EA0;
.tool-name { color: #004EA0; }
}
&.disabled {
opacity: 0.55;
cursor: not-allowed;
}
.tool-icon {
font-size: 24px;
width: 42px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: 8px;
flex-shrink: 0;
.tool-logo-img { width: 30px; height: 30px; object-fit: contain; }
}
.tool-info {
height: 73px;
margin-left: 12px;
flex: 1;
margin-left: 10px;
min-width: 0;
.top {
.tool-top {
display: flex;
justify-content: space-between;
margin-bottom: 14px;
.name {
width: 128px;
font-weight: bold;
font-size: 16px;
color: #000000;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.type {
width: 64px;
height: 23px;
line-height: 23px;
text-align: center;
font-size: 12px;
color: #1C73CE;
background: rgba(28, 115, 206, 0.11);
border-radius: 11px
}
}
.bottom {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.desc {
width: 185px;
height: 37px;
.tool-name {
font-size: 14px;
font-weight: bold;
color: #000;
}
.tool-badge {
font-size: 10px;
color: #999;
background: rgba(255,255,255,0.7);
padding: 2px 8px;
border-radius: 8px;
}
.tool-desc {
font-size: 12px;
color: #858A94;
display: flex;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
}
}
img {
.tool-arrow {
width: 7px;
height: 14px;
margin-left: 16px
}
}
}
}
.tools-top-level {
font-weight: bold;
font-size: 16px;
color: #000000;
}
}
.tools-info {
overflow-y: auto;
.level-name {
font-weight: bold;
font-size: 16px;
color: #000000;
margin-bottom: 16px;
}
}
.footer {
font-weight: 400;
font-size: 14px;
color: #B9B9B9;
line-height: 30px;
text-align: right;
font-style: normal;
text-transform: none;
position: absolute;
bottom: 2vh;
right: 160px;
a {
color: inherit; /* 继承父元素的文字颜色 */
text-decoration: none; /* 去除下划线 */
cursor: pointer;
}
margin-left: 10px;
flex-shrink: 0;
}
}
</style>

View File

@@ -20,13 +20,10 @@
<div class="chatAll" ref="chatDiv">
<div class="content" v-if="!chatStatus">
<div class="title">
<span class="titleHead"></span>
<span>海无涯</span>
<span class="titleHead" style="margin-left: 40px"></span>
<span>技卓越</span>
<span>聚尖端之力创多维平台</span>
</div>
<div class="titleInfo">
提升信息处理效率促进科研创新优化工艺流程冶金行业AI助手
聚合科技动能扩展创新疆界引领行业跃迁升级
</div>
<div class="operate">
<div class="scenario">

View File

@@ -7,22 +7,16 @@
<div class="loginContent">
<div class="loginTitle">
<div>
知海无涯搜万象
<br />
冶技卓越析微尘
聚尖端之力创多维平台
<br />
<span class="loginInfo">
支持多轮对话集成冶金专业搜索具备自建知识库内容创作信息归纳总结等能力
</span>
<br>
<span class="loginInfo">
已接入DeepSeek-R1满血版
聚合科技动能扩展创新疆界引领行业跃迁升级
</span>
</div>
</div>
<div class="loginOperate">
<img
src="../../assets/images/login/projectLogo2.png"
:src="projectLogo2"
class="logoImg"
alt=""
/>
@@ -69,8 +63,8 @@
</template>
<script setup lang='ts'>
import { computed, ref, onMounted, reactive } from "vue";
import projectLogo from "@/assets/images/login/projectLogo.png";
import projectLogo2 from "@/assets/images/login/projectLogo2.png";
import projectLogo from "@/assets/images/login/projectLogo-white.svg";
import projectLogo2 from "@/assets/images/login/projectLogo.svg";
import type { FormInstance, FormRules } from "element-plus";
import { fetchVerify } from "@/api";
import { useAuthStore } from "@/store";
@@ -144,6 +138,7 @@ const handleVerify = async () => {
} finally {
}
};
</script>
<style scoped lang="scss">
.loginPage {

View File

@@ -0,0 +1,210 @@
<template>
<div class="profileWrap">
<div class="profileCard">
<h2 class="profileTitle">个人中心</h2>
<div class="profileSection">
<div class="avatarSection">
<el-avatar :size="80" :src="userInfo.avatar || defaultAvatar" />
</div>
</div>
<el-divider />
<div class="profileSection">
<h3>基本信息</h3>
<el-form :model="userForm" label-width="80px" style="max-width: 400px">
<el-form-item label="手机号">
<el-input :value="userInfo.tel" disabled />
</el-form-item>
<el-form-item label="姓名">
<el-input v-model="userForm.name" />
</el-form-item>
<el-form-item label="昵称">
<el-input v-model="userForm.nickName" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSaveInfo" :loading="saving">保存</el-button>
</el-form-item>
</el-form>
</div>
<el-divider />
<div class="profileSection">
<h3>修改密码</h3>
<el-form
ref="passwordFormRef"
:model="passwordForm"
:rules="passwordRules"
label-width="80px"
style="max-width: 400px"
>
<el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="passwordForm.oldPassword" type="password" show-password />
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="passwordForm.newPassword" type="password" show-password />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="passwordForm.confirmPassword" type="password" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleChangePassword" :loading="changingPwd">
修改密码
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { fetchSession, updateUser, updatePassword } from '@/api'
import { useAuthStore } from '@/store'
import defaultAvatarImg from '@/assets/images/operates/user.png'
const authStore = useAuthStore()
const defaultAvatar = defaultAvatarImg
const userInfo = reactive({
id: 0,
tel: '',
name: '',
nickName: '',
avatar: '',
})
const userForm = reactive({
name: '',
nickName: '',
})
const passwordForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: '',
})
const passwordFormRef = ref<FormInstance>()
const saving = ref(false)
const changingPwd = ref(false)
const validateConfirm = (rule: any, value: any, callback: any) => {
if (value !== passwordForm.newPassword) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
const passwordRules = reactive<FormRules>({
oldPassword: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码长度至少6位', trigger: 'blur' },
],
confirmPassword: [
{ required: true, message: '请确认新密码', trigger: 'blur' },
{ validator: validateConfirm, trigger: 'blur' },
],
})
const loadUserInfo = async () => {
const res = await fetchSession<any>()
if (res.code === 200) {
Object.assign(userInfo, res.data)
userForm.name = res.data.name || ''
userForm.nickName = res.data.nickName || ''
}
}
const handleSaveInfo = async () => {
saving.value = true
try {
const res = await updateUser<any>({
name: userForm.name,
nickName: userForm.nickName,
})
if (res.code === 200) {
ElMessage.success('信息保存成功')
userInfo.name = userForm.name
userInfo.nickName = userForm.nickName
} else {
ElMessage.error(res.msg || '保存失败')
}
} finally {
saving.value = false
}
}
const handleChangePassword = async () => {
const valid = await passwordFormRef.value?.validate().catch(() => false)
if (!valid) return
changingPwd.value = true
try {
const res = await updatePassword<any>({
oldPassword: passwordForm.oldPassword,
newPassword: passwordForm.newPassword,
})
if (res.code === 200) {
ElMessage.success('密码修改成功,请重新登录')
passwordForm.oldPassword = ''
passwordForm.newPassword = ''
passwordForm.confirmPassword = ''
authStore.removeToken()
window.location.hash = '#/login'
} else {
ElMessage.error(res.msg || '密码修改失败')
}
} finally {
changingPwd.value = false
}
}
onMounted(() => {
loadUserInfo()
})
</script>
<style lang="scss" scoped>
.profileWrap {
flex: 1;
padding: 30px;
overflow-y: auto;
z-index: 1;
}
.profileCard {
max-width: 600px;
margin: 0 auto;
background: #fff;
border-radius: 12px;
padding: 30px 40px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
.profileTitle {
font-size: 20px;
color: #333;
margin-bottom: 24px;
}
.profileSection {
h3 {
font-size: 16px;
color: #333;
margin-bottom: 16px;
}
}
.avatarSection {
display: flex;
align-items: center;
gap: 20px;
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<div class="userManageWrap">
<div class="userManageCard">
<div class="headerRow">
<h2>用户管理</h2>
<el-button type="primary" @click="handleAdd">新增用户</el-button>
</div>
<el-table :data="tableData" v-loading="loading" stripe style="width: 100%">
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="tel" label="手机号" width="140" />
<el-table-column prop="name" label="姓名" width="140" />
<el-table-column prop="nickName" label="昵称" width="140" />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
{{ row.status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" min-width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button link type="warning" size="small" @click="handleResetPwd(row)">重置密码</el-button>
<el-popconfirm title="确定删除该用户?" @confirm="handleDelete(row)">
<template #reference>
<el-button link type="danger" size="small">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="paginationRow">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="loadUsers"
@current-change="loadUsers"
/>
</div>
</div>
<!-- 新增/编辑用户弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增用户' : '编辑用户'"
width="460px"
destroy-on-close
>
<el-form
ref="userFormRef"
:model="userForm"
:rules="userRules"
label-width="80px"
>
<el-form-item label="手机号" prop="tel">
<el-input v-model="userForm.tel" :disabled="dialogType === 'edit'" />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model="userForm.name" />
</el-form-item>
<el-form-item label="昵称">
<el-input v-model="userForm.nickName" />
</el-form-item>
<el-form-item v-if="dialogType === 'add'" label="密码" prop="password">
<el-input v-model="userForm.password" type="password" show-password />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
</template>
</el-dialog>
<!-- 重置密码弹窗 -->
<el-dialog v-model="resetPwdVisible" title="重置密码" width="400px" destroy-on-close>
<el-form ref="resetPwdFormRef" :model="resetPwdForm" :rules="resetPwdRules" label-width="80px">
<el-form-item label="用户">
<el-input :value="resetPwdForm.tel" disabled />
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="resetPwdForm.newPassword" type="password" show-password />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="resetPwdVisible = false">取消</el-button>
<el-button type="primary" @click="handleResetPwdSubmit" :loading="resettingPwd">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { fetchUserPage, createUser, editUser, deleteUser, resetUserPassword } from '@/api'
const tableData = ref<any[]>([])
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 新增/编辑
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const userFormRef = ref<FormInstance>()
const submitting = ref(false)
const userForm = reactive({
id: 0,
tel: '',
name: '',
nickName: '',
password: '',
})
const userRules = reactive<FormRules>({
tel: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度至少6位', trigger: 'blur' },
],
})
// 重置密码
const resetPwdVisible = ref(false)
const resetPwdFormRef = ref<FormInstance>()
const resettingPwd = ref(false)
const resetPwdForm = reactive({
id: 0,
tel: '',
newPassword: '',
})
const resetPwdRules = reactive<FormRules>({
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码长度至少6位', trigger: 'blur' },
],
})
const loadUsers = async () => {
loading.value = true
try {
const res = await fetchUserPage<any>({
current: currentPage.value,
size: pageSize.value,
})
if (res.code === 200) {
tableData.value = res.data.records || []
total.value = res.data.total || 0
}
} catch (e: any) {
console.error('加载用户列表失败:', e)
ElMessage.error('加载用户列表失败')
} finally {
loading.value = false
}
}
const handleAdd = () => {
dialogType.value = 'add'
userForm.id = 0
userForm.tel = ''
userForm.name = ''
userForm.nickName = ''
userForm.password = ''
dialogVisible.value = true
}
const handleEdit = (row: any) => {
dialogType.value = 'edit'
userForm.id = row.id
userForm.tel = row.tel
userForm.name = row.name || ''
userForm.nickName = row.nickName || ''
userForm.password = ''
dialogVisible.value = true
}
const handleSubmit = async () => {
const valid = await userFormRef.value?.validate().catch(() => false)
if (!valid) return
submitting.value = true
try {
let res: any
if (dialogType.value === 'add') {
res = await createUser<any>({
tel: userForm.tel,
name: userForm.name,
nickName: userForm.nickName,
password: userForm.password,
})
} else {
res = await editUser<any>({
id: userForm.id,
name: userForm.name,
nickName: userForm.nickName,
})
}
if (res.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '编辑成功')
dialogVisible.value = false
loadUsers()
} else {
ElMessage.error(res.msg || '操作失败')
}
} finally {
submitting.value = false
}
}
const handleResetPwd = (row: any) => {
resetPwdForm.id = row.id
resetPwdForm.tel = row.tel
resetPwdForm.newPassword = ''
resetPwdVisible.value = true
}
const handleResetPwdSubmit = async () => {
const valid = await resetPwdFormRef.value?.validate().catch(() => false)
if (!valid) return
resettingPwd.value = true
try {
const res = await resetUserPassword<any>({
id: resetPwdForm.id,
newPassword: resetPwdForm.newPassword,
})
if (res.code === 200) {
ElMessage.success('密码重置成功')
resetPwdVisible.value = false
} else {
ElMessage.error(res.msg || '重置失败')
}
} finally {
resettingPwd.value = false
}
}
const handleDelete = async (row: any) => {
const res = await deleteUser<any>(row.id)
if (res.code === 200) {
ElMessage.success('删除成功')
loadUsers()
} else {
ElMessage.error(res.msg || '删除失败')
}
}
onMounted(() => {
loadUsers()
})
</script>
<style lang="scss" scoped>
.userManageWrap {
flex: 1;
padding: 30px;
overflow-y: auto;
z-index: 1;
}
.userManageCard {
background: #fff;
border-radius: 12px;
padding: 24px 30px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
.headerRow {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
font-size: 20px;
color: #333;
margin: 0;
}
}
.paginationRow {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>

View File

@@ -35,7 +35,16 @@ export default defineConfig((env) => {
[viteEnv.VITE_GLOB_API_CTX]: {
target: viteEnv.VITE_GLOB_API_DEV_IP,
changeOrigin: true,
}
},
// 工具服务通过 Nginx(:18000) 反代sub_filter 处理子资源路径
'/pdf/': {
target: 'http://localhost:18000',
changeOrigin: true,
},
'/draw/': {
target: 'http://localhost:18000',
changeOrigin: true,
},
},
},
build: {

View File

@@ -94,8 +94,8 @@ DUPLICATE_THRESHOLD = 0.98
# 默认搜索引擎。可选bing, duckduckgo, metaphor
# DEFAULT_SEARCH_ENGINE = "duckduckgo"
DEFAULT_SEARCH_ENGINE = "duckduckgo" # 本地未部署 KGO 搜索时用 duckduckgo自建搜索后再改为 kgo
kgo_search_url = r"http://127.0.0.1:10326/search/search" # 若部署 KGO 搜索服务可改端口
kgo_professional_search_url = r"http://127.0.0.1:8327/search/professionalSearch"
kgo_search_url = r"http://192.168.203.21:8326/search/search" # 若部署 KGO 搜索服务可改端口
kgo_professional_search_url = r"http://192.168.203.21:8326/search/professionalSearch"
# 画图接口
realistic_url = r"http://127.0.0.1:5000/generate"

View File

@@ -2518,62 +2518,17 @@ PROMPT_TEMPLATES = {
PROMPT_ABSTRACT = {
"llm_chat": {
"policy_standard_chat": {"title": "政策问答",
"desc": "提供各行业最新的政策、法规等信息。",
"prompt": "您可以向我咨询行业的政策,我将为您提供信息,并给出解读建议。",
"type": "行业专业问答"},
"Topic Recommend Assistant": {"title": "选题推荐",
"desc": "聚焦十四五规划、国内前沿会议,提供项目选题推荐服务。",
"prompt": "请给我提供一个方向或一个主题,我将为您推荐相关的选题。(默认推荐一个)",
"type": "项目选题"},
"Policy History Assistant": {"title": "政策脉络",
"desc": "梳理政策脉络,支撑立项和研究工作。",
"prompt": "输入政策的具体名称或提供一个主题,我将为您提供其背后的历史背景和发展脉络。",
"type": "政策分析挖掘"},
# "process_flow": {"title": "工艺流程问答",
# "desc": "针对特定工艺的详细流程进行回答。",
# "prompt": "您可以向我提问有关产品的产能、产量、进出口、技术经济指标等工艺流程的问题,我将给您提供相关的答案。",
# "type": "行业专业问答"},
"concept_explain": {"title": "术语解释",
"desc": "提供各领域专业术语的即时解释。",
"prompt": "您可以直接输入任何领域的专业术语,我都将为您进行专业全面的解释",
"type": "行业专业问答"},
"statistic_search": {"title": "智能问数",
"desc": "将自然语言智能转换成数据库检索SQL查询相关数据并回答问题。",
"prompt": "您可以直接问我问题,我将基于全球统计数据库的结构化数据来给您回答。",
"type": "行业专业问答"},
}
,
"knowledge_base_chat": {
"policy_standard_chat": {"title": "政策问答",
"desc": "提供各行业最新的政策、法规等信息。",
"prompt": "您可以向我咨询行业的政策,我将为您提供信息,并给出解读建议。",
"type": "行业专业问答"},
"Topic Recommend Assistant": {"title": "选题推荐",
"desc": "聚焦十四五规划、国内前沿会议,提供项目选题推荐服务。",
"prompt": "请给我提供一个方向或一个主题,我将为您推荐相关的选题。(默认推荐一个)",
"type": "项目选题"},
"Policy History Assistant": {"title": "政策脉络",
"desc": "梳理政策脉络,支撑立项和研究工作。",
"prompt": "输入政策的具体名称或提供一个主题,我将为您提供其背后的历史背景和发展脉络。",
"type": "政策分析挖掘"},
# "process_flow": {"title": "工艺流程问答",
# "desc": "针对特定工艺的详细流程进行回答。",
# "prompt": "您可以向我提问有关产品的产能、产量、进出口、技术经济指标等工艺流程的问题,我将给您提供相关的答案。",
# "type": "行业专业问答"},
"concept_explain": {"title": "术语解释",
"desc": "提供各领域专业术语的即时解释。",
"prompt": "您可以直接输入任何领域的专业术语,我都将为您进行专业全面的解释",
"type": "行业专业问答"},
},
}

View File

@@ -1,99 +0,0 @@
#!/bin/bash
# 确保使用 bash 运行
if [ -z "$BASH_VERSION" ]; then
exec bash "$0" "$@"
fi
# 颜色定义
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# 打印带颜色的消息
print_yellow() { printf "${YELLOW}%s${NC}\n" "$1"; }
print_green() { printf "${GREEN}%s${NC}\n" "$1"; }
print_red() { printf "${RED}%s${NC}\n" "$1"; }
print_yellow "=== 停止 7861 和 8501 端口服务 ==="
for port in 7861 8501; do
pids=$(lsof -t -i:"$port" 2>/dev/null)
if [ -n "$pids" ]; then
print_yellow "正在停止端口 $port 的进程: $pids"
kill -9 $pids 2>/dev/null
print_green "端口 $port 已停止"
else
echo "端口 $port 无运行中的服务"
fi
done
# 也停止所有 startup.py 进程
pids=$(ps aux | grep "[p]ython.*startup.py -a" | awk '{print $2}')
if [ -n "$pids" ]; then
print_yellow "正在停止 startup.py 进程: $pids"
kill -9 $pids 2>/dev/null
print_green "startup.py 进程已停止"
fi
echo ""
print_yellow "=== 启动服务 ==="
cd /home/gc/gangyan/langchain-chat
# 初始化 conda确保 PATH 中包含 conda 路径)
CONDA_INIT="/root/miniconda3/etc/profile.d/conda.sh"
if [ -f "$CONDA_INIT" ]; then
# 初始化 conda将 conda 路径添加到 PATH
. "$CONDA_INIT" 2>/dev/null || source "$CONDA_INIT" 2>/dev/null
fi
# 查找 conda 可执行文件(按优先级)
if command -v conda &> /dev/null; then
CONDA_EXE="conda"
elif [ -f "/root/miniconda3/bin/conda" ]; then
CONDA_EXE="/root/miniconda3/bin/conda"
elif [ -f "/root/miniconda3/condabin/conda" ]; then
CONDA_EXE="/root/miniconda3/condabin/conda"
else
print_red "错误: 未找到 conda 命令"
print_red "请检查 conda 是否已安装: /root/miniconda3"
exit 1
fi
# 使用 conda 环境启动
print_yellow "使用环境: gangyan"
print_yellow "日志文件: nohup.out"
print_yellow "Conda路径: $CONDA_EXE"
# 获取 python 的完整路径
PYTHON_EXE="/root/miniconda3/envs/gangyan/bin/python"
if [ ! -f "$PYTHON_EXE" ]; then
# 尝试通过 conda run 获取路径
PYTHON_EXE="$($CONDA_EXE run -n gangyan which python 2>/dev/null)"
if [ -z "$PYTHON_EXE" ] || [ ! -f "$PYTHON_EXE" ]; then
print_red "错误: 无法找到 python 可执行文件"
print_red "请检查 conda 环境 gangyan 是否已安装"
exit 1
fi
fi
print_yellow "Python路径: $PYTHON_EXE"
# 直接使用 python 完整路径启动(不依赖 conda activate更可靠
# 设置 PYTHONPATH 确保能正确导入模块
export PYTHONPATH="/home/gc/gangyan/langchain-chat:$PYTHONPATH"
cd /home/gc/gangyan/langchain-chat
nohup "$PYTHON_EXE" startup.py -a >> nohup.out 2>&1 &
PID=$!
print_green "服务已启动PID: $PID"
print_yellow "日志文件: /home/gc/gangyan/langchain-chat/nohup.out"
print_yellow "查看日志: tail -f nohup.out"
# 等待几秒后显示日志
sleep 2
echo ""
print_yellow "=== 最近日志 ==="
tail -20 nohup.out

View File

@@ -1,100 +1,92 @@
import asyncio
import re
import aiohttp
import json
import logging
import requests
from pydantic import BaseModel, Field
from server.chat import utils
# 配置日志记录器
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__)
async def duckduckgo_search_iter(query: str, uuid: str = "",time: str = "", resource_type: str = None, limit: int = 3):
# 定义三个API的URL
text_url = 'http://43.251.225.121/inspur/search_text'
video_url = 'http://43.251.225.121/inspur/search_video'
news_url = 'http://43.251.225.121/inspur/search_new'
# 新接口:内网 searxng 服务(原 43.251.225.121 已下线)
# aiohttp 与该 searxng 配合会 30s 超时(疑似 header/UA 被拦),所以改用 requests。
SEARXNG_URL = 'http://118.196.92.255/searxng/search'
SEARXNG_HEADERS = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) gangyan-langchain'}
payload = {
"query": query,
"time": time
}
async def fetch(session, url, json_payload,limit):
logger.info(f"{url} 获取数据,请求参数: {json_payload}")
def _searxng_results_to_items(results, mapping, limit):
"""把 searxng 统一的 {url,title,content} 映射成老接口期望的字段格式"""
out = []
for r in results[:limit]:
title = r.get('title', '') or ''
url = r.get('url', '') or ''
content = r.get('content', '') or ''
item = {}
for dst_key, src in mapping.items():
if src == 'title':
item[dst_key] = title
elif src == 'url':
item[dst_key] = url
elif src == 'content':
item[dst_key] = content
out.append(item)
return out
def _sync_fetch(params, limit_n, kind):
logger.info(f"searxng 请求 {kind}: params={params}")
try:
json_payload["limit"] = limit
async with session.post(url, json=json_payload) as response:
if response.status != 200:
logger.error(f"{url} 请求失败,状态码 {response.status}")
data = await response.json()
logger.info(f"{url} 获取的资料数: {len(data) if isinstance(data, list) else '未知'}")
return data
r = requests.get(SEARXNG_URL, params=params, headers=SEARXNG_HEADERS, timeout=15)
if r.status_code != 200:
logger.error(f"searxng {kind} HTTP {r.status_code}")
return []
data = r.json()
results = data.get('results', []) if isinstance(data, dict) else []
logger.info(f"searxng {kind} 条数: {len(results)}")
if kind == 'text':
return _searxng_results_to_items(results, {'title': 'title', 'href': 'url', 'body': 'content'}, limit_n)
if kind == 'video':
return _searxng_results_to_items(results, {'title': 'title', 'content': 'url', 'description': 'content'}, limit_n)
if kind == 'news':
return _searxng_results_to_items(results, {'title': 'title', 'url': 'url', 'body': 'content'}, limit_n)
return []
except Exception as e:
logger.error(f"获取 {url} 数据时发生错误: {e}")
logger.error(f"searxng {kind} 请求异常: {type(e).__name__}: {e}")
return []
# 根据 resource_type 确定要请求的 API
# 默认并发请求三个API
# 视频只请求 video_url
# 新闻只请求 news_url
# 其他类型只请求 text_url
async with aiohttp.ClientSession() as session:
logger.info("发起请求duckduckgo...")
async def duckduckgo_search_iter(query: str, uuid: str = "", time: str = "", resource_type: str = None, limit: int = 3):
logger.info("发起 searxng 搜索请求...")
# 三类按 limit 平均分配
n = limit % 3
limit1 = 0
limit2 = 0
limit3 = 0
match n:
case 0:
limit1 = limit//3
limit2 = limit1
limit3 = limit1
case 1:
limit1 = limit//3 +1
limit2 = limit//3
limit3 = limit2
case 2:
limit1 = limit//3 +1
limit2 = limit1
limit2 = limit
if n == 0:
limit1 = limit2 = limit3 = limit // 3
elif n == 1:
limit1 = limit // 3 + 1
limit2 = limit3 = limit // 3
else:
limit1 = limit2 = limit // 3 + 1
limit3 = limit // 3
if resource_type is None or not resource_type == 'video':
text_task = asyncio.create_task(fetch(session, text_url, payload,limit1))
video_task = asyncio.create_task(fetch(session, video_url, payload, limit3))
news_task = asyncio.create_task(fetch(session, news_url, payload, limit2))
text_result, video_result, news_result = await asyncio.gather(text_task, video_task, news_task)
logger.info("合并结果...")
logger.info("合并结果完成")
if resource_type is None or resource_type != 'video':
text_task = asyncio.to_thread(_sync_fetch, {'q': query, 'format': 'json', 'categories': 'general'}, limit1, 'text')
news_task = asyncio.to_thread(_sync_fetch, {'q': query, 'format': 'json', 'categories': 'news'}, limit2, 'news')
video_task = asyncio.to_thread(_sync_fetch, {'q': query, 'format': 'json', 'categories': 'videos'}, limit3, 'video')
text_result, news_result, video_result = await asyncio.gather(text_task, news_task, video_task)
combined_result = {
"text": text_result,
"video": video_result,
"news": news_result
"news": news_result,
}
else:
video_result = await fetch(session, video_url, payload, limit)
combined_result = {
"video": video_result
}
del limit1,limit2,limit3
# elif resource_type == 'news':
# news_result = await fetch(session, news_url, payload)
# combined_result = {
# "news": news_result
# }
video_result = await asyncio.to_thread(_sync_fetch, {'q': query, 'format': 'json', 'categories': 'videos'}, limit, 'video')
combined_result = {"video": video_result}
# else: # 其他类型
# text_result = await fetch(session, text_url, payload)
# combined_result = {
# "text": text_result
# }
logger.info("searxng 请求已完成")
logger.info("请求已完成")
res = []
source = []
info = utils.get_shared_variable(uuid)
@@ -116,71 +108,41 @@ async def duckduckgo_search_iter(query: str, uuid: str = "",time: str = "", reso
source.append(f'资料[{index}] [{item["title"]}]({item["url"]})')
info["source_docs"].extend(source)
utils.set_shared_variable(uuid, info)
return res,source
return res, source
def duckduckgo_search(query: str, time: str = "", resource_type: str = None):
logger.info(f"模型输入: {query}")
# 对传入的 query 字段进行解析
# 判断 query 是否包含 "}{"
# if "}{" in query:
# # 将 query 分割为两个JSON字符串
# split_index = query.find("}{")
# json_part1 = query[:split_index+1]
# json_part2 = query[split_index+1:]
# try:
# obj1 = json.loads(json_part1)
# obj2 = json.loads(json_part2)
# # 提取 query, resource_type, time, uuid
# parsed_query = obj1.get("query", "")
# parsed_resource_type = obj1.get("resource_type", None)
# parsed_time = obj1.get("time", time) # 如obj1未包含time则使用传入的默认值
# parsed_uuid = obj2.get("uuid", "")
matches = re.findall(r'\{.*?\}', query)
if len(matches)>=2:
if len(matches) >= 2:
query = matches[0]
else:
return "<关键指令>不需要再调用该工具了</关键指令>"
parsed_uuid = ""
parsed_limit = 3
try:
obj1= json.loads(query)
obj1 = json.loads(query)
parsed_query = obj1.get("query", "")
parsed_limit = obj1.get("limit", 3)
parsed_resource_type = obj1.get("resource_type", None)
parsed_time = obj1.get("time", time) # 如obj1未包含time则使用传入的默认值
parsed_time = obj1.get("time", time)
parsed_uuid = json.loads(matches[1])["uuid"]
# 将解析到的值覆盖原有的参数
query = parsed_query if parsed_query else query
resource_type = parsed_resource_type if parsed_resource_type else resource_type
time = parsed_time if parsed_time else time
logger.info(f"解析完成query: {query}, uuid: {parsed_uuid}, time: {time}, resource_type: {resource_type}, parsed_limit: {parsed_limit}")
except json.JSONDecodeError as e:
logger.error(f"解析JSON出错: {e}")
# 在同步环境中运行异步函数
combined_result = asyncio.run(duckduckgo_search_iter(query, parsed_uuid, time, resource_type, parsed_limit))
# 以标准json格式输出
logger.info("返回JSON格式的结果给到模型...")
return combined_result
class DuckduckgoInput(BaseModel):
location: str = Field(description="网络搜索查询")
if __name__ == "__main__":
# 测试调用
# 1. 默认请求三个API
# result_default = duckduckgo_search("粉末冶金", "m", "default")
# print("duckduckgo输出(默认):\n", result_default)
# # 2. 只请求视频
# result_video = duckduckgo_search("粉末冶金", "m", "video")
# print("duckduckgo输出(视频):\n", result_video)
# # 3. 只请求新闻
# result_news = duckduckgo_search("粉末冶金", "m", "news")
# print("duckduckgo输出(新闻):\n", result_news)
# 4. 其它类型只请求文本
result_other = duckduckgo_search("粉末冶金", "m", "other")
print("duckduckgo输出(其他):\n", result_other)
result_other = duckduckgo_search('{"query":"粉末冶金","limit":3}{"uuid":"test-uuid"}', "m", "other")
print("searxng输出(其他):\n", result_other)

View File

@@ -70,7 +70,7 @@ class ZhipuSearchAPIWrapper:
)
logging.info(f"Zhipu检索内容:{search_query}")
url = "http://ywk3hvt4d:01Jp2V1tR9PdTsYSz919779Rb9_@134.122.191.214/search"
url = "http://118.196.92.255/searxng/search"
engines = "duckduckgo,bing"
data = {
"format":"json",

View File

@@ -1,2 +0,0 @@
# mac设备上的grep命令可能不支持grep -P选项请使用Homebrew安装;或使用ggrep命令
ps -eo pid,user,cmd|grep -P 'server/api.py|webui.py|fastchat.serve|langchain_chat'|grep -v grep|awk '{print $1}'|xargs kill -9

View File

@@ -1,60 +0,0 @@
#!/bin/bash
# 启动 Python 后端服务脚本(使用 nohup
# 颜色定义
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# 检查是否已有进程在运行
EXISTING=$(ps aux | grep "[p]ython.*startup.py -a" | awk '{print $2}')
if [ -n "$EXISTING" ]; then
echo -e "${YELLOW}警告: 检测到已有 Python 后端进程在运行 (PID: $EXISTING)${NC}"
read -p "是否先停止现有进程? (y/n): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo -e "${YELLOW}正在停止现有进程...${NC}"
kill $EXISTING 2>/dev/null
sleep 2
# 如果还在运行,强制终止
if ps -p $EXISTING > /dev/null 2>&1; then
kill -9 $EXISTING 2>/dev/null
fi
echo -e "${GREEN}已停止现有进程${NC}"
else
echo -e "${RED}请先停止现有进程或使用 stop.sh${NC}"
exit 1
fi
fi
# 获取脚本所在目录
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
# 设置 PDF 转换服务环境变量(确保与 pdf-convert-service 一致)
export PDF_CONVERT_KB_ROOT="${PDF_CONVERT_KB_ROOT:-$SCRIPT_DIR/knowledge_base}"
# 检查 conda 环境
if ! command -v conda &> /dev/null; then
echo -e "${RED}错误: 未找到 conda 命令${NC}"
exit 1
fi
# 激活 conda 环境并启动
echo -e "${YELLOW}正在启动 Python 后端...${NC}"
echo -e "${YELLOW}使用环境: gangyan${NC}"
echo -e "${YELLOW}日志文件: nohup.out${NC}"
# 使用 nohup 启动
nohup conda run -n gangyan python startup.py -a > nohup.out 2>&1 &
PID=$!
echo -e "${GREEN}后端已启动PID: $PID${NC}"
echo -e "${YELLOW}查看日志: tail -f nohup.out${NC}"
echo -e "${YELLOW}停止服务: ./stop.sh${NC}"
# 等待几秒后显示日志
sleep 2
echo -e "\n${YELLOW}=== 最近日志 ===${NC}"
tail -20 nohup.out

View File

@@ -1,70 +0,0 @@
#!/bin/bash
# 停止 Python 后端服务脚本
# 默认端口(从 server_config.py 中获取)
DEFAULT_PORT=7861
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${YELLOW}正在查找运行中的 Python 后端进程...${NC}"
# 方法1: 通过进程名查找
PIDS=$(ps aux | grep "[p]ython.*startup.py -a" | awk '{print $2}')
# 方法2: 如果方法1没找到通过端口查找
if [ -z "$PIDS" ]; then
echo -e "${YELLOW}未找到 startup.py 进程,尝试通过端口 ${DEFAULT_PORT} 查找...${NC}"
PIDS=$(lsof -ti:${DEFAULT_PORT} 2>/dev/null)
fi
if [ -z "$PIDS" ]; then
echo -e "${RED}未找到运行中的 Python 后端进程${NC}"
exit 0
fi
echo -e "${GREEN}找到以下进程:${NC}"
ps aux | grep "[p]ython.*startup.py -a" | grep -v grep
if [ -n "$PIDS" ]; then
echo -e "${YELLOW}进程 PID: $PIDS${NC}"
fi
# 询问是否确认停止
read -p "是否停止这些进程? (y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo -e "${YELLOW}已取消${NC}"
exit 0
fi
# 停止进程
for PID in $PIDS; do
if [ -n "$PID" ]; then
echo -e "${YELLOW}正在停止进程 $PID...${NC}"
kill $PID 2>/dev/null
if [ $? -eq 0 ]; then
echo -e "${GREEN}进程 $PID 已发送停止信号${NC}"
else
echo -e "${RED}无法停止进程 $PID${NC}"
fi
fi
done
# 等待进程结束
echo -e "${YELLOW}等待进程结束...${NC}"
sleep 3
# 检查是否还有进程在运行
REMAINING=$(ps aux | grep "[p]ython.*startup.py -a" | awk '{print $2}')
if [ -n "$REMAINING" ]; then
echo -e "${YELLOW}仍有进程在运行,强制终止...${NC}"
for PID in $REMAINING; do
kill -9 $PID 2>/dev/null
echo -e "${GREEN}已强制终止进程 $PID${NC}"
done
fi
echo -e "${GREEN}所有进程已停止${NC}"

View File

@@ -1,53 +0,0 @@
#!/bin/bash
# 快速停止 Python 后端服务脚本(无需确认)
# 默认端口
DEFAULT_PORT=7861
# 颜色定义
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
echo -e "${YELLOW}正在查找并停止 Python 后端进程...${NC}"
# 方法1: 通过进程名查找
PIDS=$(ps aux | grep "[p]ython.*startup.py -a" | awk '{print $2}')
# 方法2: 如果方法1没找到通过端口查找
if [ -z "$PIDS" ]; then
PIDS=$(lsof -ti:${DEFAULT_PORT} 2>/dev/null)
fi
if [ -z "$PIDS" ]; then
echo -e "${RED}未找到运行中的 Python 后端进程${NC}"
exit 0
fi
# 显示找到的进程
echo -e "${GREEN}找到以下进程:${NC}"
ps aux | grep "[p]ython.*startup.py -a" | grep -v grep
# 停止进程
for PID in $PIDS; do
if [ -n "$PID" ]; then
echo -e "${YELLOW}正在停止进程 $PID...${NC}"
kill $PID 2>/dev/null
fi
done
# 等待进程结束
sleep 2
# 检查是否还有进程在运行,如果有则强制终止
REMAINING=$(ps aux | grep "[p]ython.*startup.py -a" | awk '{print $2}')
if [ -n "$REMAINING" ]; then
echo -e "${YELLOW}强制终止剩余进程...${NC}"
for PID in $REMAINING; do
kill -9 $PID 2>/dev/null
echo -e "${GREEN}已强制终止进程 $PID${NC}"
done
fi
echo -e "${GREEN}✓ 所有进程已停止${NC}"

View File

@@ -1,13 +1,12 @@
#!/usr/bin/env bash
# 须用 bash 执行;若误用 sh/dash 会自动改用 bash 再跑一遍
[ -n "${BASH_VERSION:-}" ] || exec /usr/bin/env bash "$0" ${1+"$@"}
# 重启 Java 后端 chat_web_yj.jar与 start_all.sh 一致
# 重启 Java 后端 chat_web_backend.jar源码 mvn 编译产物profile=dev
# 日志gangyan/logs/backend.log
# 勿在此加 -Dspring.datasource*:与 application-local.yml 打架易连错库
# 若 jar 未重打、内嵌 yml 仍是远程库,必须依赖 backend/application-local.yml含完整 spring 数据源)。
# backend/chat_web_yj.jar同一 jar 的改名副本)及其 application-local.yml 覆盖配置已于 2026-04-20 归档删除
set -u
source "$(cd "$(dirname "$0")" && pwd)/common-restart.sh"
# 默认 backend.log若曾被 root 创建导致当前用户不可写,则改用同目录 backend-<用户>.log(仍在 gangyan/logs 下)
# 默认 backend.log若曾被 root 创建导致当前用户不可写,则改用同目录 backend-<用户>.log
LOG_FILE="$LOG_DIR/backend.log"
if ! ( umask 022; : >>"$LOG_FILE" ) 2>/dev/null; then
ALT="$LOG_DIR/backend-$(id -un).log"
@@ -25,34 +24,41 @@ fi
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
export PATH="$JAVA_HOME/bin:$PATH"
log_tee "======== 停止 chat_web_yj.jar ========"
pkill -f "chat_web_yj.jar" 2>/dev/null && log_tee "已发送停止信号" || log_tee "未找到运行中的进程"
sleep 2
BACKEND_DIR="$GANGYAN_ROOT/chat_web_backend"
JAR_REL="target/chat_web_backend.jar"
log_tee "======== 启动 chat_web_yj.jar ========"
cd "$GANGYAN_ROOT/backend"
LOCAL_CFG="$GANGYAN_ROOT/backend/application-local.yml"
EXTRA_JAVA_ARGS=()
if [ -f "$LOCAL_CFG" ]; then
log_tee "加载本地配置: $LOCAL_CFG"
EXTRA_JAVA_ARGS=(-Dspring.config.additional-location="file:${LOCAL_CFG}")
if [ ! -f "$BACKEND_DIR/$JAR_REL" ]; then
log_tee "错误: 未找到 $BACKEND_DIR/$JAR_REL。先执行 chat_web_backend/compile.sh 编译。"
exit 1
fi
log_tee "======== 停止 chat_web_backend.jar ========"
pkill -f "target/chat_web_backend.jar" 2>/dev/null && log_tee "已发送停止信号" || log_tee "未找到运行中的进程"
sleep 2
# 端口兜底(非 hawei 启的旧进程 lsof 可能看不到,用 ss 探测)
if ss -tln 2>/dev/null | grep -qE ':8099[[:space:]]'; then
log_tee "8099 仍被占用,再等 3 秒..."
sleep 3
fi
log_tee "======== 启动 chat_web_backend.jar (profile=dev) ========"
cd "$BACKEND_DIR"
nohup java -jar \
-Xms512m -Xmx2048m \
"${EXTRA_JAVA_ARGS[@]}" \
-Dspring.profiles.active=yj \
chat_web_yj.jar >> "$LOG_FILE" 2>&1 &
"$JAR_REL" \
--spring.profiles.active=dev >> "$LOG_FILE" 2>&1 &
STARTED_PID=$!
log_tee "已后台启动PID=$STARTED_PID"
log_tee "Java 标准输出/错误写入: $LOG_FILE"
sleep 3
if kill -0 "$STARTED_PID" 2>/dev/null; then
if command -v ss >/dev/null 2>&1 && ss -tln 2>/dev/null | grep -q ':8099'; then
log_tee "自检: 进程存活且 8099 已监听(后端已就绪或即将就绪)"
if ss -tln 2>/dev/null | grep -qE ':8099[[:space:]]'; then
log_tee "自检: 进程存活且 8099 已监听"
else
log_tee "自检: 进程存活,但 8099 尚未监听(可能仍在启动;10 秒后仍无请 tail 上面日志路径)"
log_tee "自检: 进程存活,但 8099 尚未监听可能仍在启动10 秒后仍无请 tail $LOG_FILE"
fi
else
log_tee "自检失败: 进程已退出。请查看日志末尾:"
log_tee "自检失败: 进程已退出。最近日志:"
tail -n 40 "$LOG_FILE" 2>/dev/null | while IFS= read -r line || [ -n "$line" ]; do log_line "$line"; done
exit 1
fi

View File

@@ -0,0 +1,98 @@
"""
Excalidraw AI 代理服务
接收 Excalidraw 的 text-to-diagram 请求,转发给 deepseek-v3 生成 Mermaid 语法
"""
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
import requests
import json
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
LLM_API = "http://10.102.24.75:3000/v1/chat/completions"
LLM_KEY = "sk-BlQIGRrotbVDWE5mXCPBFjVWIvJ83hldzz67xInNwzVo7pPb"
SYSTEM_PROMPT = """You are an expert at creating Mermaid diagrams.
When the user describes a diagram, workflow, flowchart, or similar,
generate ONLY valid Mermaid syntax. Do not include any explanation,
just the Mermaid code. Do not wrap it in markdown code blocks.
Always start with a diagram type declaration like: graph TD, flowchart LR, sequenceDiagram, classDiagram, etc."""
@app.post("/v1/ai/text-to-diagram/generate")
async def text_to_diagram(request: Request):
body = await request.json()
print(f"[AI] Received request: {json.dumps(body, ensure_ascii=False)[:500]}")
prompt = body.get("prompt", body.get("text", body.get("message", "")))
try:
resp = requests.post(LLM_API, json={
"model": "deepseek-v3",
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt}
],
"temperature": 0.3,
"max_tokens": 2048
}, headers={"Authorization": f"Bearer {LLM_KEY}"}, timeout=60)
result = resp.json()
print(f"[AI] LLM response status: {resp.status_code}")
mermaid_code = result["choices"][0]["message"]["content"].strip()
# 清理可能的 markdown 包裹
if mermaid_code.startswith("```"):
mermaid_code = mermaid_code.split("\n", 1)[1] if "\n" in mermaid_code else mermaid_code[3:]
if mermaid_code.endswith("```"):
mermaid_code = mermaid_code[:-3].strip()
print(f"[AI] Mermaid output: {mermaid_code[:200]}")
return {"generatedResponse": mermaid_code}
except Exception as e:
print(f"[AI] Error: {e}")
return {"generatedResponse": f"graph TD\n A[Error: {str(e)[:50]}]"}
@app.post("/v1/ai/diagram-to-code/generate")
async def diagram_to_code(request: Request):
body = await request.json()
texts = body.get("texts", [])
theme = body.get("theme", "light")
print(f"[AI] diagram-to-code request, texts: {texts[:5]}, theme: {theme}")
prompt = f"Based on these UI element labels: {', '.join(texts) if texts else 'empty wireframe'}. Theme: {theme}. Generate a complete, beautiful HTML page with inline CSS that represents this wireframe layout. Only output the HTML code, nothing else."
try:
resp = requests.post(LLM_API, json={
"model": "deepseek-v3",
"messages": [
{"role": "system", "content": "You are an expert web developer. Generate complete HTML with inline CSS based on wireframe descriptions. Output ONLY the HTML code, no explanation, no markdown code blocks."},
{"role": "user", "content": prompt}
],
"temperature": 0.3,
"max_tokens": 4096
}, headers={"Authorization": f"Bearer {LLM_KEY}"}, timeout=60)
result = resp.json()
html_code = result["choices"][0]["message"]["content"].strip()
if html_code.startswith("```"):
html_code = html_code.split("\n", 1)[1] if "\n" in html_code else html_code[3:]
if html_code.endswith("```"):
html_code = html_code[:-3].strip()
print(f"[AI] HTML output length: {len(html_code)}")
return html_code
except Exception as e:
print(f"[AI] diagram-to-code error: {e}")
return f"<html><body><h1>Generation Error</h1><p>{str(e)[:100]}</p></body></html>"
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=18082)

View File

@@ -0,0 +1,153 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LaTeX 公式编辑器</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #f0f2f5; color: #333; }
.header { background: #004EA0; color: #fff; padding: 16px 32px; font-size: 18px; font-weight: bold; }
.container { max-width: 1200px; margin: 24px auto; padding: 0 24px; display: flex; gap: 20px; }
.panel { background: #fff; border-radius: 10px; padding: 20px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); flex: 1; }
.panel h3 { font-size: 15px; color: #333; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; }
textarea { width: 100%; height: 200px; border: 1px solid #ddd; border-radius: 8px; padding: 14px; font-family: 'Courier New', monospace; font-size: 15px; resize: vertical; outline: none; }
textarea:focus { border-color: #004EA0; }
.preview { min-height: 200px; border: 1px solid #eee; border-radius: 8px; padding: 20px; display: flex; align-items: center; justify-content: center; background: #fafbfc; overflow: auto; }
.preview .katex { font-size: 24px; }
.error { color: #dc2626; font-size: 13px; text-align: center; }
.toolbar { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
.toolbar button { padding: 4px 10px; border: 1px solid #ddd; border-radius: 6px; background: #f5f7fa; cursor: pointer; font-size: 13px; transition: all 0.15s; }
.toolbar button:hover { border-color: #004EA0; color: #004EA0; background: #e8f0fe; }
.symbols { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 16px; }
.symbols button { width: 42px; height: 36px; border: 1px solid #eee; border-radius: 6px; background: #fafbfc; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
.symbols button:hover { background: #e8f0fe; border-color: #004EA0; }
.actions { margin-top: 14px; display: flex; gap: 10px; }
.actions button { padding: 8px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; }
.btn-primary { background: #004EA0; color: #fff; }
.btn-primary:hover { background: #003a7a; }
.btn-secondary { background: #f0f2f5; color: #333; border: 1px solid #ddd !important; }
.btn-secondary:hover { background: #e5e7eb; }
.templates { margin-top: 20px; }
.templates h4 { font-size: 14px; color: #666; margin-bottom: 8px; }
.tpl-list { display: flex; flex-wrap: wrap; gap: 6px; }
.tpl-list button { padding: 4px 12px; border: 1px solid #e0e0e0; border-radius: 6px; background: #fff; cursor: pointer; font-size: 12px; color: #555; }
.tpl-list button:hover { border-color: #004EA0; color: #004EA0; }
.copied { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); background: rgba(0,0,0,0.7); color: #fff; padding: 10px 24px; border-radius: 8px; font-size: 14px; display: none; z-index: 999; }
</style>
</head>
<body>
<div class="header">LaTeX 公式编辑器</div>
<div class="container">
<div class="panel" style="flex:1.2">
<h3>输入公式</h3>
<div class="toolbar">
<button onclick="ins('\\frac{a}{b}')">分数</button>
<button onclick="ins('\\sqrt{x}')">根号</button>
<button onclick="ins('\\sum_{i=1}^{n}')">求和</button>
<button onclick="ins('\\int_{a}^{b}')">积分</button>
<button onclick="ins('\\lim_{x \\to \\infty}')">极限</button>
<button onclick="ins('\\prod_{i=1}^{n}')">连乘</button>
<button onclick="ins('x^{2}')">上标</button>
<button onclick="ins('x_{i}')">下标</button>
<button onclick="ins('\\binom{n}{k}')">组合</button>
<button onclick="ins('\\vec{a}')">向量</button>
<button onclick="ins('\\hat{x}')"></button>
<button onclick="ins('\\bar{x}')">横线</button>
<button onclick="ins('\\dot{x}')"></button>
<button onclick="ins('\\overline{AB}')">上划线</button>
<button onclick="ins('\\begin{matrix} a & b \\\\ c & d \\end{matrix}')">矩阵</button>
<button onclick="ins('\\begin{cases} x & y \\\\ a & b \\end{cases}')">分段</button>
<button onclick="ins('\\log')">log</button>
<button onclick="ins('\\ln')">ln</button>
</div>
<textarea id="input" placeholder="在此输入 LaTeX 公式E = mc^{2}" oninput="render()">E = mc^{2}</textarea>
<div class="symbols" id="symbolBar"></div>
<div class="actions">
<button class="btn-primary" onclick="copyLatex()">复制公式</button>
<button class="btn-secondary" onclick="downloadImg()">下载为图片</button>
<button class="btn-secondary" onclick="document.getElementById('input').value='';render()">清空</button>
</div>
<div class="templates">
<h4>常用公式模板</h4>
<div class="tpl-list">
<button onclick="setTpl('E = mc^{2}')">质能方程</button>
<button onclick="setTpl('e^{i\\pi} + 1 = 0')">欧拉公式</button>
<button onclick="setTpl('a^2 + b^2 = c^2')">勾股定理</button>
<button onclick="setTpl('x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}')">求根公式</button>
<button onclick="setTpl('f\'(x) = \\lim_{h \\to 0} \\frac{f(x+h) - f(x)}{h}')">导数定义</button>
<button onclick="setTpl('\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}')">高斯积分</button>
<button onclick="setTpl('\\nabla \\times \\vec{E} = -\\frac{\\partial \\vec{B}}{\\partial t}')">麦克斯韦</button>
<button onclick="setTpl('\\sigma = \\sqrt{\\frac{1}{N}\\sum_{i=1}^{N}(x_i - \\mu)^2}')">标准差</button>
<button onclick="setTpl('P(A|B) = \\frac{P(B|A) \\cdot P(A)}{P(B)}')">贝叶斯</button>
<button onclick="setTpl('\\mathbf{A} = \\begin{pmatrix} a_{11} & a_{12} \\\\ a_{21} & a_{22} \\end{pmatrix}')">矩阵</button>
</div>
</div>
</div>
<div class="panel">
<h3>实时预览 <span style="font-weight:normal;font-size:12px;color:#999">公式渲染由 KaTeX 提供</span></h3>
<div class="preview" id="preview"></div>
</div>
</div>
<div class="copied" id="toast">已复制!</div>
<script>
const symbols = ['\\alpha','\\beta','\\gamma','\\delta','\\epsilon','\\zeta','\\eta','\\theta','\\lambda','\\mu','\\nu','\\xi','\\pi','\\rho','\\sigma','\\tau','\\phi','\\psi','\\omega','\\Gamma','\\Delta','\\Theta','\\Lambda','\\Pi','\\Sigma','\\Phi','\\Psi','\\Omega','\\infty','\\partial','\\nabla','\\forall','\\exists','\\in','\\notin','\\subset','\\cup','\\cap','\\times','\\cdot','\\div','\\pm','\\mp','\\leq','\\geq','\\neq','\\approx','\\equiv','\\sim','\\rightarrow','\\leftarrow','\\Rightarrow','\\Leftarrow'];
const bar = document.getElementById('symbolBar');
symbols.forEach(s => {
const b = document.createElement('button');
try { katex.render(s, b, {throwOnError:false}); } catch(e) { b.textContent = s; }
b.onclick = () => ins(s);
bar.appendChild(b);
});
function render() {
const input = document.getElementById('input').value.trim();
const el = document.getElementById('preview');
if (!input) { el.innerHTML = '<span style="color:#bbb">在左侧输入公式</span>'; return; }
try { katex.render(input, el, { displayMode: true, throwOnError: true }); }
catch(e) { el.innerHTML = '<span class="error">' + e.message + '</span>'; }
}
function ins(s) {
const t = document.getElementById('input');
const start = t.selectionStart, end = t.selectionEnd;
t.value = t.value.slice(0, start) + s + t.value.slice(end);
t.focus();
t.selectionStart = t.selectionEnd = start + s.length;
render();
}
function setTpl(s) { document.getElementById('input').value = s; render(); }
function copyLatex() {
navigator.clipboard.writeText(document.getElementById('input').value);
showToast('公式已复制!');
}
function downloadImg() {
const el = document.getElementById('preview');
if (!el.querySelector('.katex')) { showToast('请先输入公式'); return; }
// 直接用 KaTeX 的 mathml/svg 渲染结果导出为 SVG 文件下载
const w = el.scrollWidth + 40, h = el.scrollHeight + 40;
const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
<rect width="100%" height="100%" fill="white"/>
<foreignObject width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:center;justify-content:center;width:${w}px;height:${h}px;font-size:24px">
${el.innerHTML}
</div>
</foreignObject>
</svg>`;
const blob = new Blob([svgStr], {type: 'image/svg+xml'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'formula.svg';
a.click();
URL.revokeObjectURL(a.href);
showToast('图片已下载!');
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg; t.style.display = 'block';
setTimeout(() => t.style.display = 'none', 1500);
}
render();
</script>
</body>
</html>

View File

@@ -31,7 +31,8 @@ fi
MAX_BYTES=$((MAX_MB * 1024 * 1024))
TMP="${FILE}.trimtmp.$$"
LOCK="${FILE}.trimlock"
# LOCK 放 /tmp 并按用户命名,避免不同用户启动时 .trimlock owner 错配导致 Permission denied
LOCK="/tmp/gangyan-trim-$(basename "$FILE").$(id -un).lock"
mkdir -p "$(dirname "$FILE")"
touch "$FILE" 2>/dev/null || true

View File

@@ -0,0 +1,49 @@
version: '3'
services:
sharelatex:
image: docker.1ms.run/sharelatex/sharelatex:latest
container_name: overleaf
restart: always
depends_on:
- mongo
- redis
ports:
- 18090:80
volumes:
- overleaf-data:/var/lib/overleaf
environment:
OVERLEAF_APP_NAME: "LaTeX 论文编辑器"
OVERLEAF_MONGO_URL: "mongodb://mongo/overleaf?replicaSet=overleaf"
OVERLEAF_REDIS_HOST: "redis"
REDIS_HOST: "redis"
ENABLED_LINKED_FILE_TYPES: "project_file,project_output_file"
ENABLE_CONVERSIONS: "true"
EMAIL_CONFIRMATION_DISABLED: "true"
OVERLEAF_SITE_URL: "http://localhost:18090"
OVERLEAF_LEFT_OPEN_REGISTRATION: "true"
mongo:
image: docker.1ms.run/mongo:8.0
container_name: overleaf-mongo
restart: always
command: --replSet overleaf
volumes:
- overleaf-mongo:/data/db
healthcheck:
test: >
mongosh --eval "try { rs.status().ok } catch(e) { rs.initiate({_id:'overleaf',members:[{_id:0,host:'mongo:27017'}]}).ok }" --quiet
interval: 10s
timeout: 10s
retries: 5
redis:
image: docker.1ms.run/redis:7-alpine
container_name: overleaf-redis
restart: always
volumes:
- overleaf-redis:/data
volumes:
overleaf-data:
overleaf-mongo:
overleaf-redis:

View File

@@ -0,0 +1,128 @@
"""
Overleaf 自助注册服务
提供简单的注册页面,用户输入邮箱和密码即可注册
内部通过管理员账号调用 Overleaf API 创建用户
"""
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse
import requests
import os
app = FastAPI()
OL_URL = os.getenv("OL_INSTANCE", "http://overleaf")
OL_ADMIN_EMAIL = os.getenv("OL_ADMIN_EMAIL", "")
OL_ADMIN_PASSWORD = os.getenv("OL_ADMIN_PASSWORD", "")
REGISTER_HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>注册 - LaTeX 论文编辑器</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, sans-serif; background: #f5f5f5; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.card { background: #fff; border-radius: 12px; padding: 40px; width: 400px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); }
h2 { text-align: center; margin-bottom: 8px; color: #333; }
.sub { text-align: center; color: #999; font-size: 14px; margin-bottom: 30px; }
label { display: block; margin-bottom: 6px; color: #555; font-size: 14px; }
input { width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; margin-bottom: 16px; }
input:focus { outline: none; border-color: #004EA0; }
button { width: 100%; padding: 12px; background: #004EA0; color: #fff; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; }
button:hover { background: #003a7a; }
.msg { text-align: center; margin-top: 16px; font-size: 14px; }
.msg.ok { color: #16a34a; }
.msg.err { color: #dc2626; }
a { color: #004EA0; text-decoration: none; }
</style>
</head>
<body>
<div class="card">
<h2>注册账号</h2>
<p class="sub">LaTeX 论文编辑器</p>
<form method="POST" action="/register">
<label>邮箱</label>
<input type="email" name="email" required placeholder="请输入邮箱">
<button type="submit">注册</button>
</form>
<p class="sub" style="margin-top:20px">已有账号?<a href="OLURL">去登录</a></p>
MSG_PLACEHOLDER
</div>
</body>
</html>
""".replace("OLURL", OL_URL)
class OverleafAdmin:
def __init__(self):
self.session = requests.Session()
self.logged_in = False
def _get_csrf(self, path="/login"):
resp = self.session.get(f"{OL_URL}{path}")
# 从页面中提取 CSRF token
import re
match = re.search(r'name="_csrf"[^>]*value="([^"]+)"', resp.text)
if not match:
match = re.search(r'ol-csrfToken["\s]*content="([^"]+)"', resp.text)
if not match:
match = re.search(r'csrfToken["\s]*:\s*"([^"]+)"', resp.text)
return match.group(1) if match else ""
def login(self):
csrf = self._get_csrf("/login")
resp = self.session.post(f"{OL_URL}/login", data={
"_csrf": csrf,
"email": OL_ADMIN_EMAIL,
"password": OL_ADMIN_PASSWORD,
}, allow_redirects=False)
self.logged_in = resp.status_code in (200, 302)
return self.logged_in
def register_user(self, email):
if not self.logged_in:
self.login()
csrf = self._get_csrf("/admin/register")
resp = self.session.post(f"{OL_URL}/admin/register", data={
"_csrf": csrf,
"email": email,
})
# 从响应中提取设置密码的链接
import re
set_password_url = ""
match = re.search(r'(https?://[^\s"<>]*user/password/set\?[^\s"<>]+)', resp.text)
if match:
set_password_url = match.group(1)
return resp.status_code == 200, set_password_url
admin = OverleafAdmin()
@app.get("/", response_class=HTMLResponse)
async def index():
return REGISTER_HTML.replace("MSG_PLACEHOLDER", "")
@app.post("/register", response_class=HTMLResponse)
async def register(email: str = Form(...)):
try:
ok, set_url = admin.register_user(email)
if ok and set_url:
# 把链接中的 localhost 替换为实际访问地址
import re
set_url = re.sub(r'https?://[^/]+', OL_URL, set_url)
msg = f'<p class="msg ok">注册成功!请点击下方链接设置密码:</p><p style="text-align:center;margin-top:10px"><a href="{set_url}" target="_blank" style="background:#004EA0;color:#fff;padding:10px 24px;border-radius:8px;text-decoration:none;font-size:14px">设置密码</a></p>'
elif ok:
msg = f'<p class="msg ok">注册成功!该邮箱可能已注册。请前往 <a href="{OL_URL}/login">登录页面</a> 登录。</p>'
else:
msg = f'<p class="msg err">注册失败,请稍后重试。</p>'
except Exception as e:
msg = f'<p class="msg err">注册失败:{str(e)[:100]}</p>'
return REGISTER_HTML.replace("MSG_PLACEHOLDER", msg)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=18091)

View File

@@ -0,0 +1,239 @@
"""
PPTist AI 后端服务
接收 PPTist 的 AIPPT 请求,调用 deepseek-v3 生成大纲和PPT内容
端口: 18086
"""
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
import httpx
import json
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
LLM_API = "http://10.102.24.75:3000/v1/chat/completions"
LLM_KEY = "sk-BlQIGRrotbVDWE5mXCPBFjVWIvJ83hldzz67xInNwzVo7pPb"
LLM_MODEL = "deepseek-v3"
OUTLINE_SYSTEM_PROMPT = """你是一个专业的PPT大纲生成助手。用户会给你一个主题你需要生成一份内容丰富、结构清晰的PPT大纲。
要求:
1. 使用Markdown格式用 # 表示演示文稿标题,## 表示章节标题,### 表示该章节下的内容页标题
2. 必须包含6个主要章节##
3. 每个章节下必须有2-3个内容页###
4. 每个内容页标题要具体、有信息量,不要太笼统
5. 内容要专业、逻辑清晰、层层递进
6. 直接输出Markdown内容不要有多余的解释
示例格式:
# 人工智能技术发展
## 人工智能概述
### 定义与发展历史
### 核心技术与分支
## 机器学习基础
### 监督学习与无监督学习
### 深度学习与神经网络
### 常用算法对比
..."""
AIPPT_SYSTEM_PROMPT = """你是一个专业的PPT内容生成助手。用户会给你一份PPT大纲你需要根据大纲生成完整、丰富、详实的PPT内容。
你必须严格按照以下JSON数组格式输出不要输出任何其他内容。
结构规则:
1. 第一页cover类型包含演示标题和副标题描述
2. 第二页contents类型列出所有章节名称
3. 每个章节先一个transition过渡页然后每个###子内容对应一个content页
4. 每个content页必须包含4个items每个item有title和text
5. text内容必须详实具体每条20-40字包含实质性的数据、观点或分析不要空泛
6. 最后一页end类型
输出格式JSON数组每个元素是一页slide
[
{"type": "cover", "data": {"title": "演示标题", "text": "一句话描述演示主题和价值"}},
{"type": "contents", "data": {"items": ["章节1名称", "章节2名称", "章节3名称", "章节4名称", "章节5名称", "章节6名称"]}},
{"type": "transition", "data": {"title": "章节标题", "text": "本章将介绍什么内容,一句话概述"}},
{"type": "content", "data": {"title": "内容页标题", "items": [
{"title": "要点1标题", "text": "要点1的详细说明包含具体信息、数据或分析"},
{"title": "要点2标题", "text": "要点2的详细说明"},
{"title": "要点3标题", "text": "要点3的详细说明"},
{"title": "要点4标题", "text": "要点4的详细说明"}
]}},
{"type": "end"}
]
重要:
- 生成的总页数应在20-25页之间
- 只输出JSON数组不要markdown代码块不要任何解释
- 确保JSON格式完全正确可被直接解析"""
async def stream_llm(messages, temperature=0.7, max_tokens=4096):
"""流式调用LLM并逐块返回文本"""
async with httpx.AsyncClient(timeout=120) as client:
async with client.stream(
"POST",
LLM_API,
json={
"model": LLM_MODEL,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"stream": True,
},
headers={"Authorization": f"Bearer {LLM_KEY}"},
) as resp:
async for line in resp.aiter_lines():
if line.startswith("data: "):
data = line[6:]
if data.strip() == "[DONE]":
break
try:
chunk = json.loads(data)
delta = chunk["choices"][0].get("delta", {})
content = delta.get("content", "")
if content:
yield content
except (json.JSONDecodeError, KeyError, IndexError):
continue
@app.post("/tools/aippt_outline")
async def aippt_outline(request: Request):
data = await request.json()
content = data.get("content", "")
language = data.get("language", "中文")
print(f"[AIPPT] Outline request: {content}, language: {language}")
user_prompt = f"请用{language}为以下主题生成PPT大纲\n\n{content}"
async def generate():
async for text in stream_llm(
[
{"role": "system", "content": OUTLINE_SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
temperature=0.7,
max_tokens=2048,
):
yield text
return StreamingResponse(generate(), media_type="text/event-stream")
@app.post("/tools/aippt")
async def aippt(request: Request):
data = await request.json()
content = data.get("content", "")
language = data.get("language", "中文")
style = data.get("style", "通用")
print(f"[AIPPT] Generate request, style: {style}, language: {language}")
user_prompt = f"请根据以下大纲生成PPT内容风格为「{style}」,语言为{language}\n\n{content}"
async def generate():
"""流式收集LLM输出按JSON对象级别缓冲每检测到一个完整对象就立即发送"""
buf = ""
brace_depth = 0
in_string = False
escape_next = False
found_array = False # 是否已经跳过了数组开头的 [
try:
async for chunk in stream_llm(
[
{"role": "system", "content": AIPPT_SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
temperature=0.7,
max_tokens=8192,
):
for ch in chunk:
# 跳过数组开头的 [ 和结尾的 ] 以及逗号分隔符
if not found_array:
if ch == '[':
found_array = True
continue
if brace_depth == 0 and ch in (' ', '\n', '\r', '\t', ',', ']'):
continue
buf += ch
if escape_next:
escape_next = False
continue
if ch == '\\' and in_string:
escape_next = True
continue
if ch == '"' and not escape_next:
in_string = not in_string
continue
if in_string:
continue
if ch == '{':
brace_depth += 1
elif ch == '}':
brace_depth -= 1
if brace_depth == 0:
# 得到一个完整的JSON对象
try:
obj = json.loads(buf)
yield json.dumps(obj, ensure_ascii=False) + "\n"
print(f"[AIPPT] Sent slide: {obj.get('type', '?')}")
except json.JSONDecodeError as e:
print(f"[AIPPT] JSON parse error: {e}, buf: {buf[:100]}")
buf = ""
except Exception as e:
print(f"[AIPPT] Error: {e}")
yield json.dumps({"type": "cover", "data": {"title": "生成失败", "text": str(e)[:100]}}) + "\n"
yield json.dumps({"type": "end"}) + "\n"
return StreamingResponse(generate(), media_type="text/event-stream")
WRITING_COMMANDS = {
"rewrite": "请改写以下文本,保持原意但使用不同的表达方式:",
"expand": "请扩写以下文本,补充更多细节和内容:",
"abbreviate": "请缩写以下文本,保留核心要点,使其更简洁:",
"polish": "请润色以下文本,使其更流畅、专业:",
"translate": "请将以下文本翻译为英文:",
}
@app.post("/tools/ai_writing")
async def ai_writing(request: Request):
data = await request.json()
content = data.get("content", "")
command = data.get("command", "rewrite")
print(f"[AIPPT] Writing request, command: {command}")
instruction = WRITING_COMMANDS.get(command, WRITING_COMMANDS["rewrite"])
user_prompt = f"{instruction}\n\n{content}"
async def generate():
async for text in stream_llm(
[
{"role": "system", "content": "你是一个专业的文字助手,帮助用户改写、扩写、缩写、润色或翻译文本。直接输出结果,不要有额外解释。"},
{"role": "user", "content": user_prompt},
],
temperature=0.7,
max_tokens=2048,
):
yield text
return StreamingResponse(generate(), media_type="text/event-stream")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=18086)

View File

@@ -0,0 +1,41 @@
# PPTist 构建 & 部署
FROM docker.1ms.run/node:20 AS builder
ENV http_proxy=http://219.234.197.247:53128
ENV https_proxy=http://219.234.197.247:53128
ENV no_proxy=localhost,127.0.0.1,10.0.0.0/8,192.168.0.0/16,172.17.0.0/16
WORKDIR /app
COPY PPTist/package*.json ./
RUN npm config set registry https://registry.npmmirror.com && npm install
COPY PPTist/ .
# 修改 SERVER_URL: 生产环境使用相对路径 /pptapi由 nginx 反代到 AI 后端
RUN sed -i "s|'https://server.pptist.cn'|'/pptapi'|g" src/services/index.ts
# 修改模型选项为 deepseek-v3
RUN sed -i "s/glm-4\.7-flash/deepseek-v3/g" src/views/Editor/AIPPTDialog.vue && \
sed -i "s/GLM-4\.7-Flash/DeepSeek-V3/g" src/views/Editor/AIPPTDialog.vue && \
sed -i "s/doubao-seed-1\.6-flash/deepseek-v3/g" src/views/Editor/AIPPTDialog.vue && \
sed -i "s/Doubao-Seed-1\.6-Flash/DeepSeek-V3/g" src/views/Editor/AIPPTDialog.vue && \
sed -i "s/GLM-4\.5-Flash/DeepSeek-V3/g" src/services/index.ts
RUN npm run build
# 生产阶段
FROM docker.1ms.run/nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
RUN echo 'server { \
listen 80; \
root /usr/share/nginx/html; \
index index.html; \
location / { \
try_files $uri $uri/ /index.html; \
} \
location /pptapi/ { \
proxy_pass http://172.17.0.1:18086/; \
proxy_http_version 1.1; \
proxy_set_header Connection ""; \
proxy_buffering off; \
proxy_cache off; \
} \
}' > /etc/nginx/conf.d/default.conf
EXPOSE 80

View File

@@ -0,0 +1,41 @@
#!/bin/bash
# 一键部署三个工具: TrWebOCR, LibreTranslate, PPTist + AI后端
set -e
echo "========== 1. 部署 TrWebOCR (端口 18083) =========="
docker rm -f trwebocr 2>/dev/null || true
docker run -d --name trwebocr --restart always -p 18083:8089 mmmz/trwebocr:latest
echo "TrWebOCR 启动完成"
echo "========== 2. 部署 LibreTranslate (端口 18084) =========="
docker rm -f libretranslate 2>/dev/null || true
docker run -d --name libretranslate --restart always \
-p 18084:5000 \
-e LT_LOAD_ONLY=en,zh \
-v lt-data:/home/libretranslate/.local \
libretranslate/libretranslate
echo "LibreTranslate 启动完成语言模型下载中约5分钟后可用"
echo "========== 3. 启动 PPTist AI 后端 (端口 18086) =========="
# 先安装依赖
pip install fastapi uvicorn httpx 2>/dev/null || pip3 install fastapi uvicorn httpx
# 杀掉旧进程
pkill -f "pptist-ai-backend" 2>/dev/null || true
sleep 1
# 用 screen 启动后台运行
screen -dmS pptist-ai python3 /root/gangyan/scripts/pptist-ai-backend.py
echo "PPTist AI 后端启动完成"
echo "========== 4. 构建并部署 PPTist (端口 18085) =========="
cd /root/gangyan/scripts/pptist-deploy
docker rm -f pptist 2>/dev/null || true
docker build -t pptist:latest .
docker run -d --name pptist --restart always -p 18085:80 pptist:latest
echo "PPTist 启动完成"
echo ""
echo "========== 部署完成 =========="
echo "TrWebOCR: http://localhost:18083"
echo "LibreTranslate: http://localhost:18084"
echo "PPTist: http://localhost:18085"
echo "PPTist AI后端: http://localhost:18086"

83
scripts/start-all.sh Executable file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# 须用 bash 执行;若误用 sh/dash 会自动改用 bash 再跑一遍
[ -n "${BASH_VERSION:-}" ] || exec /usr/bin/env bash "$0" ${1+"$@"}
# 一键启动全部 gangyan 服务:基础设施 -> 后端 -> 前端。
# 每一步直接 source 已有的 *-restart.sh保持单一事实来源。
# 日志gangyan/logs/start-all.log
set -u
source "$(cd "$(dirname "$0")" && pwd)/common-restart.sh"
LOG_FILE="$LOG_DIR/start-all.log"
run_step() {
local label="$1" script="$2"
log_tee "━━━━━━━━ ${label} ━━━━━━━━"
if [ ! -x "$SCRIPT_DIR/$script" ]; then
log_tee "跳过: $script 不存在或不可执行"
return 0
fi
bash "$SCRIPT_DIR/$script" 2>&1 | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
[ "$rc" -eq 0 ] || log_tee "警告: $script 返回 $rc"
return 0
}
port_check() {
local port="$1" name="$2"
if ss -tln 2>/dev/null | grep -qE ":${port}[[:space:]]"; then
log_tee " [OK] ${name} :${port}"
else
log_tee " [!!] ${name} :${port} 未监听"
fi
}
log_tee "╔══════════════════════════════════════╗"
log_tee "║ gangyan 全量启动 $(date '+%F %H:%M:%S')"
log_tee "╚══════════════════════════════════════╝"
# 1) 基础设施Docker 容器MySQL / Redis / Milvus
run_step "[1/5] MySQL" mysql-restart.sh
run_step "[2/5] Redis" redis-restart.sh
run_step "[3/5] Milvus" milvus-restart.sh
# 2) 应用层langchain-chat 会顺带起 pdf-convert-service + log-trim daemon
run_step "[4/6] langchain-chat + pdf-convert + log-trim" langchain-restart.sh
run_step "[5/6] Java 后端" backend-restart.sh
run_step "[6/6] 前端 vite" frontend-restart.sh
# 3) 辅助进程状态excalidraw-ai / pptist-ai 当前以 screen -dmS 管理,非脚本化;本脚本仅做健康检查)
log_tee "━━━━━━━━ 辅助进程检查 ━━━━━━━━"
if pgrep -f 'excalidraw-ai-proxy\.py' >/dev/null; then
log_tee " [OK] excalidraw-ai-proxy.py (screen -dmS aiproxy)"
else
log_tee " [!!] excalidraw-ai-proxy.py 未运行。手动启: screen -dmS aiproxy bash -c 'cd $GANGYAN_ROOT/scripts && /opt/software/miniconda3/envs/langchain-chat/bin/python excalidraw-ai-proxy.py'"
fi
if pgrep -f 'pptist-ai-backend\.py' >/dev/null; then
log_tee " [OK] pptist-ai-backend.py (screen -dmS pptist-ai)"
else
log_tee " [!!] pptist-ai-backend.py 未运行。手动启: screen -dmS pptist-ai bash -c 'cd $GANGYAN_ROOT/scripts && /opt/software/miniconda3/envs/langchain-chat/bin/python pptist-ai-backend.py'"
fi
# 4) 系统级 tools-nginxsystemd 管理,不在本脚本中重启)
if systemctl is-active --quiet nginx 2>/dev/null; then
log_tee " [OK] nginx (systemd, tools-nginx.conf -> :18000)"
else
log_tee " [!!] nginx 未运行。sudo systemctl start nginx"
fi
# 5) 端口自检
log_tee "━━━━━━━━ 端口自检(等 5 秒) ━━━━━━━━"
sleep 5
port_check 3306 "MySQL (3306 映射)"
port_check 33306 "MySQL (33306 映射)"
port_check 6379 "Redis"
port_check 19530 "Milvus gRPC"
port_check 9091 "Milvus HTTP"
port_check 7861 "langchain-chat API"
port_check 6006 "pdf-convert-service"
port_check 8099 "Java 后端"
port_check 3000 "vite 前端"
port_check 18000 "tools-nginx (工具集合)"
log_tee ""
log_tee "完成。完整日志: $LOG_FILE"
log_tee "访问: http://<server-ip>:3000/metalinfo"

147
scripts/tools-nginx.conf Normal file
View File

@@ -0,0 +1,147 @@
server {
listen 18000;
client_max_body_size 500M;
# ===== 1. Stirling PDF =====
# <base href="/"> 导致相对路径从根开始用sub_filter改成/pdf/
# context-path=/pdf所有PDF资源都在 /pdf/ 下,无冲突
location /pdf/ {
proxy_pass http://127.0.0.1:18080/pdf/;
proxy_set_header Host $host;
proxy_buffering off;
client_max_body_size 500M;
}
# ===== 2. Excalidraw =====
location /draw/ {
proxy_pass http://127.0.0.1:18081/;
proxy_set_header Host $host;
}
location /assets/ { proxy_pass http://127.0.0.1:18081/assets/; }
location /v1/ai/ {
proxy_pass http://127.0.0.1:18082/v1/ai/;
proxy_set_header Host $host;
proxy_read_timeout 60s;
proxy_buffering off;
}
# ===== 3. TrWebOCR =====
# 不用 sub_filter后端返回 gzip 无法改写),直接用精确文件名 location
location /ocr/ {
proxy_pass http://127.0.0.1:18083/;
proxy_set_header Host $host;
proxy_buffering off;
}
# OCR 的 JS/CSS精确文件名不会和 Overleaf 冲突)
location /js/app.7dd3e457.js { proxy_pass http://127.0.0.1:18083/js/app.7dd3e457.js; }
location /js/chunk-vendors.ae13d15d.js { proxy_pass http://127.0.0.1:18083/js/chunk-vendors.ae13d15d.js; }
location /css/app.77d50329.css { proxy_pass http://127.0.0.1:18083/css/app.77d50329.css; }
location /css/chunk-vendors.9d96bc97.css { proxy_pass http://127.0.0.1:18083/css/chunk-vendors.9d96bc97.css; }
# OCR 的 API
location /api/tr-run/ {
proxy_pass http://127.0.0.1:18083/api/tr-run/;
proxy_set_header Host $host;
proxy_buffering off;
client_max_body_size 50M;
}
location /tools/ocr_text/ {
proxy_pass http://127.0.0.1:18083/tools/ocr_text/;
proxy_set_header Host $host;
}
# ===== 4. LibreTranslate =====
location /translate/ {
proxy_pass http://127.0.0.1:18084/;
proxy_set_header Host $host;
}
location /static/ { proxy_pass http://127.0.0.1:18084/static/; }
location /languages { proxy_pass http://127.0.0.1:18084/languages; }
location /frontend/settings { proxy_pass http://127.0.0.1:18084/frontend/settings; }
location /detect { proxy_pass http://127.0.0.1:18084/detect; }
# ===== 5. PPTist =====
location /ppt/ {
proxy_pass http://127.0.0.1:18085/;
proxy_set_header Host $host;
proxy_buffering off;
}
location /pptapi/ {
proxy_pass http://127.0.0.1:18086/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
}
# ===== 6. imgcompress =====
location /imgcompress/ {
proxy_pass http://127.0.0.1:18087/;
proxy_set_header Host $host;
}
location /_next/ { proxy_pass http://127.0.0.1:18087/_next/; }
location /api/ {
proxy_pass http://127.0.0.1:18087/api/;
proxy_set_header Host $host;
proxy_buffering off;
client_max_body_size 500M;
}
# ===== 7. Lama Cleaner =====
# sub_filter 不生效(后端 gzip用精确 location 匹配所有 API 和静态资源
location /lama/ {
proxy_pass http://127.0.0.1:18088/;
proxy_set_header Host $host;
proxy_buffering off;
}
# Lama 静态资源(精确文件名)
location = /static/js/main.1bd455bc.js { proxy_pass http://127.0.0.1:18088/static/js/main.1bd455bc.js; }
location = /static/css/main.c28d98ca.css { proxy_pass http://127.0.0.1:18088/static/css/main.c28d98ca.css; }
# Lama API全部用精确路径
location = /inpaint {
proxy_pass http://127.0.0.1:18088/inpaint;
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 300s;
client_max_body_size 500M;
}
location = /inputimage { proxy_pass http://127.0.0.1:18088/inputimage; proxy_set_header Host $host; proxy_buffering off; }
location = /model { proxy_pass http://127.0.0.1:18088/model; proxy_set_header Host $host; }
location = /is_desktop { proxy_pass http://127.0.0.1:18088/is_desktop; }
location = /is_disable_model_switch { proxy_pass http://127.0.0.1:18088/is_disable_model_switch; }
location = /is_enable_file_manager { proxy_pass http://127.0.0.1:18088/is_enable_file_manager; }
location = /interactive_seg { proxy_pass http://127.0.0.1:18088/interactive_seg; proxy_set_header Host $host; }
location = /save_image { proxy_pass http://127.0.0.1:18088/save_image; proxy_set_header Host $host; client_max_body_size 500M; }
# ===== 8. webp2jpg =====
location /webp2jpg/ {
proxy_pass http://127.0.0.1:18089/;
proxy_set_header Host $host;
}
location /cdn/ { proxy_pass http://127.0.0.1:18089/cdn/; }
# ===== 9. Overleaf =====
location /overleaf/ {
proxy_pass http://127.0.0.1:18090/;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
}
# ===== 10. LaTeX 公式编辑器 =====
location /latex/ {
proxy_pass http://127.0.0.1:18091/;
proxy_set_header Host $host;
}
# ===== 默认回落到 Overleaf =====
location / {
proxy_pass http://127.0.0.1:18090;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
}
}

View File

@@ -1,58 +0,0 @@
#!/bin/bash
# 启动 gangyan 项目所有服务(与 /opt/start_all.sh 同步维护时可覆盖过去)
echo "启动 gangyan 项目服务..."
# 1. 启动 Docker 服务
echo "[1/4] 启动 Docker 服务..."
cd /opt/download/oss_files/gangyan-deploy/gangyan/milvus && docker compose up -d
cd /opt/download/oss_files/gangyan-deploy/gangyan/mysql/mysql-8.4.4 && docker compose up -d
docker start redis-server 2>/dev/null || docker run -d --name redis-server -p 6379:6379 redis:7-alpine
# 2. 启动 Java 后端MySQL 33306 / Redis 见 jar 内 application-yj.yml勿在此加 -Dspring.datasource* 以免覆盖端口)
echo "[2/4] 启动 Java 后端..."
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH
pkill -f "chat_web_yj.jar" 2>/dev/null
sleep 2
cd /opt/download/oss_files/gangyan-deploy/gangyan/backend
LOCAL_CFG="/opt/download/oss_files/gangyan-deploy/gangyan/backend/application-local.yml"
EXTRA_JAVA=()
[ -f "$LOCAL_CFG" ] && EXTRA_JAVA=(-Dspring.config.additional-location="file:${LOCAL_CFG}")
nohup java -jar \
-Xms512m -Xmx2048m \
"${EXTRA_JAVA[@]}" \
-Dspring.profiles.active=yj \
chat_web_yj.jar > nohup.out 2>&1 &
# 3. 启动前端
echo "[3/4] 启动前端..."
if ! pgrep -f "vite" > /dev/null; then
cd /opt/download/oss_files/gangyan-deploy/gangyan/chat_web_front
nohup npm run dev > nohup.out 2>&1 &
fi
# 4. 启动 langchain-chat可选
# --all-api仅 API(7861)+依赖进程,不启 Streamlit WebUI-a 需要 pip install streamlit
echo "[4/4] 启动 langchain-chat可选..."
read -p "是否启动 langchain-chat? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
source /opt/software/miniconda3/etc/profile.d/conda.sh
conda activate langchain-chat
cd /opt/download/oss_files/gangyan-deploy/gangyan/langchain-chat
export PYTHONPATH="/opt/download/oss_files/gangyan-deploy/gangyan/langchain-chat"
nohup python startup.py --all-api > langchain.log 2>&1 &
echo "langchain-chat 启动中API :7861查看日志: tail -f langchain.log"
# PDF 预览依赖的本地转换微服务(:6006PyMuPDF 抽文本→Markdown
bash /opt/download/oss_files/gangyan-deploy/gangyan/scripts/pdf-convert-service.sh
echo "pdf-convert-service 已尝试启动(:6006日志: tail -f /opt/download/oss_files/gangyan-deploy/gangyan/logs/pdf-convert-service.log"
fi
echo ""
echo "服务启动完成!"
echo " 前端: http://localhost:3000/metalinfo"
echo " 后端: http://localhost:8099/chat_web_backend"
echo " MySQL: localhost:33306 (Docker 映射,勿用 3306 连宿主)"
echo " Redis: localhost:6379"
echo " Milvus: localhost:19530"