This commit is contained in:
ivamp 2024-09-12 00:50:46 +08:00
parent a0c325cd97
commit 92da9181e1
25 changed files with 402 additions and 178 deletions

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
out
.gitignore

17
.eslintrc.cjs Normal file
View File

@ -0,0 +1,17 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'@electron-toolkit',
'@electron-toolkit/eslint-config-ts/eslint-recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier'
],
rules: {
'vue/require-default-prop': 'off',
'vue/multi-word-component-names': 'off'
}
}

7
openapitools.json Normal file
View File

@ -0,0 +1,7 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.8.0"
}
}

View File

@ -1,6 +1,5 @@
.gitignore
.npmignore
.openapi-generator-ignore
api.ts
base.ts
common.ts

4
src/components.d.ts vendored
View File

@ -7,14 +7,14 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AssistantMenu: typeof import('./components/AssistantMenu.vue')['default']
Chat: typeof import('./components/chat/chat.vue')['default']
ChatMenu: typeof import('./components/ChatMenu.vue')['default']
Container: typeof import('./components/Container.vue')['default']
copy: typeof import('./components/Menu copy.vue')['default']
copy: typeof import('./components/AssistantMenu.vue')['default']
Menu: typeof import('./components/Menu.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Test: typeof import('./components/chat/test.vue')['default']
UserMenu: typeof import('./components/UserMenu.vue')['default']
}
}

View File

@ -0,0 +1,41 @@
<template>
<n-list hoverable clickable>
<template #header>
<div>
<span class="text-xl">切换助理</span>
</div>
</template>
<n-list-item v-for="a in assistantStore.assistants" :key="a.id">
<n-thing>
{{ a.name }}
</n-thing>
</n-list-item>
</n-list>
</template>
<script setup lang="ts">
import { useRoute } from "vue-router";
import { useUserStore } from "../stores/user";
import { updateAll } from "../plugins/update/update";
import { useAssistantStore } from "../stores/assistants";
const route = useRoute();
const currentRoute: any = computed(() => route.name);
const collapsed = ref(false);
const userStore = useUserStore();
const assistantStore = useAssistantStore();
watch(
() => userStore.logined,
(newValue, oldValue) => {
if (newValue) {
update();
}
}
);
function update() {
updateAll();
}
update();
</script>

View File

@ -8,8 +8,10 @@
/> -->
<n-list hoverable clickable>
<template #header>
<!-- <n-icon :component="ChatboxOutline" /> -->
<span>对话列表</span>
<div class="text-xl">
<n-icon :component="ChatboxOutline" class="align-middle" />
<span class="font-xl ml-2">对话列表</span>
</div>
</template>
<n-list-item v-for="i in 10" :key="i">
<n-thing> 对话 </n-thing>

View File

@ -5,7 +5,7 @@ import { useIsMobile } from "../utils/composables.js";
import Menu from "../components/Menu.vue";
import { useUserStore } from "../stores/user";
import Guest from "../pages/guest/index.vue";
import router from "../plugins/router";
import router from "../router";
const currentRoute = computed(() => router.currentRoute.value.name);
const userStore = useUserStore();

View File

@ -17,10 +17,28 @@
</template>
<Menu></Menu>
</n-popover>
<!-- 更新状态 -->
<div v-show="appStore.updating">正在更新数据</div>
</n-grid-item>
<n-grid-item class="flex items-center justify-end mr-1.5">
<!-- 右侧 -->
<!-- 助理选择 -->
<n-popover
:placement="userPlacement"
class="w-full"
trigger="click"
style="padding: 0"
>
<template #trigger>
<n-icon class="text-2xl mr-4">
<PersonOutline />
</n-icon>
</template>
<AssistantMenu class="select-none" />
</n-popover>
<!-- 用户弹出 -->
<n-popover
:placement="userPlacement"
class="w-full"
@ -45,16 +63,18 @@
<script setup lang="ts">
import UserMenu from "../components/UserMenu.vue";
import AssistantMenu from "../components/AssistantMenu.vue";
import { useAppStore } from "../stores/app";
import { useUserStore } from "../stores/user";
import { useIsMobile, useIsTablet } from "../utils/composables";
import { MenuOutline } from "@vicons/ionicons5";
import { MenuOutline, PersonOutline } from "@vicons/ionicons5";
const userStore = useUserStore();
const isMobile = useIsMobile();
const isTablet = useIsTablet();
const appStore = useAppStore();
const userPlacement = ref("bottom");
if (isMobile.value) {
userPlacement.value = "bottom";
}
</script>

