mirror of
https://github.com/Kakune55/PyGetGPT.git
synced 2025-09-14 11:19:31 +08:00
Compare commits
29 Commits
2b8f18157d
...
2.0Dev
Author | SHA1 | Date | |
---|---|---|---|
848484682a | |||
aab5043fed | |||
98ce021726 | |||
c10d5d6ff8 | |||
64f58ab5ec | |||
dc9d9d9369 | |||
8e086129b7 | |||
4a809254cd | |||
d6dffde18f | |||
d533cfc6d1 | |||
|
68ae60d529 | ||
dfc0071d1f | |||
dc374e5259 | |||
afdbdec803 | |||
7b66cbb7e4 | |||
31aa97deca | |||
f16d34e5c3 | |||
316a07a85f | |||
e42a2042a9 | |||
bfef66d293 | |||
3157ccd3a9 | |||
f85cf254ca | |||
b25e598f39 | |||
e00a704a56 | |||
8d9bd619c4 | |||
50445d8af3 | |||
9837a91e65 | |||
6654ea0953 | |||
90403d78bb |
0
.gitattributes
vendored
Normal file
0
.gitattributes
vendored
Normal file
37
.gitignore
vendored
37
.gitignore
vendored
@@ -1,2 +1,37 @@
|
||||
__pycache__
|
||||
.venv
|
||||
test.py
|
||||
data
|
||||
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
*.json
|
||||
|
84
configUtil.py
Normal file
84
configUtil.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import configparser
|
||||
|
||||
class ConfigUtil:
|
||||
# 初始化ConfigParser对象
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
def __init__(self,path:str):
|
||||
# 读取并加载配置文件config.ini
|
||||
self.config.read(path,encoding="utf-8")
|
||||
|
||||
def get(self, section: str, key: str) -> str:
|
||||
"""
|
||||
获取配置项的字符串值
|
||||
|
||||
参数:
|
||||
section (str): 配置文件中的节名
|
||||
key (str): 节中的配置项键名
|
||||
|
||||
返回:
|
||||
str: 配置项的字符串值
|
||||
"""
|
||||
return self.config.get(section, key)
|
||||
|
||||
def getBool(self, section: str, key: str) -> bool:
|
||||
"""
|
||||
获取配置项的布尔值
|
||||
|
||||
参数:
|
||||
section (str): 配置文件中的节名
|
||||
key (str): 节中的配置项键名
|
||||
|
||||
返回:
|
||||
bool: 配置项的布尔值
|
||||
"""
|
||||
return self.config.getboolean(section, key)
|
||||
|
||||
def getInt(self, section: str, key: str) -> int:
|
||||
"""
|
||||
获取配置项的整数值
|
||||
|
||||
参数:
|
||||
section (str): 配置文件中的节名
|
||||
key (str): 节中的配置项键名
|
||||
|
||||
返回:
|
||||
int: 配置项的整数值
|
||||
"""
|
||||
return self.config.getint(section, key)
|
||||
|
||||
def getFloat(self, section: str, key: str) -> float:
|
||||
"""
|
||||
获取配置项的浮点数值
|
||||
|
||||
参数:
|
||||
section (str): 配置文件中的节名
|
||||
key (str): 节中的配置项键名
|
||||
|
||||
返回:
|
||||
float: 配置项的浮点数值
|
||||
"""
|
||||
return self.config.getfloat(section, key)
|
||||
|
||||
|
||||
def getSectionList(self) -> list:
|
||||
"""
|
||||
获取配置文件中的所有节名
|
||||
|
||||
返回:
|
||||
list: 所有节名的列表
|
||||
"""
|
||||
return self.config.sections()
|
||||
|
||||
|
||||
def getKeyList(self, section: str) -> list:
|
||||
"""
|
||||
获取指定节中的所有键名
|
||||
|
||||
参数:
|
||||
section (str): 配置文件中的节名
|
||||
|
||||
返回:
|
||||
list: 指定节中的所有键名的列表
|
||||
"""
|
||||
return self.config.options(section)
|
89
dao/db/user.py
Normal file
89
dao/db/user.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import sqlite3, time
|
||||
import configUtil
|
||||
|
||||
def getConnection() -> sqlite3.Connection:
|
||||
return sqlite3.connect(configUtil.ConfigUtil("config.ini").get("database","path"))
|
||||
|
||||
|
||||
def init():
|
||||
conn = getConnection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
'''
|
||||
CREATE TABLE User (
|
||||
uid TEXT,
|
||||
email TEXT,
|
||||
password_hash TEXT,
|
||||
created_at INT,
|
||||
surplus INT);
|
||||
'''
|
||||
)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
print("数据库初始化完成")
|
||||
|
||||
|
||||
def addUser(uid: str, email: str, password_hash: str) -> bool:
|
||||
conn = getConnection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
"INSERT INTO User (uid, email, password_hash, created_at, surplus) VALUES (?, ?, ?, ?, ?);",
|
||||
[uid, email, password_hash, int(time.time()), 0]
|
||||
)
|
||||
conn.commit()
|
||||
return True
|
||||
except sqlite3.IntegrityError:
|
||||
return False
|
||||
|
||||
|
||||
def checkUser(uid: str,password_hash) -> bool:
|
||||
"""检查用户与密码是否合法 可输入邮箱或uid"""
|
||||
conn = getConnection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM User WHERE ( uid = ? AND password_hash = ? ) OR ( email = ? AND password_hash = ? )",[uid,password_hash,uid,password_hash])
|
||||
result = cursor.fetchone()
|
||||
return result != None
|
||||
|
||||
|
||||
def getUser(uid: str) -> dict:
|
||||
"""获取用户信息"""
|
||||
conn = getConnection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM User WHERE uid = ?",[uid])
|
||||
result = cursor.fetchone()
|
||||
return {"uid":result[0],"email":result[1],"password_hash":result[2],"created_at":result[3],"surplus":result[4]}
|
||||
|
||||
|
||||
def updateUserSurplus(uid: str, surplus: int) -> bool:
|
||||
"""更新用户剩余额度"""
|
||||
conn = getConnection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute("UPDATE User SET surplus = ? WHERE uid = ?",[surplus,uid])
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
except: return False
|
||||
return True
|
||||
|
||||
def getUserSurplus(uid: str) -> int:
|
||||
"""获取用户剩余额度"""
|
||||
return getUser(uid)["surplus"]
|
||||
|
||||
|
||||
def updateUserPasswd(uid: str, password_hash: str) -> bool:
|
||||
"""更新用户密码"""
|
||||
conn = getConnection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute("UPDATE User SET password_hash = ? WHERE uid = ?",[password_hash,uid])
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
except: return False
|
||||
return True
|
||||
|
||||
|
||||
|
@@ -1,7 +0,0 @@
|
||||
# 配置文件说明
|
||||
{"url": "<此处为api URL>", "userkey": "<此处为你的UserKey>", "context": <此处为1 开启上下位关联功能 默认0关闭>}
|
||||
注意:开启上下文关联会消耗大量的token
|
||||
# 什么是Token
|
||||
在大型语言模型中,"token"是指文本中的一个最小单位。 通常,一个token可以是一个单词、一个标点符号、一个数字、一个符号等。 在自然语言处理中,tokenization是将一个句子或文本分成tokens的过程。 在大型语言模型的训练和应用中,模型接收一串tokens作为输入,并尝试预测下一个最可能的token。
|
||||
# 上下文关联
|
||||
在使用上下文关联时,模型除了处理你当前的问题时还需要一并将历史对话的一部分共同处理。所以需要消耗更多的token。在本应用中token消耗平均值大约是不开启上下文功能的300%。
|
@@ -1,17 +0,0 @@
|
||||
# server API 接口文档
|
||||
|
||||
## 请求格式(json)
|
||||
| 键 | 类型 | 必选 | 示例 |
|
||||
|---------|------|-----|-----|
|
||||
| prompt | str | yes | "你好" |
|
||||
| userkey | str | yes | "2b3j41b2xh1hz1" |
|
||||
| history | list | no | "history":[{"user":"XXXXXX","bot":"XXXXXX"},{"user":"XXXXXX""bot":"XXXXXX"}] |
|
||||
|
||||
|
||||
## 返回格式(json)
|
||||
| 键 | 类型 | 示例 |
|
||||
|----|------|------|
|
||||
| code | int | 200 |
|
||||
| output | str | "你好" |
|
||||
| surplus | int | 10000 |
|
||||
| |
|
3
frontend/env.d.ts
vendored
Normal file
3
frontend/env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue"
|
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
1861
frontend/package-lock.json
generated
Normal file
1861
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
frontend/package.json
Normal file
30
frontend/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.7",
|
||||
"element-plus": "^2.8.5",
|
||||
"marked": "^14.1.3",
|
||||
"vue": "^3.5.11",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^20.16.11",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"npm-run-all2": "^6.2.3",
|
||||
"typescript": "~5.5.4",
|
||||
"vite": "^5.4.8",
|
||||
"vue-tsc": "^2.1.6"
|
||||
}
|
||||
}
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
34
frontend/src/App.vue
Normal file
34
frontend/src/App.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-container">
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 聊天框容器样式 */
|
||||
.chat-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 90vh; /* 高度占据视口的 90% */
|
||||
width: 90vw; /* 宽度占据视口的 90% */
|
||||
margin: auto;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden; /* 防止内容溢出 */
|
||||
}
|
||||
|
||||
/* 聊天框内部内容样式 */
|
||||
.chat-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto; /* 允许垂直滚动 */
|
||||
}
|
||||
</style>
|
86
frontend/src/assets/base.css
Normal file
86
frontend/src/assets/base.css
Normal file
@@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
35
frontend/src/assets/main.css
Normal file
35
frontend/src/assets/main.css
Normal file
@@ -0,0 +1,35 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
15
frontend/src/main.ts
Normal file
15
frontend/src/main.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
// import ElementPlus from 'element-plus';
|
||||
// import 'element-plus/dist/index.css';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
// app.use(ElementPlus);
|
||||
|
||||
app.mount('#app');
|
||||
|
29
frontend/src/router/index.ts
Normal file
29
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import ChatPage from '../views/ChatPage.vue';
|
||||
import LoginPage from '../views/LoginPage.vue';
|
||||
|
||||
const routes = [
|
||||
{ path: '/', redirect: '/chat' },
|
||||
{ path: '/login', component: LoginPage },
|
||||
{ path: '/chat', component: ChatPage }, // 聊天页面
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
// 路由守卫,确保用户登录后才能访问聊天页面
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isAuthenticated = document.cookie.includes('auth_token'); // 假设后端返回的是这个Cookie
|
||||
if (to.path === '/chat' && !isAuthenticated) {
|
||||
next('/login'); // 未登录时跳转到登录页面
|
||||
} else {
|
||||
next(); // 允许进入
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
340
frontend/src/views/ChatPage.vue
Normal file
340
frontend/src/views/ChatPage.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<div class="chat-app">
|
||||
<!-- 左侧对话列表 -->
|
||||
<aside :class="['conversation-list', { 'collapsed': isCollapsed }]">
|
||||
<div v-if="!isCollapsed" v-for="(conversation, index) in conversations" :key="index"
|
||||
class="conversation-item" @click="selectConversation(index)"
|
||||
:class="{ 'active': index === selectedConversation }">
|
||||
<span>对话 {{ index + 1 }}</span>
|
||||
<button @click.stop="deleteConversation(index)" class="delete-btn">删除</button>
|
||||
</div>
|
||||
<button v-if="!isCollapsed" @click="newConversation" class="new-conversation-btn">新建对话</button>
|
||||
<button @click="toggleCollapse" class="collapse-btn">{{ isCollapsed ? '>' : '折叠' }}</button>
|
||||
</aside>
|
||||
|
||||
<!-- 聊天内容区域 -->
|
||||
<section class="chat-container">
|
||||
<!-- 模型选择 -->
|
||||
<div class="model-selection">
|
||||
<select v-model="selectedModel" @change="fetchMessages" class="model-dropdown">
|
||||
<option v-for="model in models" :key="model.id" :value="model.id">
|
||||
{{ model.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 聊天窗口 -->
|
||||
<div ref="chatWindow" class="chat-window">
|
||||
<div v-for="(message, index) in conversations[selectedConversation].messages" :key="index"
|
||||
:class="['message', message.sender]">
|
||||
<div class="message-bubble" v-html="renderMarkdown(message.text)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<div class="input-area">
|
||||
<input v-model="userInput" @keyup.enter="sendMessage" placeholder="请输入您的消息..." class="chat-input" />
|
||||
<button @click="sendMessage" class="send-btn">发送</button>
|
||||
<button @click="clearHistory" class="clear-btn">清除记录</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { marked } from 'marked';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userInput: '',
|
||||
conversations: JSON.parse(localStorage.getItem('conversations')) || [
|
||||
{
|
||||
messages: [{ sender: 'bot', text: '你好!请选择一个模型开始聊天。' }],
|
||||
},
|
||||
],
|
||||
selectedConversation: 0,
|
||||
models: [],
|
||||
selectedModel: '',
|
||||
isCollapsed: false, // 控制菜单折叠的状态
|
||||
conversationLimit: 6, // 对话数量上限
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchModels();
|
||||
},
|
||||
methods: {
|
||||
async fetchModels() {
|
||||
try {
|
||||
const response = await axios.get('/api/chat/models');
|
||||
this.models = response.data.models;
|
||||
this.selectedModel = this.models[0].id;
|
||||
} catch (error) {
|
||||
console.error('获取模型列表时出错:', error);
|
||||
this.conversations[this.selectedConversation].messages.push({ sender: 'bot', text: '获取模型列表失败,请稍后再试。' });
|
||||
}
|
||||
},
|
||||
async sendMessage() {
|
||||
if (this.userInput.trim() === '' || !this.selectedModel) return;
|
||||
|
||||
const currentMessages = this.conversations[this.selectedConversation].messages;
|
||||
currentMessages.push({ sender: 'user', text: this.userInput });
|
||||
currentMessages.push({ sender: 'bot', text: '正在思考...' });
|
||||
|
||||
const userMessage = this.userInput;
|
||||
this.userInput = '';
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/chat', {
|
||||
message: userMessage,
|
||||
model: this.selectedModel,
|
||||
});
|
||||
|
||||
currentMessages.pop();
|
||||
currentMessages.push({ sender: 'bot', text: response.data.message });
|
||||
this.saveConversations();
|
||||
this.scrollToBottom();
|
||||
} catch (error) {
|
||||
console.error('发送消息出错:', error);
|
||||
currentMessages.push({ sender: 'bot', text: '抱歉!出现了错误。' });
|
||||
}
|
||||
},
|
||||
renderMarkdown(text) {
|
||||
return marked(text);
|
||||
},
|
||||
scrollToBottom() {
|
||||
const chatWindow = this.$refs.chatWindow;
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight;
|
||||
},
|
||||
saveConversations() {
|
||||
localStorage.setItem('conversations', JSON.stringify(this.conversations));
|
||||
},
|
||||
loadConversation() {
|
||||
this.scrollToBottom();
|
||||
},
|
||||
clearHistory() {
|
||||
this.conversations[this.selectedConversation].messages = [];
|
||||
this.saveConversations();
|
||||
},
|
||||
newConversation() {
|
||||
if (this.conversations.length >= this.conversationLimit) {
|
||||
alert(`对话数量已达到上限(${this.conversationLimit}条)。`);
|
||||
return;
|
||||
}
|
||||
this.conversations.push({
|
||||
messages: [{ sender: 'bot', text: '新对话已创建,请选择一个模型开始聊天。' }],
|
||||
});
|
||||
this.selectedConversation = this.conversations.length - 1;
|
||||
this.saveConversations();
|
||||
},
|
||||
deleteConversation(index) {
|
||||
if (this.conversations.length === 1) {
|
||||
alert('至少需要保留一个对话。');
|
||||
return;
|
||||
}
|
||||
if (confirm('确定要删除这个对话吗?')) {
|
||||
this.conversations.splice(index, 1);
|
||||
if (this.selectedConversation === index) {
|
||||
this.selectedConversation = 0;
|
||||
}
|
||||
this.saveConversations();
|
||||
}
|
||||
},
|
||||
selectConversation(index) {
|
||||
this.selectedConversation = index;
|
||||
this.scrollToBottom();
|
||||
},
|
||||
toggleCollapse() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.scrollToBottom();
|
||||
},
|
||||
updated() {
|
||||
this.scrollToBottom();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 全局容器样式 */
|
||||
.chat-app {
|
||||
display: flex;
|
||||
height: 90vh;
|
||||
width: 90vw;
|
||||
margin: auto;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 左侧对话列表 */
|
||||
.conversation-list {
|
||||
width: 20%;
|
||||
background-color: #f9f9f9;
|
||||
border-right: 1px solid #ddd;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.conversation-list.collapsed {
|
||||
width: 50px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.conversation-item.active {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.new-conversation-btn {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.new-conversation-btn:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background-color: #dc3545;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
margin-top: auto;
|
||||
padding: 5px 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.collapse-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
/* 右侧聊天窗口 */
|
||||
.chat-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.model-selection {
|
||||
padding: 10px;
|
||||
background-color: #f1f1f1;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.model-dropdown {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ddd;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message.bot {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
max-width: 60%;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message.user .message-bubble {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.message.bot .message-bubble {
|
||||
background-color: #f1f1f1;
|
||||
color: black;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
background-color: #f1f1f1;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 20px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.send-btn,
|
||||
.clear-btn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send-btn:hover,
|
||||
.clear-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
136
frontend/src/views/LoginPage.vue
Normal file
136
frontend/src/views/LoginPage.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<h2>登录</h2>
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="form-group">
|
||||
<label for="userinfo">UID或邮箱</label>
|
||||
<input
|
||||
type="text"
|
||||
id="userinfo"
|
||||
v-model="loginForm.userinfo"
|
||||
placeholder="请输入用户名"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
v-model="loginForm.password"
|
||||
placeholder="请输入密码"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="login-btn">登录</button>
|
||||
</div>
|
||||
</form>
|
||||
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loginForm: {
|
||||
userinfo: '',
|
||||
password: '',
|
||||
},
|
||||
errorMessage: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
try {
|
||||
const response = await axios.post('/api/user/login', this.loginForm);
|
||||
if (response.status === 200) {
|
||||
// 登录成功,跳转到聊天页面
|
||||
this.$router.push('/chat');
|
||||
}
|
||||
} catch (error) {
|
||||
// 登录失败,显示错误信息
|
||||
this.errorMessage = '登录失败,请检查用户名或密码';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background-color: #fff;
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
14
frontend/tsconfig.app.json
Normal file
14
frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
11
frontend/tsconfig.json
Normal file
11
frontend/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
19
frontend/tsconfig.node.json
Normal file
19
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
16
frontend/vite.config.ts
Normal file
16
frontend/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
16
main.py
Normal file
16
main.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
import router.chat
|
||||
import router.page
|
||||
import router.user
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router.chat.router)
|
||||
app.include_router(router.user.router)
|
||||
app.include_router(router.page.router)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
32
model/glm-4-flash.py
Normal file
32
model/glm-4-flash.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from zhipuai import ZhipuAI
|
||||
from model.util import InputData, OutputData, getModelAPIKey
|
||||
|
||||
# client = ZhipuAI(api_key="") # 填写您自己的APIKey
|
||||
# response = client.chat.completions.create(
|
||||
# model="glm-4-0520", # 填写需要调用的模型编码
|
||||
# messages=[
|
||||
# {"role": "user", "content": "作为一名营销专家,请为我的产品创作一个吸引人的slogan"},
|
||||
# {"role": "assistant", "content": "当然,为了创作一个吸引人的slogan,请告诉我一些关于您产品的信息"},
|
||||
# {"role": "user", "content": "智谱AI开放平台"},
|
||||
# {"role": "assistant", "content": "智启未来,谱绘无限一智谱AI,让创新触手可及!"},
|
||||
# {"role": "user", "content": "创造一个更精准、吸引人的slogan"}
|
||||
# ],
|
||||
# )
|
||||
# print(response.choices[0].message)
|
||||
|
||||
|
||||
|
||||
def predict(input_data:InputData):
|
||||
client = ZhipuAI(api_key=getModelAPIKey("glm-4-flash"))
|
||||
response = client.chat.completions.create(
|
||||
model="glm-4-flash", # 填写需要调用的模型编码
|
||||
messages=[
|
||||
{"role": "user", "content": input_data.message}],
|
||||
)
|
||||
if response.choices[0].finish_reason == "stop":
|
||||
return OutputData(response.choices[0].message.content,200,response.usage.total_tokens)
|
||||
elif response.choices[0].finish_reason == "length":
|
||||
return OutputData(response.choices[0].message.content,201,response.usage.total_tokens)
|
||||
elif response.choices[0].finish_reason == "network_error":
|
||||
return OutputData("Server Network Error",500,0)
|
||||
else: return OutputData("Unknown Error",500,0)
|
57
model/util.py
Normal file
57
model/util.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import importlib
|
||||
from configUtil import ConfigUtil
|
||||
|
||||
|
||||
class InputData:
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
class OutputData:
|
||||
def __init__(self, message,code,tokenUsed):
|
||||
self.message = message
|
||||
self.code = code
|
||||
self.tokenUsed = tokenUsed
|
||||
|
||||
|
||||
|
||||
def getModels() -> list:
|
||||
model_config = ConfigUtil("data/models.ini")
|
||||
out = []
|
||||
for model in model_config.getSectionList():
|
||||
if model_config.getBool(model, "enabled") == False:
|
||||
continue
|
||||
out.append(model)
|
||||
return out
|
||||
|
||||
|
||||
def getModelsInfo() -> list:
|
||||
model_config = ConfigUtil("data/models.ini")
|
||||
out = []
|
||||
for model in model_config.getSectionList():
|
||||
if model_config.getBool(model, "enabled") == False:
|
||||
continue
|
||||
util = {
|
||||
"name":model_config.get(model, "name"),
|
||||
"id":model,
|
||||
}
|
||||
out.append(util)
|
||||
return out
|
||||
|
||||
|
||||
def getModelAPIKey(model: str) -> str:
|
||||
model_config = ConfigUtil("data/models.ini")
|
||||
try:
|
||||
return model_config.get(model, "key")
|
||||
except:
|
||||
return "Model API key not found"
|
||||
|
||||
def requestModel(model: str, input_data: InputData) -> OutputData:
|
||||
if model not in getModels():
|
||||
ret = OutputData("Model not found",404,0)
|
||||
else:
|
||||
module = importlib.import_module(f"model.{model}")
|
||||
ret = module.predict(input_data)
|
||||
resq = {}
|
||||
for key in ret.__dict__: resq[key] = ret.__dict__[key]
|
||||
return resq
|
||||
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
requests
|
||||
fastapi[standard]==0.115.0
|
||||
zhipuai
|
21
router/chat.py
Normal file
21
router/chat.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from fastapi import APIRouter
|
||||
from model.util import InputData, getModels, getModelsInfo, requestModel
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
model: str
|
||||
message: str
|
||||
@router.post("/api/chat")
|
||||
def chat(req: ChatRequest):
|
||||
model_name = req.model
|
||||
input_data = InputData(req.message)
|
||||
#调用模型
|
||||
ret = requestModel(model_name, input_data)
|
||||
return ret
|
||||
|
||||
|
||||
@router.get("/api/chat/models")
|
||||
def get_models():
|
||||
return {"models": getModelsInfo()}
|
30
router/page.py
Normal file
30
router/page.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 将 Vue 构建后的 dist 目录中的 assets 作为静态资源目录
|
||||
#router.mount("/assets", StaticFiles(directory="frontend/dist/assets"), name="assets")
|
||||
@router.get("/assets/{path:path}")
|
||||
async def serve_static(path: str):
|
||||
if path.endswith(".css"):
|
||||
return FileResponse(f"frontend/dist/assets/{path}",media_type="text/css")
|
||||
elif path.endswith(".js"):
|
||||
return FileResponse(f"frontend/dist/assets/{path}",media_type="text/javascript")
|
||||
elif path.endswith(".png"):
|
||||
return FileResponse(f"frontend/dist/assets/{path}",media_type="image/png")
|
||||
else:
|
||||
return FileResponse(f"frontend/dist/assets/{path}")
|
||||
|
||||
|
||||
# 根路径提供 index.html
|
||||
@router.get("/")
|
||||
async def serve_vue_app():
|
||||
return FileResponse("frontend/dist/index.html")
|
||||
|
||||
# 通用路由处理,确保 history 模式下的路由正常工作
|
||||
@router.get("/{full_path:path}")
|
||||
async def serve_vue_app_catch_all(full_path: str):
|
||||
return FileResponse("frontend/dist/index.html")
|
19
router/user.py
Normal file
19
router/user.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import APIRouter, Response
|
||||
from pydantic import BaseModel
|
||||
|
||||
import dao.db.user as user
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
userinfo: str | None = None
|
||||
password: str
|
||||
|
||||
@router.post("/api/user/login")
|
||||
def login(response: Response,LoginRequest: LoginRequest):
|
||||
#if not user.checkUser(uid, password_hash):
|
||||
if False:
|
||||
return {"code": 401, "message": "Invalid credentials"}
|
||||
response.set_cookie(key="auth_token", value="1234567890")
|
||||
return {"code": 200, "message": "Login successful"}
|
79
server.sh
Normal file
79
server.sh
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/bin/bash
|
||||
|
||||
VENV_DIR=".venv"
|
||||
PYTHON_APP="main.py"
|
||||
LOG_FILE="output.log"
|
||||
PID_FILE="app.pid"
|
||||
|
||||
start_app() {
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo -e "\033[31m Virtual environment directory $VENV_DIR not found! \033[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$PYTHON_APP" ]; then
|
||||
echo -e "\033[31m Python application $PYTHON_APP not found! \033[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source "$VENV_DIR/bin/activate"
|
||||
nohup python3 "$PYTHON_APP" > "$LOG_FILE" 2>&1 &
|
||||
echo $! > "$PID_FILE"
|
||||
echo -e "\033[32m Application started! \033[0m"
|
||||
}
|
||||
|
||||
stop_app() {
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
echo -e "\033[31m PID file not found! \033[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pid=$(cat "$PID_FILE")
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
kill "$pid"
|
||||
rm "$PID_FILE"
|
||||
echo -e "\033[32m Application ended! \033[0m"
|
||||
else
|
||||
echo -e "\033[31m Application not running or PID not found! \033[0m"
|
||||
fi
|
||||
}
|
||||
|
||||
check_app_status() {
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
echo -e "\033[31m Application not running! \033[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pid=$(cat "$PID_FILE")
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
echo -e "PID: $pid"
|
||||
echo -e "\033[32m Application running! \033[0m"
|
||||
else
|
||||
echo -e "\033[31m Application not running! \033[0m"
|
||||
fi
|
||||
}
|
||||
|
||||
restart_app() {
|
||||
stop_app
|
||||
start_app
|
||||
echo -e "\033[32m Application restarted! \033[0m"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start_app
|
||||
;;
|
||||
stop)
|
||||
stop_app
|
||||
;;
|
||||
status)
|
||||
check_app_status
|
||||
;;
|
||||
restart)
|
||||
restart_app
|
||||
;;
|
||||
*)
|
||||
echo -e "\033[33m Usage: $0 {start|stop|status|restart} \033[0m"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
@@ -1,27 +0,0 @@
|
||||
import zhipuai , config
|
||||
|
||||
zhipuai.api_key = config.readConf()["chatglmturbo"]["Authorization"]
|
||||
|
||||
def service(prompt,history = ""):
|
||||
if history == "":
|
||||
response = zhipuai.model_api.invoke(
|
||||
model="chatglm_turbo",
|
||||
prompt=[
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
)
|
||||
else:
|
||||
response = zhipuai.model_api.invoke(
|
||||
model="chatglm_turbo",
|
||||
prompt=[
|
||||
{"role": "user", "content": history[1]["user"]},
|
||||
{"role": "assistant", "content": history[1]["bot"]},
|
||||
{"role": "user", "content": history[0]["user"]},
|
||||
{"role": "assistant", "content": history[0]["bot"]},
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
)
|
||||
if response["code"] == 200:
|
||||
return 200, response["data"]["choices"][0]["content"], response["data"]["usage"]['total_tokens']
|
||||
else:
|
||||
return 50 , str(response["code"])+response["msg"], 0
|
@@ -1,5 +0,0 @@
|
||||
import json
|
||||
|
||||
def readConf():
|
||||
with open('config.json') as f:
|
||||
return json.load(f)
|
@@ -1,58 +0,0 @@
|
||||
import pymysql , config
|
||||
|
||||
|
||||
def userSurplus(userkey):
|
||||
#打开数据库连接
|
||||
db = pymysql.connect(host=config.readConf()["db"]["host"],
|
||||
port=config.readConf()["db"]["port"],
|
||||
user=config.readConf()["db"]["user"],
|
||||
password=config.readConf()["db"]["passwd"],
|
||||
database=config.readConf()["db"]["database"])
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 使用 execute() 方法执行 SQL 查询
|
||||
cursor.execute(f"SELECT surplus FROM usersurplus WHERE userkey = '{userkey}';")
|
||||
# 使用 fetchone() 方法获取单条数据.
|
||||
data = cursor.fetchone()
|
||||
|
||||
# 关闭连接
|
||||
db.close()
|
||||
|
||||
if data != None:
|
||||
return data[0]
|
||||
return -99999
|
||||
|
||||
def reduce_value(userkey, value): # 减去对应的值
|
||||
#打开数据库连接
|
||||
db = pymysql.connect(host=config.readConf()["db"]["host"],
|
||||
port=config.readConf()["db"]["port"],
|
||||
user=config.readConf()["db"]["user"],
|
||||
password=config.readConf()["db"]["passwd"],
|
||||
database=config.readConf()["db"]["database"])
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 执行 SQL 查询以获取当前值
|
||||
cursor.execute(f"SELECT surplus FROM usersurplus WHERE userkey = '{userkey}';")
|
||||
current_value = cursor.fetchone()[0]
|
||||
|
||||
# 如果没有找到用户,则返回错误信息
|
||||
if current_value is None:
|
||||
db.close()
|
||||
return -1
|
||||
|
||||
# 计算新的值
|
||||
new_value = current_value - value
|
||||
|
||||
# 更新数据库中的值
|
||||
cursor.execute(f"UPDATE usersurplus SET surplus={new_value} WHERE userkey='{userkey}'")
|
||||
|
||||
# 提交事务
|
||||
db.commit()
|
||||
|
||||
# 关闭连接
|
||||
db.close()
|
||||
|
||||
# 返回新值
|
||||
return 0
|
@@ -1,48 +0,0 @@
|
||||
import flask , requests , json
|
||||
from flask_cors import CORS
|
||||
import db , qwenTurbo ,chatglmTurbo
|
||||
|
||||
|
||||
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
CORS(app,origins="*")
|
||||
|
||||
@app.route('/api/user', methods=['POST'])
|
||||
def post_data():
|
||||
userRequest = flask.request.json
|
||||
surplusToken = db.userSurplus(userRequest['userkey'])
|
||||
|
||||
if userRequest["prompt"] == "":
|
||||
return {"code":42,"output":"Input is empty"}
|
||||
|
||||
if userRequest["prompt"] == "":
|
||||
return {"code":42,"output":"UserKey is empty"}
|
||||
|
||||
if surplusToken == -99999: # 判断用户是否存在和余额
|
||||
return {"code":41,"output":"UserKey not found"}
|
||||
elif surplusToken <= 0:
|
||||
return {"code":40,"output":"Token has been use up"}
|
||||
|
||||
if userRequest["model"] == "qwen-turbo": # 调用qwen-Turbo
|
||||
if userRequest["context"] == 1: # 是否使用上文关联
|
||||
code , output , tokenUsed = qwenTurbo.service(userRequest['prompt'],userRequest['history'])
|
||||
elif userRequest["context"] == 0:
|
||||
code , output , tokenUsed = qwenTurbo.service(userRequest['prompt'])
|
||||
|
||||
if userRequest["model"] == "chatglm-turbo": # 调用chatglm-turbo
|
||||
if userRequest["context"] == 1: # 是否使用上文关联
|
||||
code , output , tokenUsed = chatglmTurbo.service(userRequest['prompt'],userRequest['history'])
|
||||
elif userRequest["context"] == 0:
|
||||
code , output , tokenUsed = chatglmTurbo.service(userRequest['prompt'])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
db.reduce_value(userRequest['userkey'], tokenUsed)
|
||||
return {"code":code,"output":output,"surplus":surplusToken}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True,host='0.0.0.0',port=5000)
|
@@ -1,31 +0,0 @@
|
||||
import requests , json , config
|
||||
|
||||
# 设置请求的目标URL
|
||||
url = config.readConf()["qwenturbo"]["url"] # 替换为你的API端点URL
|
||||
header = {
|
||||
"Content-Type":"application/json",
|
||||
"Authorization":config.readConf()["qwenturbo"]["Authorization"]
|
||||
}
|
||||
|
||||
def service(prompt,history = ""):
|
||||
# 设置请求数据
|
||||
if history == "":
|
||||
data = {
|
||||
"model": "qwen-turbo",
|
||||
"input":{
|
||||
"prompt":f"{prompt}"
|
||||
}
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"model": "qwen-turbo",
|
||||
"input":{
|
||||
"prompt":f"{prompt}",
|
||||
"history":history
|
||||
}
|
||||
}
|
||||
# 发送POST请求
|
||||
response = json.loads(requests.post(url, json=data ,headers=header).text)
|
||||
if 'code' in response:
|
||||
return 50,response['code']+response['message'],0
|
||||
return 200,response['output']['text'],response["usage"]["total_tokens"]
|
@@ -1,288 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KakuAI</title>
|
||||
<link rel="stylesheet" type="text/css" href="popup.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background-image: url("img/bg_circles.png");
|
||||
background-repeat: repeat;
|
||||
}
|
||||
#chat-container {
|
||||
width: 80%; /* 使用百分比宽度 */
|
||||
max-width: 1300px; /* 最大宽度,防止界面变得过宽 */
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
#chat-messages {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
word-wrap: break-word; /* 自动换行 */
|
||||
white-space: pre-wrap; /* 保留空格并自动换行 */
|
||||
}
|
||||
.message-divider {
|
||||
border-top: 1px solid #ccc;
|
||||
margin: 10px 0;
|
||||
}
|
||||
#user-input {
|
||||
width: 97%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
#user-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
#user-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
#user-input-button {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 10px 20px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#user-input-button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
/* 新增样式 */
|
||||
#additional-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between; /* 左右对齐 */
|
||||
margin-top: 10px;
|
||||
}
|
||||
#additional-controls input[type="checkbox"] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
#additional-controls button {
|
||||
background-color: #466d2b;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body onload="checkCookie()">
|
||||
<div id="chat-container">
|
||||
<div id="chat-messages"></div>
|
||||
<input type="text" id="user-input" placeholder="输入消息..."> <!-- 用户输入消息的输入框 -->
|
||||
<button id="user-input-button" disabled>发送</button> <!-- 发送消息按钮初始状态禁用 -->
|
||||
<!-- 新增复选框、文本和附加功能按钮 -->
|
||||
<div id="additional-controls">
|
||||
<label>
|
||||
<input type="checkbox" id="additional-checkbox"> 联系上文 <a id="showtoken"><br>剩余Token将会被显示在这里</a>
|
||||
</label>
|
||||
<button id="additional-button">设置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="popup-container" class="hidden"> <!--菜单部分-->
|
||||
<div id="popup">
|
||||
<button id="close-popup">关闭</button>
|
||||
<h1>设置</h1>
|
||||
<p>使用的AI模型</p>
|
||||
<select id="setUpDropdown" defaultValue="qwen-turbo">
|
||||
<option value="qwen-turbo">qwen-turbo</option>
|
||||
<option value="chatglm-turbo">chatglmTurbo</option>
|
||||
</select>
|
||||
<hr>
|
||||
<h3>当前UserKey</h3>
|
||||
<a id="showUserKey"><br>当前UserKey将会被显示在这里</a>
|
||||
<hr>
|
||||
<div id="buttons">
|
||||
<button id="setUpButton1">设置UserKey</button>
|
||||
<button id="setUpButton2" onclick="window.location.href = 'https://afdian.net/a/kaku55'">获取更多Token</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const chatMessages = document.getElementById('chat-messages'); // 聊天消息容器
|
||||
const userInput = document.getElementById('user-input'); // 用户输入消息的输入框
|
||||
const sendButton = document.getElementById('user-input-button'); // 发送消息按钮
|
||||
const additionalButton = document.getElementById('additional-button'); // 附加功能按钮
|
||||
const additionalCheckbox = document.getElementById('additional-checkbox'); // 复选框
|
||||
const popupContainer = document.getElementById('popup-container'); // 菜单容器
|
||||
const closePopup = document.getElementById('close-popup'); // 关闭菜单按钮
|
||||
const setUpButton1 = document.getElementById('setUpButton1');// 菜单按钮1
|
||||
|
||||
var userhs1 = "x"; // 历史记录的保存
|
||||
var userhs0 = "x";
|
||||
var boths1 = "x";
|
||||
var boths0 = "x";
|
||||
|
||||
// 关闭菜单
|
||||
closePopup.addEventListener('click', () => {
|
||||
popupContainer.style.right = '-100%';
|
||||
popupContainer.classList.add('hidden');
|
||||
});
|
||||
|
||||
// 点击发送按钮后的处理函数
|
||||
sendButton.addEventListener('click', sendMessage);
|
||||
// 用户输入消息后的处理函数
|
||||
userInput.addEventListener('input', handleUserInput);
|
||||
// 点击附加功能按钮后的处理函数
|
||||
additionalButton.addEventListener('click', additionalFunction);
|
||||
// 菜单按钮的处理函数
|
||||
setUpButton1.addEventListener('click',resetCookie);
|
||||
|
||||
// 发送消息函数
|
||||
function sendMessage() {
|
||||
const userMessage = userInput.value; // 获取用户输入的消息
|
||||
appendMessage('你', userMessage); // 在聊天界面中添加用户消息
|
||||
userInput.value = ''; // 清空输入框
|
||||
|
||||
// 立即禁用发送按钮
|
||||
sendButton.disabled = true;
|
||||
|
||||
// 在实际应用中,你可以将用户消息发送到后端进行处理
|
||||
// 在这个示例中,我们模拟了来自助手的响应
|
||||
setTimeout(function() {
|
||||
requestAPI(userMessage);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 在聊天界面中添加消息
|
||||
function appendMessage(sender, message) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.innerHTML = `<strong>${sender}:</strong> ${message}`;
|
||||
chatMessages.appendChild(messageDiv);
|
||||
|
||||
// 在每条消息后添加分隔线
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'message-divider';
|
||||
chatMessages.appendChild(divider);
|
||||
}
|
||||
|
||||
// 附加功能按钮的处理函数
|
||||
function additionalFunction() {
|
||||
// 处理附加功能按钮的点击事件
|
||||
popupContainer.classList.remove('hidden');
|
||||
popupContainer.style.right = '0';
|
||||
}
|
||||
|
||||
// 用户输入框的输入事件处理函数
|
||||
function handleUserInput() {
|
||||
// 根据用户输入的内容启用或禁用发送按钮
|
||||
sendButton.disabled = userInput.value.trim() === '';
|
||||
}
|
||||
|
||||
// 使用Cookie保存用户的UserKey
|
||||
function setCookie(cname, cvalue, exdays) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires=" + d.toGMTString();
|
||||
document.cookie = cname + "=" + cvalue + "; " + expires;
|
||||
}
|
||||
function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var ca = document.cookie.split(';');
|
||||
for (var i = 0; i < ca.length; i++) {
|
||||
var c = ca[i].trim();
|
||||
if (c.indexOf(name) == 0) { return c.substring(name.length, c.length); }
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function checkCookie() {
|
||||
var user = getCookie("userkey");
|
||||
if (user != "") {
|
||||
alert("欢迎回来 UserKey:" + user);
|
||||
document.getElementById("showUserKey").innerHTML = user;
|
||||
}
|
||||
else {
|
||||
user = prompt("请输入你的Userkey:", "");
|
||||
if (user != "" && user != null) {
|
||||
setCookie("userkey", user, 265);
|
||||
}
|
||||
}
|
||||
}
|
||||
function resetCookie() {
|
||||
user = prompt("请输入你的Userkey:", "");
|
||||
if (user != "" && user != null) {
|
||||
setCookie("userkey", user, 265);
|
||||
alert("UserKey已更新");
|
||||
document.getElementById("showUserKey").innerHTML = user;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function requestAPI(message) {
|
||||
// 创建包含JSON数据的对象
|
||||
var requestData = {
|
||||
"model":document.getElementById("setUpDropdown").value,
|
||||
"prompt": message,
|
||||
"userkey": getCookie("userkey"),
|
||||
"context": Number(additionalCheckbox.checked),
|
||||
"history": [
|
||||
{
|
||||
"user": userhs1 ,
|
||||
"bot": boths1
|
||||
},
|
||||
{
|
||||
"user": userhs0,
|
||||
"bot": boths0
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
fetch("http://chat.kakuweb.top:5000/api/user", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 在这里处理返回的JSON数据
|
||||
if (data["code"] == 200) {
|
||||
appendMessage("KakuAI"+"("+document.getElementById("setUpDropdown").value+")",data["output"]);
|
||||
document.getElementById("showtoken").innerHTML = "<br>剩余Tokens:" + data["surplus"];
|
||||
}
|
||||
else{
|
||||
alert("ErrCode:"+data["code"]+" "+data["output"])
|
||||
}
|
||||
userhs1 = userhs0;
|
||||
boths1 = boths0;
|
||||
userhs0 = message;
|
||||
boths0 = data["output"];
|
||||
loading = false;
|
||||
// 可以根据返回的数据执行相应的操作
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('请求出错:', error);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -1,47 +0,0 @@
|
||||
#popup-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#popup {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 20%;
|
||||
min-width: 150px;
|
||||
min-width: 200px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#close-popup {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
#dropdown {
|
||||
margin: auto;
|
||||
width: 80%; /* 让<select>元素宽度占满容器 */
|
||||
margin-bottom: 10px; /* 添加一些底部间距 */
|
||||
}
|
||||
|
||||
#buttons {
|
||||
margin: auto;
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column; /* 设置按钮垂直排列 */
|
||||
}
|
||||
|
||||
#buttons button {
|
||||
margin: 10px 0; /* 添加按钮之间的垂直间距 */
|
||||
}
|
Reference in New Issue
Block a user