View File

@ -1,30 +1,23 @@
console.log("load")
const meta = document.createElement("meta");
meta.name = "naive-ui-style";
document.head.appendChild(meta);
import "./style.css";
import { registerPlugins } from "./plugins";
import router from "./router";
import { createApp } from "vue";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import naive from "naive-ui";
// 通用字体
import "vfonts/Lato.css";
// 等宽字体
import "vfonts/FiraCode.css";
import App from "./App.vue";
import router from "./plugins/router";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
import { createApp } from "vue";
const app = createApp(App);
app.use(pinia);
app.use(naive);
registerPlugins(app);
app.use(router);
// @ts-ignore ...
if (process.env.NODE_ENV === "production") {
setTimeout(() => {

View File

@ -7,7 +7,7 @@
<script setup lang="ts">
import axios from "axios";
import { useUserStore } from "../../stores/user";
import router from "../../plugins/router";
import router from "../../router";
import config from "../../config/config";
const userStore = useUserStore();

View File

@ -8,7 +8,7 @@
import config from "../../config/config";
import axios from "axios";
import { useUserStore } from "../../stores/user";
import router from "../../plugins/router";
import router from "../../router";
const userStore = useUserStore();
function generateRandomString(length: number) {

View File

@ -14,7 +14,7 @@
</template>
<script setup lang="ts">
import router from "../../plugins/router";
import router from "../../router";
import { useUserStore } from "../../stores/user";
const userStore = useUserStore();

View File

@ -15,7 +15,6 @@
:text="text"
@input="handleInput"
height="500px"
></v-md-preview>
</div>
</div>
@ -24,20 +23,18 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import VMdPreview from '@kangc/v-md-editor/lib/preview';
import '@kangc/v-md-editor/lib/style/preview.css';
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
import '@kangc/v-md-editor/lib/theme/style/github.css';
import VMdPreview from "@kangc/v-md-editor/lib/preview";
import "@kangc/v-md-editor/lib/style/preview.css";
import githubTheme from "@kangc/v-md-editor/lib/theme/github.js";
import "@kangc/v-md-editor/lib/theme/style/github.css";
// highlightjs
import hljs from 'highlight.js';
import hljs from "highlight.js";
VMdPreview.use(githubTheme, {
Hljs: hljs,
});
const text = ref("text");
const textLength = 10; //
const textParts = ref<string[]>([]);
@ -59,10 +56,10 @@ onMounted(() => {
updateTextParts();
// text.value
// setInterval(() => {
// const randomString = Math.random().toString(36).substring(2, 2 + textLength);
// text.value += randomString;
// }, 1000);
// setInterval(() => {
// const randomString = Math.random().toString(36).substring(2, 2 + textLength);
// text.value += randomString;
// }, 1000);
});
onUnmounted(() => {

View File

@ -1,36 +1,57 @@
// import {
// AssistantApi,
// ChatApi,
// ChatMessageApi,
// ChatPublicApi,
// Configuration,
// PingApi,
// ToolApi,
// } from "@/api";
// import config from "@/config/config";
// import { useUserStore } from "@/stores/user";
//
// const userStore = useUserStore();
//
// const conf = new Configuration();
//
// conf.basePath = config.backend;
// conf.apiKey = () => {
// return "Bearer " + userStore.id_token;
// };
import {
AssistantApi,
ChatApi,
ChatMessageApi,
ChatPublicApi,
Configuration,
PingApi,
ToolApi,
} from "../api";
import config from "../config/config";
// userStore.$subscribe((mutation, state) => {
// console.log(mutation);
// conf.apiKey = "Bearer " + state.id_token;
// });
import { useUserStore } from "../stores/user";
import axios from "./axios";
// const api = {
// Chat: new ChatApi(conf),
// Assistant: new AssistantApi(conf),
// Ping: new PingApi(conf),
// Tool: new ToolApi(conf),
// ChatMessage: new ChatMessageApi(conf),
// ChatPublic: new ChatPublicApi(conf),
// };
//
// export { api, conf };
// 定义 Api 类型
interface Api {
Chat: ChatApi;
Assistant: AssistantApi;
Ping: PingApi;
Tool: ToolApi;
ChatMessage: ChatMessageApi;
ChatPublic: ChatPublicApi;
}
let api: Api | null = null; // 使用联合类型来表示初始状态可能是 null
const getApi = () => {
if (api) {
return api;
}
const userStore = useUserStore();
const conf = new Configuration();
conf.basePath = config.backend;
conf.apiKey = () => {
return "Bearer " + userStore.id_token;
};
userStore.$subscribe((mutation, state) => {
conf.apiKey = "Bearer " + state.id_token;
});
api = {
Chat: new ChatApi(conf, undefined, axios),
Assistant: new AssistantApi(conf, undefined, axios),
Ping: new PingApi(conf, undefined, axios),
Tool: new ToolApi(conf, undefined, axios),
ChatMessage: new ChatMessageApi(conf, undefined, axios),
ChatPublic: new ChatPublicApi(conf, undefined, axios),
};
return api;
};
export default getApi;

View File

@ -1,5 +1,10 @@
import axios from 'axios'
import { request, response } from "./httpInterceptors";
axios.interceptors.request.use(request.onFulfilled, request.onRejected)
axios.interceptors.response.use(response.onFulfilled, response.onRejected)
// axios.defaults.adapter = window.axiosHttpAdapter
export default axios

View File

@ -0,0 +1,71 @@
import { useUserStore } from "../stores/user";
import { h, computed } from "vue";
import { createDiscreteApi, darkTheme, lightTheme, useOsTheme } from "naive-ui";
import type { ConfigProviderProps } from "naive-ui";
const osThemeRef = useOsTheme();
const configProviderPropsRef = computed<ConfigProviderProps>(() => ({
theme: osThemeRef.value === "light" ? lightTheme : darkTheme,
}));
const { dialog, loadingBar } = createDiscreteApi(
["message", "dialog", "notification", "loadingBar", "modal"],
{
configProviderProps: configProviderPropsRef,
}
);
const request = {
onFulfilled: (config: any) => {
if (config.headers === undefined) {
config.headers = {};
}
loadingBar.start();
return Promise.resolve(config);
},
onRejected: (error: any) => {
console.error(error);
// loadingBar.error()
return Promise.reject(error);
},
};
const response = {
onFulfilled: (res: any) => {
// if 20x
if (res.status >= 200 && res.status < 300) {
loadingBar.finish();
} else if (res.status >= 400 && res.status < 600) {
loadingBar.error();
}
return Promise.resolve(res);
},
onRejected: (error: any) => {
loadingBar.error();
console.error("axios error", error);
let data = [];
if (error.response.data.data) {
data = error.response.data.data;
}
if (error.response.data.message) {
data = error.response.data.message;
}
if (error.response.data.error) {
data = error.response.data.error.message;
}
return Promise.reject(error);
},
};
export { request, response };

13
src/plugins/index.ts Normal file
View File

@ -0,0 +1,13 @@
import pinia from "../stores";
import naive from "naive-ui";
// Types
import type { App } from "vue";
// 通用字体
import "vfonts/Lato.css";
// 等宽字体
import "vfonts/FiraCode.css";
export function registerPlugins(app: App) {
app.use(naive).use(pinia);
}

View File

@ -0,0 +1,14 @@
import { useAssistantStore } from "../../stores/assistants";
import api from "../../plugins/api";
import { useUserStore } from "../../stores/user";
const updateAll = async () => {
// 更新所有的数据
const userStore = useUserStore();
const assistantStore = useAssistantStore();
const assistantList = await api().Assistant.apiV1AssistantsGet();
assistantStore.assistants = assistantList.data.data;
};
export { updateAll };

View File

@ -4,28 +4,6 @@ import { defineStore } from "pinia";
export const useAppStore = defineStore("app", {
persist: false,
state: () => ({
navigation_drawer: false,
navigation_items: [
{
icon: "mdi-home",
text: "主页",
to: "/",
},
{
icon: "mdi-assistant",
text: "助理",
to: "/assistants",
},
{
icon: "mdi-tools",
text: "工具",
to: "/tools",
},
{
icon: "mdi-key",
text: "令牌",
to: "/tokens",
},
],
updating: false,
}),
});

10
src/stores/assistants.ts Normal file
View File

@ -0,0 +1,10 @@
// Utilities
import { defineStore } from "pinia";
import { EntityAssistant } from "../api";
export const useAssistantStore = defineStore("assistant", {
persist: false,
state: () => ({
assistants: <EntityAssistant[] | undefined> [],
}),
});

9
src/stores/chat.ts Normal file
View File

@ -0,0 +1,9 @@
import { defineStore } from "pinia";
import { EntityChat } from "../api";
export const useChatStore = defineStore("chats", {
persist: false,
state: () => ({
chats: <EntityChat[] | undefined>[],
}),
});

15
src/stores/index.ts Normal file
View File

@ -0,0 +1,15 @@
// Utilities
import { createPinia, setActivePinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
// console
// 设置当前活跃的 Pinia 实例
// setActivePinia(pinia);
export default pinia;

View File

@ -4,93 +4,101 @@ import config from "../config/config";
import { Base64 } from "js-base64";
let timer: any = null;
export const useUserStore = () => {
const innerStore = defineStore("user", {
persist: true,
state: () => ({
logined: false,
id_token: "",
refresh_token: "",
access_token: "",
expired_at: 0,
user: {
id: 0,
name: "",
email: "",
avatar: "",
},
timer: 0,
}),
actions: {
login(
idToken: string,
accessToken: string,
refreshToken: string,
expiredAt: number
) {
const idTokenParts = idToken.split(".");
export const useUserStore = defineStore("user", {
persist: true,
state: () => ({
logined: false,
id_token: "",
refresh_token: "",
access_token: "",
expired_at: 0,
user: {
id: 0,
name: "",
email: "",
avatar: "",
},
timer: 0,
}),
actions: {
login(
idToken: string,
accessToken: string,
refreshToken: string,
expiredAt: number
) {
const idTokenParts = idToken.split(".");
const idTokenPayload = JSON.parse(Base64.decode(idTokenParts[1]));
const idTokenPayload = JSON.parse(Base64.decode(idTokenParts[1]));
expiredAt = Date.now() + expiredAt * 1000;
this.expired_at = expiredAt;
expiredAt = Date.now() + expiredAt * 1000;
this.expired_at = expiredAt;
this.refresh_token = refreshToken;
this.access_token = accessToken;
this.refresh_token = refreshToken;
this.access_token = accessToken;
this.id_token = idToken;
this.user = { ...idTokenPayload };
this.logined = true;
},
checkAndRefresh() {
if (this.logined) {
if (this.expired_at - Date.now() < 60000) {
this.refresh();
}
}
},
setupTimer() {
this.checkAndRefresh();
timer = setInterval(() => {
this.checkAndRefresh();
}, 10 * 1000);
},
async refresh() {
const discovery = await axios.get(config.oauth_discovery_url);
// post /oauth/token to refresh
// 构建 form 参数
const data = new URLSearchParams();
data.set("grant_type", "refresh_token");
data.set("refresh_token", this.refresh_token);
data.set("client_id", config.oauth_client_id);
data.set("scope", config.oauth_scope);
axios
.post(discovery.data.token_endpoint, data)
.then((response) => {
this.login(
response.data.id_token,
response.data.access_token,
response.data.refresh_token,
response.data.expires_in
);
})
.catch((error) => {
// if 401
if (error.response.status === 401) {
console.log("Refresh token failed");
this.id_token = idToken;
this.user = { ...idTokenPayload };
this.logined = true;
},
checkAndRefresh() {
if (this.logined) {
if (this.expired_at - Date.now() < 60000) {
this.refresh();
}
// logout
this.logout();
clearInterval(timer);
});
}
},
setupTimer() {
this.checkAndRefresh();
timer = setInterval(() => {
this.checkAndRefresh();
}, 10 * 1000);
},
async refresh() {
const discovery = await axios.get(config.oauth_discovery_url);
// post /oauth/token to refresh
// 构建 form 参数
const data = new URLSearchParams();
data.set("grant_type", "refresh_token");
data.set("refresh_token", this.refresh_token);
data.set("client_id", config.oauth_client_id);
data.set("scope", config.oauth_scope);
axios
.post(discovery.data.token_endpoint, data)
.then((response) => {
this.login(
response.data.id_token,
response.data.access_token,
response.data.refresh_token,
response.data.expires_in
);
})
.catch((error) => {
// if 401
if (error.response.status === 401) {
console.log("Refresh token failed");
}
// logout
this.logout();
clearInterval(timer);
});
},
logout() {
this.$reset();
this.user = this.$state.user;
this.id_token = this.$state.id_token;
this.logined = this.$state.logined;
},
getIdToken() {
return this.id_token;
},
},
logout() {
this.$reset();
this.user = this.$state.user;
this.id_token = this.$state.id_token;
this.logined = this.$state.logined;
},
},
});
});
console.log("storage")
return innerStore()
};