Compare commits

...

1 Commits
daima ... main

Author SHA1 Message Date
majunying b612e7ceb9 v1.0
1 year ago

@ -0,0 +1,2 @@
VITE_GLOB_API_URL=/api
VITE_APP_API_BASE_URL=http://127.0.0.1:3002/

@ -0,0 +1,35 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.localnode_modules
dist
dist-ssr
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Project specifies
*.pem
*.crx
.history
.env.production
.vercel

@ -0,0 +1,54 @@
# build front-end
FROM node:lts-alpine AS frontend
RUN npm install npm -g
WORKDIR /app
COPY ./package.json /app
RUN npm install
COPY . /app
RUN npm run build
# build backend
FROM node:lts-alpine as backend
RUN npm install pnpm -g
WORKDIR /app
COPY /service/package.json /app
COPY /service/pnpm-lock.yaml /app
RUN pnpm install
COPY /service /app
RUN pnpm build
# service
FROM node:lts-alpine
RUN npm install pnpm -g
WORKDIR /app
COPY /service/package.json /app
COPY /service/pnpm-lock.yaml /app
RUN pnpm install --production && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/*
COPY /service /app
COPY --from=frontend /app/dist /app/public
COPY --from=backend /app/build /app/build
EXPOSE 3002
CMD ["pnpm", "run", "prod"]

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,133 @@
<p align="center">
<img src="https://lty-image-bed.oss-cn-shenzhen.aliyuncs.com/blog/GPT.png" width="200" height="200">
</p>
<div align="center">
# GPT Terminal
<!-- prettier-ignore-start -->
<!-- markdownlint-disable-next-line MD036 -->
_✨ Open GPT like a programmer! Customize your own GPT terminal. ✨_
<!-- prettier-ignore-end -->
<p align="center">
<img src="https://img.shields.io/github/v/release/ltyzzzxxx/gpt-web-terminal?display_name=tag" />
<img src="https://img.shields.io/github/stars/ltyzzzxxx/gpt-web-terminal" />
<img src="https://img.shields.io/github/forks/ltyzzzxxx/gpt-web-terminal" />
<img src="https://img.shields.io/github/issues/ltyzzzxxx/gpt-web-terminal" />
<img src="https://img.shields.io/badge/license-Apache%20-yellow.svg" />
</p>
</div>
[English Doc](./README_EN.md) | [中文文档](./README_CN.md)
## Introduction
GPT Terminal is a platform that allows you to have free conversations with GPT in the terminal.
Here, you can easily implement more customized functionalities and have your own GPT terminal!
The project is implemented based on Vue3 and Express.
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/2S2-Da)
## Preview
Demo Linkhttps://gpt-terminal.up.railway.app/
![gpt-terminal-demo](https://github.com/ltyzzzxxx/gpt-web-terminal/assets/73587471/dee28750-b86b-45a1-a8b2-8357b3e27a25)
## Features
- ✨ Support dialogue between command-line terminal and GPT. As programmers, we communicate with GPT in a geeky way!
- 🌟 Support configuration of API Key (OpenAI API method) and Access Token (Web API method) - either one can be chosen.
- 🌈 Support DIY of GPT roles and persist them. Have conversations with your exclusive role!
- ☁️ Support GPT to remember the conversation history and provide commands to query the history. Give your GPT a good memory~
- 🍀 Support the widely-used Event Stream technology for GPT responses, achieving a typewriter effect~
- 🌴 Support rendering GPT responses in Markdown format.
- 🍃 Basic terminal commands such as viewing command history, accessing help manuals, clearing the screen, etc.
## Quick Start
You only need to have a basic understanding of using `npm` to unlock all the features!
1. Clone the project to your local machine.
```bash
git clone https://github.com/ltyzzzxxx/gpt-web-terminal.git
```
2. Navigate to the project directory and install dependencies for the frontend and backend separately.
```bash
cd gpt-web-terminal && npm install
cd service && npm install
```
3. Configure your API Key or Access Token in `service/.env`. API key takes priority.
```
# Choose either API Key (OpenAI API method) or Access Token (Web API method)
OPENAI_API_KEY=
OPENAI_ACCESS_TOKEN=
# Configure reverse proxy address when using Access Token
API_REVERSE_PROXY=
```
4. Run the frontend.
```bash
npm run dev
```
5. Run the backend.
```bash
npm run start
```
6. Quickly unlock command usage - use the help command in the command-line to query the usage.
```bash
# Query all command help
help
# Query specific command help
gpt -h
gpt chat -h
gpt role -h
gpt history -h
```
## How to Design Your Own GPT Role?
Use the following command to start the role `DIY` process
```bash
# k - Role unique identifier, e.g., default / cli / sql / ikun
# n - Role name, e.g., Command-line Translation Assistant, SQL-BOY
# d - Role description, e.g., Translate your natural language instructions into Windows/Unix terminal commands
gpt diy <-k GPT role unique identifier> <-n GPT role name> <-d GPT role description>
```
## Special Thanks
This project was inspired by [YuIndex](https://github.com/liyupi/yuindex)and eventually transformed it into "GPT Terminal".
- [@程序员鱼皮](https://github.com/liyupi)
- [@MagicCube](https://github.com/MagicCube)
- [@Overtrue](https://github.com/Overtrue)
## Open Source License
Apache License Version 2.0 see http://www.apache.org/licenses/LICENSE

@ -0,0 +1,184 @@
<p align="center">
<img src="https://lty-image-bed.oss-cn-shenzhen.aliyuncs.com/blog/GPT.png" width="200" height="200">
</p>
<div align="center">
# GPT Terminal
<!-- prettier-ignore-start -->
<!-- markdownlint-disable-next-line MD036 -->
_✨ 用程序员的方式打开GPT定制专属于你的 GPT 终端 ✨_
<!-- prettier-ignore-end -->
<p align="center">
<img src="https://img.shields.io/github/v/release/ltyzzzxxx/gpt-web-terminal?display_name=tag" />
<img src="https://img.shields.io/github/stars/ltyzzzxxx/gpt-web-terminal" />
<img src="https://img.shields.io/github/forks/ltyzzzxxx/gpt-web-terminal" />
<img src="https://img.shields.io/github/issues/ltyzzzxxx/gpt-web-terminal" />
<img src="https://img.shields.io/badge/license-Apache%20-yellow.svg" />
</p>
</div>
[English Doc](./README_EN.md) | [中文文档](./README_CN.md)
## 简介
GPT Terminal 是一个让你在终端上与 GPT 进行自由对话的平台。
在这里,你可以更加轻易地实现更多定制化的功能,拥有专属于你的 GPT 终端!
项目基于 Vue3 与 Express 实现
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/2S2-Da)
## 预览
演示地址https://gpt-terminal.up.railway.app/(使用前配置 🪜)
B站功能细节展示https://www.bilibili.com/video/BV1Ux4y1o7nu/?spm_id_from=333.999.0.0&vd_source=4e7654633e4719c03a8fb6c6b524ddc2
![gpt-terminal-demo](https://github.com/ltyzzzxxx/gpt-web-terminal/assets/73587471/dee28750-b86b-45a1-a8b2-8357b3e27a25)
## 专栏讲解(持续更新中)
> [耗时一下午,我实现了 GPT Terminal真正拥有了专属于我的 GPT 终端!](https://juejin.cn/post/7243252896392151097)
>
> [如何用 GPT 在 5 分钟内 ”调教“ 出一个专属于你的 ”小黑子“?](https://juejin.cn/post/7244174817679573047)
>
> [如何丝滑实现 GPT 打字机流式回复Server-Sent Events](https://juejin.cn/post/7244604894408933432)
>
> [我是如何让我的 GPT Terminal “长记性” 的?还是老配方!](https://juejin.cn/post/7245812754027823160)
>
> [一个合格的类 GPT 应用需要具备什么?一文带你打通 GPT 产品功能!](https://juejin.cn/post/7246435689419604026)
>
> [开发一个 ChatGPT 真的只是当 "接口侠" 吗GPT Terminal 细节分享!](https://juejin.cn/post/7246917539766091837)
>
> [如何借助于 OpenAI 以命令的方式在 GPT 终端上画一只 “坤”?](https://juejin.cn/post/7247167843498115130)
>
> [不满足当 ChatGPT “接口侠”?轻松可视化 Fine-tuning 训练你的模型!](https://juejin.cn/post/7247906556229828645)
>
> [耗时一下午,我终于上线了我的 GPT 终端!(内含详细部署方案记录)](https://juejin.cn/post/7250639505527504933)
## 功能概览
- ✨ 支持命令行终端与 GPT 进行对话,我们程序员就是要用极客范儿的方式与 GPT 交流!
- 🌟 支持 API KeyOpenAI API 方式)与 Access TokenWeb API 方式)配置 - 二选一
- 🌈 支持 DIY GPT 角色,并持久化。与你的专属角色进行对话!
- ☁️ 支持 GPT 记忆历史对话并提供命令查询历史对话,给你的 GPT 长长记性~
- 🍀 支持 GPT 市面使用最广泛的 Event Stream 技术,实现打字机效果~
- 🌴 支持 GPT 回复内容以 Markdown 形式展现
- 🍃 基本的终端命令,如查看历史命令、帮助手册、清屏等
## 快速开始
你只需简单地了解如何使用 `npm` ,即可解锁全部功能!
1. 将项目克隆到本地
```bash
git clone https://github.com/ltyzzzxxx/gpt-web-terminal.git
```
2. 进入项目目录,并分别安装前端与后端依赖
```bash
cd gpt-web-terminal && npm install
cd service && npm install
```
3. 在 `service/.env` 中,配置 API Key 或 Access Token
```
# API KeyOpenAI API 方式) 与 Access TokenWeb API 方式) 二选一
OPENAI_API_KEY=
OPENAI_ACCESS_TOKEN=
# 使用 Access Token 时可配置反向代理地址
API_REVERSE_PROXY=
```
4. 运行前端
```bash
npm run dev
```
5. 运行后端
```bash
npm run start
```
6. 快速解锁命令用法 - 命令行中使用help命令查询使用方法
```bash
# 查询全部命令帮助
help
# 查询具体命令帮助
gpt -h
gpt chat -h
gpt role -h
gpt history -h
```
## GPT 网络与配置问题检测
通过 `GPT Demo`,检测你能否顺利请求到 `Open AI`,确保你网络通畅 且 `API Key` 可用
1. 执行如下命令,进入 `gpt-test-demo` 文件夹,并安装依赖
```bash
cd gpt-test-demo && npm install
```
2. 在 index.js 文件中配置你的 `API Key`
```js
const configuration = new Configuration({
apiKey: "",
});
```
3. 运行 index.js 文件
```bash
node index.js
```
若顺利输出内容,则说明 `API Key` 有效且网络可访问。
<img width="1017" alt="image" src="https://github.com/ltyzzzxxx/gpt-web-terminal/assets/73587471/40703a2e-a294-40a8-bde7-52bd6882fb48">
## 如何"调教"属于你的 GPT 角色?
输入如下命令进入角色 `DIY` 流程
```bash
# k - 角色唯一标识,例如: default / cli / sql / ikun
# n - 角色名例如命令行翻译助手、SQL-BOY
# d - 角色描述,例如:将你的自然语言指令翻译为 Window/Unix 终端命令
gpt diy <-k GPT > <-n GPT > <-d GPT >
```
## 特别鸣谢
该项目灵感来源于 [YuIndex](https://github.com/liyupi/yuindex),并最终将其改造为 「GPT Terminal」
- [@程序员鱼皮](https://github.com/liyupi)
- [@MagicCube](https://github.com/MagicCube)
- [@Overtrue](https://github.com/Overtrue)
## 开源协议
Apache License Version 2.0 see http://www.apache.org/licenses/LICENSE

@ -0,0 +1,133 @@
<p align="center">
<img src="https://lty-image-bed.oss-cn-shenzhen.aliyuncs.com/blog/GPT.png" width="200" height="200">
</p>
<div align="center">
# GPT Terminal
<!-- prettier-ignore-start -->
<!-- markdownlint-disable-next-line MD036 -->
_✨ Open GPT like a programmer! Customize your own GPT terminal. ✨_
<!-- prettier-ignore-end -->
<p align="center">
<img src="https://img.shields.io/github/v/release/ltyzzzxxx/gpt-web-terminal?display_name=tag" />
<img src="https://img.shields.io/github/stars/ltyzzzxxx/gpt-web-terminal" />
<img src="https://img.shields.io/github/forks/ltyzzzxxx/gpt-web-terminal" />
<img src="https://img.shields.io/github/issues/ltyzzzxxx/gpt-web-terminal" />
<img src="https://img.shields.io/badge/license-Apache%20-yellow.svg" />
</p>
</div>
[English Doc](./README_EN.md) | [中文文档](./README_CN.md)
## Introduction
GPT Terminal is a platform that allows you to have free conversations with GPT in the terminal.
Here, you can easily implement more customized functionalities and have your own GPT terminal!
The project is implemented based on Vue3 and Express.
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/2S2-Da)
## Preview
Demo Linkhttps://gpt-terminal.up.railway.app/
![gpt-terminal-demo](https://github.com/ltyzzzxxx/gpt-web-terminal/assets/73587471/dee28750-b86b-45a1-a8b2-8357b3e27a25)
## Features
- ✨ Support dialogue between command-line terminal and GPT. As programmers, we communicate with GPT in a geeky way!
- 🌟 Support configuration of API Key (OpenAI API method) and Access Token (Web API method) - either one can be chosen.
- 🌈 Support DIY of GPT roles and persist them. Have conversations with your exclusive role!
- ☁️ Support GPT to remember the conversation history and provide commands to query the history. Give your GPT a good memory~
- 🍀 Support the widely-used Event Stream technology for GPT responses, achieving a typewriter effect~
- 🌴 Support rendering GPT responses in Markdown format.
- 🍃 Basic terminal commands such as viewing command history, accessing help manuals, clearing the screen, etc.
## Quick Start
You only need to have a basic understanding of using `npm` to unlock all the features!
1. Clone the project to your local machine.
```bash
git clone https://github.com/ltyzzzxxx/gpt-web-terminal.git
```
2. Navigate to the project directory and install dependencies for the frontend and backend separately.
```bash
cd gpt-web-terminal && npm install
cd service && npm install
```
3. Configure your API Key or Access Token in `service/.env`. API key takes priority.
```
# Choose either API Key (OpenAI API method) or Access Token (Web API method)
OPENAI_API_KEY=
OPENAI_ACCESS_TOKEN=
# Configure reverse proxy address when using Access Token
API_REVERSE_PROXY=
```
4. Run the frontend.
```bash
npm run dev
```
5. Run the backend.
```bash
npm run start
```
6. Quickly unlock command usage - use the help command in the command-line to query the usage.
```bash
# Query all command help
help
# Query specific command help
gpt -h
gpt chat -h
gpt role -h
gpt history -h
```
## How to Design Your Own GPT Role?
Use the following command to start the role `DIY` process
```bash
# k - Role unique identifier, e.g., default / cli / sql / ikun
# n - Role name, e.g., Command-line Translation Assistant, SQL-BOY
# d - Role description, e.g., Translate your natural language instructions into Windows/Unix terminal commands
gpt diy <-k GPT role unique identifier> <-n GPT role name> <-d GPT role description>
```
## Special Thanks
This project was inspired by [YuIndex](https://github.com/liyupi/yuindex)and eventually transformed it into "GPT Terminal".
- [@程序员鱼皮](https://github.com/liyupi)
- [@MagicCube](https://github.com/MagicCube)
- [@Overtrue](https://github.com/Overtrue)
## Open Source License
Apache License Version 2.0 see http://www.apache.org/licenses/LICENSE

@ -0,0 +1,21 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
ACol: typeof import('ant-design-vue/es')['Col']
ACollapse: typeof import('ant-design-vue/es')['Collapse']
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
AInput: typeof import('ant-design-vue/es')['Input']
ARow: typeof import('ant-design-vue/es')['Row']
ATag: typeof import('ant-design-vue/es')['Tag']
ContentOutput: typeof import('./src/components/gpt-terminal/ContentOutput.vue')['default']
GptTerminal: typeof import('./src/components/gpt-terminal/GptTerminal.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

@ -0,0 +1,28 @@
const { Configuration, OpenAIApi } = require("openai");
const configuration = new Configuration({
apiKey: "",
});
const openai = new OpenAIApi(configuration);
async function createChatCompletion(messages) {
const response = await openai.createChatCompletion({
model: "gpt-3.5-turbo",
messages,
});
if (response.data.choices.length) {
const firstMessage = response.data.choices[0].message;
if (firstMessage) {
return firstMessage.content;
}
}
throw new Error("Failed to get response from OpenAI service.");
}
createChatCompletion([{ role: "user", content: "Hello world" }])
.then(result => {
console.log("result:", result);
})
.catch(error => {
console.error(error);
});

@ -0,0 +1,125 @@
{
"name": "gpt-test",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gpt-test",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"axios": "^1.4.0",
"openai": "^3.2.1"
},
"devDependencies": {}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/openai": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz",
"integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==",
"dependencies": {
"axios": "^0.26.0",
"form-data": "^4.0.0"
}
},
"node_modules/openai/node_modules/axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"dependencies": {
"follow-redirects": "^1.14.8"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
}
}
}

@ -0,0 +1,15 @@
{
"name": "gpt-test",
"version": "1.0.0",
"main": "index.js",
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.4.0",
"openai": "^3.2.1"
},
"scripts": {
"start": "node index.js"
},
"description": ""
}

@ -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>GPT 终端 | 程序员方式打开 GPT | By 周三不Coding</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,59 @@
{
"name": "gpt-web-terminal",
"description": "专属于你的GPT终端",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"dev:crx": "cross-env BUILD_CRX=1 vite build --watch",
"build": "vue-tsc --noEmit && vite build",
"build:crx": "cross-env BUILD_CRX=1 vite build",
"preview": "vite preview",
"tsc": "vue-tsc --noEmit"
},
"dependencies": {
"ant-design-vue": "^3.2.10",
"axios": "^1.3.4",
"dayjs": "^1.11.3",
"getopts": "^2.3.0",
"highlight.js": "^11.8.0",
"hljs": "^6.2.3",
"lodash": "^4.17.21",
"marked": "^5.1.0",
"openai": "^3.2.1",
"pinia": "^2.0.14",
"pinia-plugin-persistedstate": "^1.6.1",
"vue": "^3.2.25",
"vue-router": "4",
"xterm": "^4.19.0",
"xterm-addon-fit": "^0.5.0",
"xterm-addon-web-links": "^0.6.0"
},
"devDependencies": {
"@types/lodash": "^4.14.182",
"@types/marked": "^5.0.0",
"@types/node": "^18.6.4",
"@typescript-eslint/eslint-plugin": "^5.23.0",
"@typescript-eslint/parser": "^5.23.0",
"@vitejs/plugin-vue": "^4.0.0",
"consola": "^2.15.3",
"cross-env": "^7.0.3",
"crx": "^5.0.1",
"eslint": "^8.15.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.2.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^8.7.1",
"npm-run-all": "^4.1.5",
"ora": "5",
"prettier": "^2.7.1",
"typescript": "^4.6.4",
"unplugin-vue-components": "^0.21.1",
"vite": "^4.2.0",
"vite-plugin-pwa": "^0.14.4",
"vue-tsc": "^0.38.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

@ -0,0 +1,45 @@
# OpenAI API Key - https://platform.openai.com/overview
OPENAI_API_KEY=model_credential.get('api_key'),
**optional_params
# change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response
OPENAI_ACCESS_TOKEN=
# OpenAI API Base URL - https://api.openai.com
OPENAI_API_BASE_URL=
# OpenAI API Model - https://platform.openai.com/docs/models
OPENAI_API_MODEL=
# set `true` to disable OpenAI API debug log
OPENAI_API_DISABLE_DEBUG=
# Reverse Proxy - Available on accessToken
# Default: https://ai.fakeopen.com/api/conversation
# More: https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy
API_REVERSE_PROXY=
# timeout
TIMEOUT_MS=100000
# Rate Limit
MAX_REQUEST_PER_HOUR=
# Secret key
AUTH_SECRET_KEY=
# Socks Proxy Host
SOCKS_PROXY_HOST=
# Socks Proxy Port
SOCKS_PROXY_PORT=
# Socks Proxy Username
SOCKS_PROXY_USERNAME=
# Socks Proxy Password
SOCKS_PROXY_PASSWORD=
# HTTPS PROXY
HTTPS_PROXY=

@ -0,0 +1,5 @@
{
"root": true,
"ignorePatterns": ["build"],
"extends": ["@antfu"]
}

@ -0,0 +1,31 @@
# 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/settings.json
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
build

@ -0,0 +1 @@
enable-pre-post-scripts=true

@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

@ -0,0 +1,22 @@
{
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [
"javascript",
"typescript",
"json",
"jsonc",
"json5",
"yaml"
],
"cSpell.words": [
"antfu",
"chatgpt",
"esno",
"GPTAPI",
"OPENAI"
]
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,47 @@
{
"name": "chatgpt-web-service",
"version": "1.0.0",
"private": false,
"description": "ChatGPT Web Service",
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
"keywords": [
"chatgpt-web",
"chatgpt",
"chatbot",
"express"
],
"engines": {
"node": "^16 || ^18 || ^19"
},
"scripts": {
"start": "esno ./src/index.ts",
"dev": "esno watch ./src/index.ts",
"prod": "node ./build/index.mjs",
"build": "pnpm clean && tsup",
"clean": "rimraf build",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml"
},
"dependencies": {
"axios": "^1.3.4",
"chatgpt": "^5.1.2",
"dotenv": "^16.0.3",
"esno": "^0.16.3",
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"https-proxy-agent": "^5.0.1",
"isomorphic-fetch": "^3.0.0",
"node-fetch": "^3.3.0",
"socks-proxy-agent": "^7.0.0"
},
"devDependencies": {
"@antfu/eslint-config": "^0.35.3",
"@types/express": "^4.17.17",
"@types/node": "^18.14.6",
"eslint": "^8.35.0",
"rimraf": "^4.3.0",
"tsup": "^6.6.3",
"typescript": "^4.9.5"
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,226 @@
import * as dotenv from 'dotenv'
import 'isomorphic-fetch'
import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt'
import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt'
import { SocksProxyAgent } from 'socks-proxy-agent'
import httpsProxyAgent from 'https-proxy-agent'
import fetch from 'node-fetch'
import { sendResponse } from '../utils'
import { isNotEmptyString } from '../utils/is'
import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
import type { RequestOptions, SetProxyOptions, UsageResponse } from './types'
const { HttpsProxyAgent } = httpsProxyAgent
dotenv.config()
const ErrorCodeMessage: Record<string, string> = {
401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided',
403: '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later',
502: '[OpenAI] 错误的网关 | Bad Gateway',
503: '[OpenAI] 服务器繁忙,请稍后再试 | Server is busy, please try again later',
504: '[OpenAI] 网关超时 | Gateway Time-out',
500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error',
}
const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 100 * 1000
const disableDebug: boolean = process.env.OPENAI_API_DISABLE_DEBUG === 'true'
let apiModel: ApiModel
const model = isNotEmptyString(process.env.OPENAI_API_MODEL) ? process.env.OPENAI_API_MODEL : 'gpt-3.5-turbo'
if (!isNotEmptyString(process.env.OPENAI_API_KEY) && !isNotEmptyString(process.env.OPENAI_ACCESS_TOKEN))
throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable')
let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
(async () => {
// More Info: https://github.com/transitive-bullshit/chatgpt-api
if (isNotEmptyString(process.env.OPENAI_API_KEY)) {
const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
const options: ChatGPTAPIOptions = {
apiKey: process.env.OPENAI_API_KEY,
completionParams: { model },
debug: !disableDebug,
}
// increase max token limit if use gpt-4
if (model.toLowerCase().includes('gpt-4')) {
// if use 32k model
if (model.toLowerCase().includes('32k')) {
options.maxModelTokens = 32768
options.maxResponseTokens = 8192
}
else {
options.maxModelTokens = 8192
options.maxResponseTokens = 2048
}
}
else if (model.toLowerCase().includes('gpt-3.5')) {
if (model.toLowerCase().includes('16k')) {
options.maxModelTokens = 16384
options.maxResponseTokens = 4096
}
}
if (isNotEmptyString(OPENAI_API_BASE_URL))
options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1`
setupProxy(options)
api = new ChatGPTAPI({ ...options })
apiModel = 'ChatGPTAPI'
}
else {
const options: ChatGPTUnofficialProxyAPIOptions = {
accessToken: process.env.OPENAI_ACCESS_TOKEN,
apiReverseProxyUrl: isNotEmptyString(process.env.API_REVERSE_PROXY) ? process.env.API_REVERSE_PROXY : 'https://ai.fakeopen.com/api/conversation',
model,
debug: !disableDebug,
}
setupProxy(options)
api = new ChatGPTUnofficialProxyAPI({ ...options })
apiModel = 'ChatGPTUnofficialProxyAPI'
}
})()
async function chatReplyProcess(options: RequestOptions) {
const { message, lastContext, process, systemMessage, temperature, top_p } = options
try {
let options: SendMessageOptions = { timeoutMs }
globalThis.console.log(systemMessage)
if (apiModel === 'ChatGPTAPI') {
if (isNotEmptyString(systemMessage)) {
options.systemMessage = systemMessage
globalThis.console.log(systemMessage)
}
options.completionParams = { model, temperature, top_p }
}
if (lastContext != null) {
if (apiModel === 'ChatGPTAPI')
options.parentMessageId = lastContext.parentMessageId
else
options = { ...lastContext }
}
const response = await api.sendMessage(message, {
...options,
onProgress: (partialResponse) => {
process?.(partialResponse)
},
})
return sendResponse({ type: 'Success', data: response })
}
catch (error: any) {
const code = error.statusCode
global.console.log(error)
if (Reflect.has(ErrorCodeMessage, code))
return sendResponse({ type: 'Fail', message: ErrorCodeMessage[code] })
return sendResponse({ type: 'Fail', message: error.message ?? 'Please check the back-end console' })
}
}
async function fetchUsage() {
const OPENAI_API_KEY = process.env.OPENAI_API_KEY
const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
if (!isNotEmptyString(OPENAI_API_KEY))
return Promise.resolve('-')
const API_BASE_URL = isNotEmptyString(OPENAI_API_BASE_URL)
? OPENAI_API_BASE_URL
: 'https://api.openai.com'
const [startDate, endDate] = formatDate()
// 每月使用量
const urlUsage = `${API_BASE_URL}/v1/dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`
const headers = {
'Authorization': `Bearer ${OPENAI_API_KEY}`,
'Content-Type': 'application/json',
}
const options = {} as SetProxyOptions
setupProxy(options)
try {
// 获取已使用量
const useResponse = await options.fetch(urlUsage, { headers })
if (!useResponse.ok)
throw new Error('获取使用量失败')
const usageData = await useResponse.json() as UsageResponse
const usage = Math.round(usageData.total_usage) / 100
return Promise.resolve(usage ? `$${usage}` : '-')
}
catch (error) {
global.console.log(error)
return Promise.resolve('-')
}
}
function formatDate(): string[] {
const today = new Date()
const year = today.getFullYear()
const month = today.getMonth() + 1
const lastDay = new Date(year, month, 0)
const formattedFirstDay = `${year}-${month.toString().padStart(2, '0')}-01`
const formattedLastDay = `${year}-${month.toString().padStart(2, '0')}-${lastDay.getDate().toString().padStart(2, '0')}`
return [formattedFirstDay, formattedLastDay]
}
async function chatConfig() {
const usage = await fetchUsage()
const reverseProxy = process.env.API_REVERSE_PROXY ?? '-'
const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-'
const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT)
? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`)
: '-'
return sendResponse<ModelConfig>({
type: 'Success',
data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, usage },
})
}
function setupProxy(options: SetProxyOptions) {
if (isNotEmptyString(process.env.SOCKS_PROXY_HOST) && isNotEmptyString(process.env.SOCKS_PROXY_PORT)) {
const agent = new SocksProxyAgent({
hostname: process.env.SOCKS_PROXY_HOST,
port: process.env.SOCKS_PROXY_PORT,
userId: isNotEmptyString(process.env.SOCKS_PROXY_USERNAME) ? process.env.SOCKS_PROXY_USERNAME : undefined,
password: isNotEmptyString(process.env.SOCKS_PROXY_PASSWORD) ? process.env.SOCKS_PROXY_PASSWORD : undefined,
})
options.fetch = (url, options) => {
return fetch(url, { agent, ...options })
}
}
else if (isNotEmptyString(process.env.HTTPS_PROXY) || isNotEmptyString(process.env.ALL_PROXY)) {
const httpsProxy = process.env.HTTPS_PROXY || process.env.ALL_PROXY
if (httpsProxy) {
const agent = new HttpsProxyAgent(httpsProxy)
options.fetch = (url, options) => {
return fetch(url, { agent, ...options })
}
}
}
else {
options.fetch = (url, options) => {
return fetch(url, { ...options })
}
}
}
function currentModel(): ApiModel {
return apiModel
}
export type { ChatContext, ChatMessage }
export { chatReplyProcess, chatConfig, currentModel }

@ -0,0 +1,23 @@
import type { ChatMessage } from 'chatgpt'
import type fetch from 'node-fetch'
export interface RequestOptions {
message: string
lastContext?: { conversationId?: string; parentMessageId?: string }
process?: (chat: ChatMessage) => void
systemMessage?: string
temperature?: number
top_p?: number
}
export interface ImageRequestOptions {
}
export interface SetProxyOptions {
fetch?: typeof fetch
}
export interface UsageResponse {
total_usage: number
}

@ -0,0 +1,61 @@
import express from 'express'
import type { RequestProps } from './types'
import type { ChatMessage } from './chatgpt'
import { chatConfig, chatReplyProcess, currentModel } from './chatgpt'
import { limiter } from './middleware/limiter'
import { isNotEmptyString } from './utils/is'
const app = express()
const router = express.Router()
app.use(express.static('public'))
app.use(express.json())
app.all('*', (_, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'authorization, Content-Type')
res.header('Access-Control-Allow-Methods', '*')
next()
})
router.post('/chat-process', [limiter], async (req, res) => {
res.setHeader('Content-type', 'application/octet-stream')
try {
const { prompt, options = {}, systemMessage, temperature, top_p } = req.body as RequestProps
let firstChunk = true
await chatReplyProcess({
message: prompt,
lastContext: options,
process: (chat: ChatMessage) => {
res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`)
firstChunk = false
},
systemMessage,
temperature,
top_p,
})
}
catch (error) {
res.write(JSON.stringify(error))
}
finally {
res.end()
}
})
router.post('/config', async (req, res) => {
try {
const response = await chatConfig()
res.send(response)
}
catch (error) {
res.send(error)
}
})
app.use('', router)
app.use('/api', router)
app.set('trust proxy', 1)
app.listen(3002, () => globalThis.console.log('Server is running on port 3002'))

@ -0,0 +1,19 @@
import { rateLimit } from 'express-rate-limit'
import { isNotEmptyString } from '../utils/is'
const MAX_REQUEST_PER_HOUR = process.env.MAX_REQUEST_PER_HOUR
const maxCount = (isNotEmptyString(MAX_REQUEST_PER_HOUR) && !isNaN(Number(MAX_REQUEST_PER_HOUR)))
? parseInt(MAX_REQUEST_PER_HOUR)
: 0 // 0 means unlimited
const limiter = rateLimit({
windowMs: 60 * 60 * 1000, // Maximum number of accesses within an hour
max: maxCount,
statusCode: 200, // 200 means successbut the message is 'Too many request from this IP in 1 hour'
message: async (req, res) => {
res.send({ status: 'Fail', message: 'Too many request from this IP in 1 hour', data: null })
},
})
export { limiter }

@ -0,0 +1,34 @@
import type { FetchFn } from 'chatgpt'
export interface RequestProps {
prompt: string
options?: ChatContext
systemMessage: string
temperature?: number
top_p?: number
}
export interface ChatContext {
conversationId?: string
parentMessageId?: string
}
export interface ChatGPTUnofficialProxyAPIOptions {
accessToken: string
apiReverseProxyUrl?: string
model?: string
debug?: boolean
headers?: Record<string, string>
fetch?: FetchFn
}
export interface ModelConfig {
apiModel?: ApiModel
reverseProxy?: string
timeoutMs?: number
socksProxy?: string
httpsProxy?: string
usage?: string
}
export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined

@ -0,0 +1,22 @@
interface SendResponseOptions<T = any> {
type: 'Success' | 'Fail'
message?: string
data?: T
}
export function sendResponse<T>(options: SendResponseOptions<T>) {
if (options.type === 'Success') {
return Promise.resolve({
message: options.message ?? null,
data: options.data ?? null,
status: options.type,
})
}
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject({
message: options.message ?? 'Failed',
data: options.data ?? null,
status: options.type,
})
}

@ -0,0 +1,19 @@
export function isNumber<T extends number>(value: T | unknown): value is number {
return Object.prototype.toString.call(value) === '[object Number]'
}
export function isString<T extends string>(value: T | unknown): value is string {
return Object.prototype.toString.call(value) === '[object String]'
}
export function isNotEmptyString(value: any): boolean {
return typeof value === 'string' && value.length > 0
}
export function isBoolean<T extends boolean>(value: T | unknown): value is boolean {
return Object.prototype.toString.call(value) === '[object Boolean]'
}
export function isFunction<T extends (...args: any[]) => any | void | never>(value: T | unknown): value is T {
return Object.prototype.toString.call(value) === '[object Function]'
}

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es2020",
"lib": [
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"baseUrl": ".",
"outDir": "build",
"noEmit": true
},
"exclude": [
"node_modules",
"build"
],
"include": [
"**/*.ts"
]
}

@ -0,0 +1,13 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
outDir: 'build',
target: 'es2020',
format: ['esm'],
splitting: false,
sourcemap: true,
minify: false,
shims: true,
dts: false,
})

@ -0,0 +1,11 @@
<template>
<router-view />
</template>
<script setup lang="ts"></script>
<style>
body {
margin: 0;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

@ -0,0 +1,76 @@
<template>
<div class="content-output">
<template v-if="output.type === 'text'">
<a-tag v-if="outputTagColor" :color="outputTagColor"
>{{ output.status }}
</a-tag>
<span v-if="output.type === 'text'" v-html="smartText(output.text)" />
</template>
<component
@start="handleStart"
@finish="handleFinish"
@failed="handleFailed"
:is="output.component"
v-if="output.type === 'component'"
v-bind="output.props ?? {}"
/>
</div>
</template>
<script setup lang="ts">
import smartText from "../../utils/smartText";
import OutputType = GptTerminal.OutputType;
import { computed, toRefs, defineEmits } from "vue";
interface OutputProps {
output: OutputType;
}
const props = defineProps<OutputProps>();
const { output } = toRefs(props);
const outputTagColor = computed((): string => {
if (!output.value.status) {
return "";
}
switch (output.value.status) {
case "info":
return "dodgerblue";
case "success":
return "limegreen";
case "warning":
return "darkorange";
case "error":
return "#c0300f";
case "system":
return "#bfc4c9";
case "loading":
return "#ead029"
default:
return "";
}
});
const emit = defineEmits(['start', 'finish', 'failed'])
// gpt
const handleStart = () => {
emit('start')
}
// gpt
const handleFinish = () => {
emit('finish')
}
const handleFailed = () => {
emit('failed')
}
</script>
<style scoped>
.content-output :deep(.ant-tag) {
border-radius: 0;
font-size: 16px;
border: none;
}
</style>

@ -0,0 +1,551 @@
<template>
<div class="yu-terminal-wrapper" :style="wrapperStyle" @click="handleClickWrapper">
<div ref="terminalRef" class="yu-terminal" :style="mainStyle">
<a-collapse v-model:activeKey="activeKeys" :bordered="false" expand-icon-position="right">
<template v-for="(output, index) in outputList" :key="index">
<!-- 折叠 -->
<a-collapse-panel v-if="output.collapsible" :key="index" class="terminal-row">
<template #header>
<span style="user-select: none; margin-right: 10px">
{{ prompt }}
</span>
<span>{{ output.text }}</span>
</template>
<div v-for="(result, idx) in output.resultList" :key="idx" class="terminal-row">
<content-output :output="result" />
</div>
</a-collapse-panel>
<!-- 不折叠 -->
<template v-else>
<!-- 输出命令及结果-->
<template v-if="output.type === 'command'">
<div class="terminal-row">
<span style="user-select: none; margin-right: 10px">{{
prompt
}}</span>
<span>{{ output.text }}</span>
</div>
<div v-for="(result, idx) in output?.resultList" :key="idx" class="terminal-row">
<content-output @start="handleStart" @finish="handleFinish" @failed="handleFailed" :output="result" />
</div>
</template>
<!-- 打印信息 -->
<template v-else>
<div class="terminal-row">
<content-output :output="output" />
</div>
</template>
</template>
</template>
</a-collapse>
<div class="terminal-row">
<a-input ref="commandInputRef" v-model:value="inputCommand.text" :disabled="isRunning" class="command-input"
:placeholder="inputCommand.placeholder" :bordered="false" autofocus @press-enter="doSubmitCommand">
<template #addonBefore>
<span class="command-input-prompt">{{ prompt }}</span>
</template>
</a-input>
</div>
<!-- 输入提示-->
<div v-if="hint && !isRunning" class="terminal-row" style="color: #bbb">
hint{{ hint }}
</div>
<div style="margin-bottom: 16px" />
</div>
</div>
</template>
<script setup lang="ts">
import {
computed,
onMounted,
Ref,
ref,
StyleValue,
toRefs,
watchEffect,
} from "vue";
import CommandOutputType = GptTerminal.CommandOutputType;
import OutputType = GptTerminal.OutputType;
import CommandInputType = GptTerminal.CommandInputType;
import { registerShortcuts } from "./shortcuts";
import TerminalType = GptTerminal.TerminalType;
import TextOutputType = GptTerminal.TextOutputType;
import useHistory from "./history";
import ContentOutput from "./ContentOutput.vue";
import OutputStatusType = GptTerminal.OutputStatusType;
import useHint from "./hint";
import { defineStore } from "pinia";
import { useMessagesStore } from "../../core/commands/gpt/messagesStore";
const messagesStore = useMessagesStore();
const messages = messagesStore.$state.messages;
interface GptTerminalProps {
height?: string | number;
fullScreen?: boolean;
// eslint-disable-next-line vue/require-default-prop
onSubmitCommand?: (inputText: string) => void;
}
const props = withDefaults(defineProps<GptTerminalProps>(), {
height: "400px",
fullScreen: false,
});
const terminalRef = ref();
const activeKeys = ref<number[]>([]);
//
const outputList = ref<OutputType[]>([]);
//
const commandList = ref<CommandOutputType[]>([]);
const commandInputRef = ref();
//
const isRunning = ref(false);
/**
* 初始命令
*/
const initCommand: CommandInputType = {
text: "",
placeholder: "",
};
/**
* 待输入的命令
*/
const inputCommand = ref<CommandInputType>({
...initCommand,
});
/**
* 全局记录当前命令便于写入结果
*/
let currentNewCommand: CommandOutputType;
const {
commandHistoryPos,
showPrevCommand,
showNextCommand,
listCommandHistory,
} = useHistory(commandList.value, inputCommand);
const { hint, setHint, debounceSetHint } = useHint();
/**
* 提交命令回车
*/
const doSubmitCommand = async () => {
isRunning.value = true;
terminal.isRunning = true;
setHint("");
let inputText = inputCommand.value.text;
//
if (inputText.startsWith("!")) {
const commandIndex = Number(inputText.substring(1));
const command = commandList.value[commandIndex - 1];
if (command) {
inputText = command.text;
}
}
//
const newCommand: CommandOutputType = {
text: inputText,
type: "command",
resultList: [],
};
// 便
currentNewCommand = newCommand;
//
await props.onSubmitCommand?.(inputText);
//
outputList.value.push(newCommand);
//
if (inputText) {
commandList.value.push(newCommand);
//
commandHistoryPos.value = commandList.value.length;
}
inputCommand.value = { ...initCommand };
//
activeKeys.value.push(outputList.value.length - 1);
//
setTimeout(() => {
terminalRef.value.scrollTop = terminalRef.value.scrollHeight;
}, 50);
isRunning.value = false;
terminal.isRunning = false;
};
// GPT
const handleStart = () => {
console.log("开始")
isRunning.value = true
terminal.isRunning = true
}
// GPT
const handleFinish = () => {
console.log("结束")
isRunning.value = false
terminal.isRunning = false
}
const handleFailed = () => {
console.log("失败")
}
//
watchEffect(() => {
debounceSetHint(inputCommand.value.text);
});
/**
* 输入提示符
*/
const prompt = computed(() => {
return '[local]$';
});
/**
* 命令样式
*/
const commandInputClass = () => {
const classNames = ['command-input'];
if (inputCommand.value.text.includes(' ')) {
classNames.push('white-background-text');
}
return classNames.join(' ');
}
/**
* 终端主样式
*/
const mainStyle = computed(() => {
const fullScreenStyle: StyleValue = {
position: "fixed",
top: 0,
bottom: 0,
left: 0,
right: 0,
};
return props.fullScreen
? fullScreenStyle
: {
height: props.height,
};
});
/**
* 终端包装类主样式
*/
const wrapperStyle = computed(() => {
const style = {
...mainStyle.value,
};
return style;
});
/**
* 清空所有输出
*/
const clear = () => {
outputList.value = [];
};
/**
* 写命令文本结果
* @param text
* @param status
*/
const writeTextResult = (text: string, status?: OutputStatusType) => {
const newOutput: TextOutputType = {
text,
type: "text",
status,
};
currentNewCommand.resultList.push(newOutput);
};
/**
* 写文本错误状态结果
* @param text
*/
const writeTextErrorResult = (text: string) => {
writeTextResult(text, "error");
};
/**
* 写文本等待状态
* @param text
*/
const writeTextLoadingResult = (text: string) => {
writeTextResult(text, "loading");
}
/**
* 写文本成功状态结果
* @param text
*/
const writeTextSuccessResult = (text: string) => {
writeTextResult(text, "success");
};
/**
* 写结果
* @param output
*/
const writeResult = (output: OutputType) => {
currentNewCommand.resultList.push(output);
};
/**
* 立即输出文本
* @param text
* @param status
*/
const writeTextOutput = (text: string, status?: OutputStatusType) => {
const newOutput: TextOutputType = {
text,
type: "text",
status,
};
outputList.value.push(newOutput);
};
/**
* 移除 outputList 最后一个元素
*/
const removeLastOutput = () => {
console.log("移除前 - outputList", currentNewCommand.resultList)
currentNewCommand.resultList.pop();
console.log("移除后 - outputList", currentNewCommand.resultList)
}
/**
* 设置命令是否可折叠
* @param collapsible
*/
const setCommandCollapsible = (collapsible: boolean) => {
currentNewCommand.collapsible = collapsible;
};
/**
* 立即输出
* @param newOutput
*/
const writeOutput = (newOutput: OutputType) => {
outputList.value.push(newOutput);
};
/**
* 输入框聚焦
*/
const focusInput = () => {
commandInputRef.value.focus();
};
/**
* 获取输入框是否聚焦
*/
const isInputFocused = () => {
return (
(commandInputRef.value.input as HTMLInputElement) == document.activeElement
);
};
/**
* 设置输入框的值
*/
const setTabCompletion = () => {
if (hint.value) {
inputCommand.value.text = `${hint.value.split(" ")[0]}${hint.value.split(" ").length > 1 ? " " : ""
}`;
}
};
/**
* 折叠 / 展开所有块
*/
const toggleAllCollapse = () => {
//
if (activeKeys.value.length === 0) {
activeKeys.value = outputList.value.map((_, index) => {
return index;
});
} else {
//
activeKeys.value = [];
}
};
/**
* GPT 历史对话记录
*/
const listGptHistory = () => {
return messages
}
/**
* 终止命令运行
*/
const terminateCurrentCommand = () => {
// GPT
if (isRunning.value == true) {
return
}
console.log("终止!!")
isRunning.value = true;
terminal.isRunning = true;
setHint("");
let inputText = inputCommand.value.text;
const newCommand: CommandOutputType = {
text: inputText,
type: "command",
resultList: [],
};
outputList.value.push(newCommand);
inputCommand.value = { ...initCommand };
//
activeKeys.value.push(outputList.value.length - 1);
//
setTimeout(() => {
terminalRef.value.scrollTop = terminalRef.value.scrollHeight;
}, 50);
isRunning.value = false;
terminal.isRunning = false;
}
/**
* 操作终端的对象
*/
const terminal: TerminalType = {
isRunning: false,
writeTextResult,
writeTextErrorResult,
writeTextLoadingResult,
writeTextSuccessResult,
writeResult,
writeTextOutput,
writeOutput,
removeLastOutput,
clear,
focusInput,
isInputFocused,
setTabCompletion,
doSubmitCommand,
showNextCommand,
showPrevCommand,
listCommandHistory,
toggleAllCollapse,
setCommandCollapsible,
listGptHistory,
terminateCurrentCommand,
// @ts-ignore
};
/**
* 只执行一次
*/
onMounted(() => {
registerShortcuts(terminal);
terminal.writeTextOutput(" _____ _____ _______ _______ _ _ ".replace(/ /g, "&nbsp;"))
terminal.writeTextOutput(" / ____| __ \\__ __| |__ __| (_) | |".replace(/ /g, "&nbsp;"))
terminal.writeTextOutput(" | | __| |__) | | | | | ___ _ __ _ __ ___ _ _ __ __ _| |".replace(/ /g, "&nbsp;"))
terminal.writeTextOutput(" | | |_ | ___/ | | | |/ _ \\ '__| '_ ` _ \\| | '_ \\ / _` | |".replace(/ /g, "&nbsp;"))
terminal.writeTextOutput(" | |__| | | | | | | __/ | | | | | | | | | | | (_| | |".replace(/ /g, "&nbsp;"))
terminal.writeTextOutput(" \\_____|_| |_| |_|\\___|_| |_| |_| |_|_|_| |_|\\__,_|_|".replace(/ /g, "&nbsp;"))
terminal.writeTextOutput("<br/>");
terminal.writeTextOutput("Welcome to GPT Terminal!");
terminal.writeTextOutput("You can enjoy your exclusive GPT service!");
terminal.writeTextOutput("Enter the <span style='color: #ec61ad;'>help</span> command to unlock all GPT services!");
terminal.writeTextOutput(`GPT Terminal Reformer - <a href="//github.com/ltyzzzxxx/gpt-web-terminal" target="_blank">ltyzzz</a>`)
terminal.writeTextOutput(
`Thanks so much to the YuIndex author - <a href="//docs.qq.com/doc/DUFFRVWladXVjeUxW" target="_blank">coder_yupi</a>`
);
terminal.writeTextOutput("<br/>")
terminal.writeTextOutput("<span style='color: red'>Notice: </span>")
terminal.writeTextOutput("<span style='color: red'>&nbsp;&nbsp; - You can only use GPT-3.5 Model if you are not a OpenAI-paying user.</span>")
terminal.writeTextOutput("<span style='color: red'>&nbsp;&nbsp; - You can only request Open AI 3 times per minute if you are not a OpenAI-paying user.</span>")
terminal.writeTextOutput("<br/>")
terminal.writeTextOutput(`Link: <a href="//platform.openai.com/docs/guides/rate-limits/overview" target="_blank">Open AI</a>`)
terminal.writeTextOutput("<br/>");
});
/**
* 当点击空白聚焦输入框
*/
function handleClickWrapper(event: Event): void {
//@ts-ignore
if (event.target.className === "yu-terminal") {
focusInput();
}
}
defineExpose({
terminal,
});
</script>
<style scoped>
.yu-terminal-wrapper {
background: black;
}
.yu-terminal {
background: rgba(0, 0, 0, 0.6);
padding: 20px;
overflow: scroll;
}
.yu-terminal::-webkit-scrollbar {
display: none;
}
.yu-terminal span {
font-size: 16px;
}
.yu-terminal :deep(.ant-collapse-icon-position-right > .ant-collapse-item > .ant-collapse-header) {
color: white;
padding: 0;
}
.yu-terminal :deep(.ant-collapse) {
background: none;
}
.yu-terminal :deep(.ant-collapse-borderless > .ant-collapse-item) {
border: none;
}
.yu-terminal :deep(.ant-collapse-content > .ant-collapse-content-box) {
padding: 0;
}
.command-input {
caret-color: white;
}
.command-input :deep(input) {
color: white !important;
font-size: 16px;
padding: 0 10px;
}
.command-input :deep(.ant-input-group-addon) {
background: none;
border: none;
padding: 0;
}
.command-input-prompt {
color: white;
background: transparent;
}
.terminal-row {
color: white;
font-size: 16px;
font-family: courier-new, courier, monospace;
}
</style>

@ -0,0 +1,55 @@
import { ref } from "vue";
import { getUsageStr } from "../../core/commands/terminal/help/helpUtils";
import { commandMap } from "../../core/commandRegister";
import _, { trim } from "lodash";
/**
*
* @author yupi
*/
const useHint = () => {
const hint = ref("");
const setHint = (inputText: string) => {
if (!inputText) {
hint.value = "";
return;
}
const args = trim(inputText).split(" ");
// 大小写无关
let func = args[0].toLowerCase();
// 前缀匹配
const likeKey = Object.keys(commandMap).filter((key) =>
key.startsWith(func)
)[0];
let command = commandMap[likeKey];
if (!command) {
hint.value = "";
return;
}
// 子命令提示
if (
command.subCommands &&
Object.keys(command.subCommands).length > 0 &&
args.length > 1
) {
hint.value = getUsageStr(command.subCommands[args[1]], command);
} else {
hint.value = getUsageStr(command);
}
};
/**
*
*/
const debounceSetHint = _.debounce(function (inputText: string) {
setHint(inputText);
}, 100);
return {
hint,
setHint,
debounceSetHint,
};
};
export default useHint;

@ -0,0 +1,50 @@
import { Ref, ref } from "vue";
import CommandOutputType = GptTerminal.CommandOutputType;
import CommandInputType = GptTerminal.CommandInputType;
/**
*
* @param commandList
* @param inputCommand
*/
const useHistory = (
commandList: CommandOutputType[],
inputCommand: Ref<CommandInputType>
) => {
/**
*
*/
const commandHistoryPos = ref(commandList.length);
const listCommandHistory = () => {
return commandList;
};
const showNextCommand = () => {
console.log(commandHistoryPos.value, commandList, inputCommand);
if (commandHistoryPos.value < commandList.length - 1) {
commandHistoryPos.value++;
inputCommand.value.text = commandList[commandHistoryPos.value].text;
} else if (commandHistoryPos.value === commandList.length - 1) {
commandHistoryPos.value++;
inputCommand.value.text = "";
}
};
const showPrevCommand = () => {
console.log(commandHistoryPos.value, commandList, inputCommand);
if (commandHistoryPos.value >= 1) {
commandHistoryPos.value--;
inputCommand.value.text = commandList[commandHistoryPos.value].text;
}
};
return {
commandHistoryPos,
listCommandHistory,
showNextCommand,
showPrevCommand,
};
};
export default useHistory;

@ -0,0 +1,135 @@
/**
*
* @author yupi
*/
import TerminalType = GptTerminal.TerminalType;
/**
*
* @param terminal
*/
export const registerShortcuts = (terminal: TerminalType) => {
document.onkeydown = (e) => {
// console.log(e);
let key = e.key;
// 自动聚焦输入框
if (key >= "a" && key <= "z" && !e.metaKey && !e.shiftKey && !e.ctrlKey) {
terminal.focusInput();
return;
}
// 匹配快捷键
let code = e.code;
for (const shortcut of shortcutList) {
if (
code === shortcut.code &&
e.ctrlKey == !!shortcut.ctrlKey &&
e.metaKey == !!shortcut.metaKey &&
e.shiftKey == !!shortcut.shiftKey
) {
shortcut.action(e, terminal);
}
}
};
};
/**
*
*/
interface ShortcutType {
code: string; // 按键码
desc?: string; // 功能描述
keyDesc?: string; // 按键描述
ctrlKey?: boolean;
metaKey?: boolean;
shiftKey?: boolean;
action: (e: Event, terminal: TerminalType) => void;
}
/**
*
*/
export const shortcutList: ShortcutType[] = [
{
desc: "清屏",
code: "KeyL",
keyDesc: "Ctrl + L",
ctrlKey: true,
action(e, terminal) {
e.preventDefault();
terminal.clear();
},
},
{
desc: "折叠",
code: "KeyO",
keyDesc: "Ctrl + O",
ctrlKey: true,
action(e, terminal) {
e.preventDefault();
terminal.toggleAllCollapse();
},
},
// {
// desc: "粘贴",
// code: "KeyV",
// keyDesc: "Ctrl + V",
// metaKey: true,
// action(e, terminal) {
// terminal.focusInput();
// },
// },
{
desc: "终止命令",
code: "KeyC",
keyDesc: "Ctrl + C",
ctrlKey: true,
action(e, terminal) {
terminal.terminateCurrentCommand();
}
},
{
code: "Tab",
action(e, terminal) {
e.preventDefault();
if (terminal.isInputFocused()) {
terminal.setTabCompletion();
} else {
terminal.focusInput();
}
},
},
{
code: "Backspace",
action(e, terminal) {
terminal.focusInput();
},
},
{
code: "Enter",
action(e, terminal) {
terminal.focusInput();
},
},
{
desc: "查看上一条命令",
code: "ArrowUp",
keyDesc: "↑",
action(e, terminal) {
e.preventDefault();
if (!terminal.isRunning) {
terminal.showPrevCommand();
}
},
},
{
desc: "查看下一条命令",
code: "ArrowDown",
keyDesc: "↓",
action(e, terminal) {
e.preventDefault();
if (!terminal.isRunning) {
terminal.showNextCommand();
}
},
},
];

@ -0,0 +1,126 @@
declare namespace GptTerminal {
type OutputStatusType =
| "info"
| "success"
| "warning"
| "error"
| "system"
| "loading";
// 输出父类型
interface OutputType {
type: "command" | "text" | "component";
text?: string;
resultList?: OutputType[];
component?: any;
status?: OutputStatusType;
props?: any;
collapsible?: boolean;
}
/**
*
*/
interface CommandOutputType extends OutputType {
type: "command";
text: string;
resultList: OutputType[];
}
/**
*
*/
interface TextOutputType extends OutputType {
type: "text";
text: string;
}
/**
*
*/
interface ComponentOutputType extends OutputType {
type: "component";
component: any;
props?: any;
}
/**
*
*/
interface CommandInputType {
text: string;
placeholder?: string;
}
/**
* GPT
*/
interface MessageElement {
role: string;
content: string;
}
interface MessageType {
roleKeyword: string | "default";
messageElements: MessageElement[];
}
/**
*
*/
interface UserType {
id: number;
username: string;
email?: string;
creatTime?: date;
updateTime?: date;
}
/**
* 访
*/
interface TerminalType {
// 是否运行
isRunning: boolean;
// 清屏
clear: () => void;
// 立即输出
writeOutput: (output: OutputType) => void;
// 立即输出文本
writeTextOutput: (text: string, status?: OutputStatusType) => void;
// 写命令文本结果
writeTextResult: (text: string, status?: OutputStatusType) => void;
// 写命令错误文本结果
writeTextErrorResult: (text: string) => void;
// 写命令等待状态 GPT
writeTextLoadingResult: (text: string) => void;
// 写命令成功文本结果
writeTextSuccessResult: (text: string) => void;
// 写命令结果
writeResult: (output: OutputType) => void;
// 移除 outputList 最后一个元素
removeLastOutput: () => void;
// 输入框聚焦
focusInput: () => void;
// 获取输入框是否聚焦
isInputFocused: () => boolean;
// 设置输入框的值
setTabCompletion: () => void;
// 提交命令
doSubmitCommand: () => void;
// 查看下一条命令
showNextCommand: () => void;
// 查看上一条命令
showPrevCommand: () => void;
// 查看历史命令
listCommandHistory: () => CommandOutputType[];
// 折叠 / 展开所有块
toggleAllCollapse: () => void;
// 设置命令是否可折叠
setCommandCollapsible: (collapsible: boolean) => void;
// GPT 历史对话记录
listGptHistory: () => MessageType[];
// 终止当前命令运行
terminateCurrentCommand: () => void;
}
}

@ -0,0 +1,6 @@
import { RouteRecordRaw } from "vue-router";
import IndexPage from "../pages/IndexPage.vue";
const route: RouteRecordRaw[] = [{ path: "/", component: IndexPage }];
export default route;

@ -0,0 +1,48 @@
import { ParsedOptions } from "getopts";
import TerminalType = GptTerminal.TerminalType;
// 命令类型
interface CommandType {
func: string; // 唯一命令英文
name: string; // 命令名称
desc?: string; // 命令描述
alias?: string[]; // 命令别名
params?: CommandParamsType[]; // 参数配置
options: CommandOptionType[]; // 选项配置
subCommands?: Record<string, CommandType>; // 子命令
// 执行功能
action: (
options: ParsedOptions,
terminal: TerminalType,
parentCommand?: CommandType
) => void;
// 执行子功能
subAction?: (
options?: ParsedOptions,
terminal: TerminalType,
params?: Record<string, T>,
parentCommand?: CommandType,
) => void;
// 结果是否允许折叠
collapsible?: boolean;
// 是否需要用户登录下才可访问
requireAuth?: boolean | false;
}
// 命令参数类型
interface CommandParamsType {
key: string; // 参数名
desc?: string; // 参数描述
defaultValue?: string | boolean; // 默认值
required?: boolean; // 是否必填
}
// 命令选项类型
interface CommandOptionType {
key: string; // 参数名
alias?: string[]; // 别名
desc?: string; // 描述
type: "string" | "boolean";
defaultValue?: string | boolean; // 默认值
required?: boolean; // 是否必填
}

@ -0,0 +1,106 @@
import getopts, { ParsedOptions } from "getopts";
import { commandMap } from "./commandRegister";
import { CommandOptionType, CommandType } from "./command";
import TerminalType = GptTerminal.TerminalType;
import helpCommand from "./commands/terminal/help/helpCommand";
export const doCommandExecute = async (
text: string,
terminal: TerminalType,
parentCommand?: CommandType
) => {
text = text.trim();
if (!text) {
return;
}
const command: CommandType | any = getCommand(terminal, text, parentCommand);
console.log("command", command);
if (command == "403") {
terminal.writeTextErrorResult("请登录后再使用此命令");
return;
}
if (!command) {
terminal.writeTextErrorResult("找不到命令");
return;
}
const parsedOptions = doParse(text, command.options);
const { _ } = parsedOptions;
if (
_.length > 0 &&
command.subCommands &&
Object.keys(command.subCommands).length > 0
) {
const subText = text.substring(text.indexOf(" ") + 1);
await doCommandExecute(subText, terminal, command);
return;
}
await doAction(command, parsedOptions, terminal, parentCommand);
};
const getCommand = (
terminal: TerminalType,
text: string,
parentCommand?: CommandType
): CommandType | string => {
let func = text.split(" ", 1)[0];
func = func.toLowerCase();
let commands = commandMap;
if (
parentCommand &&
parentCommand.subCommands &&
Object.keys(parentCommand.subCommands).length > 0
) {
commands = parentCommand.subCommands;
}
const command = commands[func];
console.log("getCommand = ", command);
return command;
};
const doParse = (
text: string,
commandOptions: CommandOptionType[]
): getopts.ParsedOptions => {
const args: string[] = text.split(" ").slice(1);
const options: getopts.Options = {
alias: {},
default: {},
string: [],
boolean: [],
};
console.log("commandOptions", commandOptions);
commandOptions.forEach((commandOption) => {
const { alias, key, type, defaultValue } = commandOption;
console.log("解析...");
if (alias && options.alias) {
console.log("alias boolean:", Boolean(options.alias));
console.log("alias是", options.alias);
options.alias[key] = alias;
}
options[type]?.push(key);
if (defaultValue && options.default) {
options.default[key] = defaultValue;
}
});
const parsedOptions = getopts(args, options);
console.log("parsedOptions = ", parsedOptions);
return parsedOptions;
};
const doAction = async (
command: CommandType,
options: ParsedOptions,
terminal: TerminalType,
parentCommand?: CommandType
) => {
const { help, h } = options;
if (command.collapsible || help) {
terminal.setCommandCollapsible(true);
}
if (help || h) {
const newOptions = { ...options, _: [command.func] };
helpCommand.action(newOptions, terminal, parentCommand);
return;
}
await command.action(options, terminal);
};

@ -0,0 +1,31 @@
import { CommandType } from "./command";
import clearCommand from "./commands/terminal/clearCommand";
import historyCommand from "./commands/terminal/historyCommand";
import helpCommand from "./commands/terminal/help/helpCommand";
import shortcutCommand from "./commands/terminal/shortcut/shortcutCommand";
import gptCommands from "./commands/gpt/gptCommands";
/**
* help
*/
const commandList: CommandType[] = [
...gptCommands,
shortcutCommand,
clearCommand,
historyCommand,
helpCommand,
];
/**
*
*/
const commandMap: Record<string, CommandType> = {};
commandList.forEach((command) => {
commandMap[command.func] = command;
command.alias?.forEach((name) => {
commandMap[name] = command;
});
});
export { commandList, commandMap };

@ -0,0 +1,21 @@
import { defineStore } from "pinia";
interface ConfigType {
model: string;
}
export const useConfigStore = defineStore("config", {
state: () => ({
config: {} as ConfigType,
}),
getters: {},
persist: {
key: "gpt-config",
storage: window.localStorage,
},
actions: {
changeModel(model: string) {
this.config.model = model;
},
},
});

@ -0,0 +1,41 @@
import { CommandType } from "../../command";
import chatCommand from "./subCommands/chat/chatCommand";
import { defineAsyncComponent } from "vue";
import ComponentOutputType = GptTerminal.ComponentOutputType;
import roleCommand from "./subCommands/role/roleCommand";
import historyCommand from "./subCommands/history/historyCommand";
import diyCommand from "./subCommands/diy/diyCommand";
import imageCommand from "./subCommands/image/imageCommand";
import modelCommand from "./subCommands/model/modelCommand";
/**
* gpt
*/
const gptCommand: CommandType = {
func: "gpt",
name: "gpt 机器人",
alias: [],
subCommands: {
chat: chatCommand,
role: roleCommand,
history: historyCommand,
diy: diyCommand,
// image: imageCommand,
// model: modelCommand
},
options: [],
async action(options, terminal) {
const output: ComponentOutputType = {
type: "component",
component: defineAsyncComponent(
() => import("./subCommands/chat/ChatBox.vue")
),
props: {
message: "您好,我是 「GPT Terminal」 机器人,请问我能为您做些什么呢?",
},
};
terminal.writeResult(output);
},
};
export default [gptCommand];

@ -0,0 +1,59 @@
import { defineStore } from "pinia";
export const useMessagesStore = defineStore("messages", {
state: () => ({
messages: [
{
roleKeyword: "default",
roleName: "ChatGPT",
roleDesc: "默认角色",
systemMessage: "",
parentMessageId: "",
messageElements: [],
},
] as Gpt.MessageType[],
}),
getters: {},
persist: {
key: "gpt-messages",
storage: window.localStorage,
},
actions: {
addRole(
roleKeyword: string,
roleName: string,
roleDesc: string,
systemMessage: string
) {
const { messages } = this.$state;
messages.push({
roleKeyword,
roleName,
roleDesc,
systemMessage,
parentMessageId: "",
messageElements: [],
});
},
addMessage(
msg: Gpt.MessageElement,
roleKeyword: string | "default",
parentMessageId: string
) {
const { messages } = this.$state;
messages.forEach((m) => {
if (m.roleKeyword == roleKeyword) {
// 表示已找到
m.parentMessageId = parentMessageId;
if (m.messageElements.length >= 20) {
m.messageElements.shift();
}
m.messageElements.push(msg);
}
});
},
clearMessages() {
this.$state.messages = [];
},
},
});

@ -0,0 +1,207 @@
<template>
<div v-html="result" class="chat-box"></div>
</template>
<script setup lang="ts">
import { computed, onMounted, toRefs, ref, defineEmits, Ref } from "vue";
import { marked } from 'marked'
import hljs from "highlight.js";
import { useMessagesStore } from "../../messagesStore"
import { useConfigStore } from "../../configStore"
import { storeToRefs } from "pinia";
import { fetchChatAPIProcess } from './chatApi'
marked.setOptions({
renderer: new marked.Renderer,
gfm: true,
async: false,
highlight(code: string): string {
return hljs.highlightAuto(code).value
},
})
interface ChatBoxProps {
message: string;
role: string;
temperature: number;
}
interface MessageElement {
role: string;
content: string;
}
interface MessageType {
roleKeyword: string | "default";
roleName: string;
roleDesc: string;
systemMessage: string;
parentMessageId: string;
messageElements: MessageElement[];
}
const props = withDefaults(defineProps<ChatBoxProps>(), {});
const { message, role, temperature } = toRefs(props);
//
const messagesStore = useMessagesStore();
const { messages } = storeToRefs(messagesStore);
// GPT
const configStore = useConfigStore();
const { config } = storeToRefs(configStore);
const output = ref("正在加载内容中...")
const result = computed(() => {
console.log("output -", output.value)
console.log("marked output -", marked(output.value))
return marked(output.value)
})
const emit = defineEmits(['start', 'finish']);
const flag = ref(false)
interface ConversationResponse {
conversationId: string
detail: {
choices: { finish_reason: string; index: number; logprobs: any; text: string }[]
created: number
id: string
model: string
object: string
usage: { completion_tokens: number; prompt_tokens: number; total_tokens: number }
}
id: string
parentMessageId: string
role: string
text: string
}
let controller = new AbortController()
const getGptOutput = async (flag: Ref<Boolean>, messageParams: MessageType, loadingInterval: any) => {
let options = { parentMessageId: messageParams.parentMessageId }
let parentMessageId = ''
await fetchChatAPIProcess<ConversationResponse>({
prompt: message.value,
signal: controller.signal,
options,
systemMessage: messageParams.systemMessage,
temperature: temperature.value,
onDownloadProgress: ({ event }) => {
clearInterval(loadingInterval)
const xhr = event.target
const { responseText } = xhr
const lastIndex = responseText.lastIndexOf('\n', responseText.length - 2)
let chunk = responseText
if (lastIndex !== -1) {
chunk = responseText.substring(lastIndex)
}
try {
const data = JSON.parse(chunk)
output.value = (data.text ?? '')
parentMessageId = data.parentMessageId
} catch (error) {
//
}
}
}).catch((error) => {
output.value = (error.message)
})
messagesStore.addMessage(
{ role: 'user', content: message.value },
messageParams.roleKeyword,
parentMessageId
)
messagesStore.addMessage(
{ role: 'assistant', content: output.value },
messageParams.roleKeyword,
parentMessageId
)
//
flag.value = true
emit('finish')
}
onMounted(async () => {
console.log("message -", message)
console.log("role -", role)
// loading
let count = 0;
let loadingInterval = setInterval(() => {
count++;
if (count > 3) {
count = 0;
}
switch (count) {
case 0:
output.value = "正在加载内容中";
break;
case 1:
output.value = "正在加载内容中.";
break;
case 2:
output.value = "正在加载内容中..";
break;
case 3:
output.value = "正在加载内容中...";
break;
}
}, 500)
emit('start')
//
let timeoutTimer = setTimeout(() => {
clearInterval(loadingInterval)
clearTimeout(timeoutTimer)
if (!flag.value) {
emit('finish')
output.value = "请求超时,请检查您的网络环境是否配置正确 或 后端是否启动~"
}
}, 35000)
let messageType: MessageType = {
roleKeyword: '',
roleName: '',
roleDesc: '',
systemMessage: '',
parentMessageId: '',
messageElements: [],
}
let hasRole = false
messages.value.forEach(m => {
if (m.roleKeyword == role.value) {
messageType = m
hasRole = true
return
}
})
//
if (hasRole) {
//
await getGptOutput(flag, messageType, loadingInterval)
} else {
output.value = "该角色不存在~"
flag.value = true
clearInterval(loadingInterval)
emit('finish')
}
});
</script>
<style scoped>
.chat-box {
background-color: #292421;
margin: 10px 0 10px 0;
padding: 20px 20px 5px 20px;
}
</style>

@ -0,0 +1,31 @@
import type { AxiosProgressEvent, GenericAbortSignal } from "axios";
import { post } from "../../../../../utils/request";
import myAxios from "../../../../../plugins/myAxios";
export const getRoleElementsByKeyword = async (keyword: string) => {
return myAxios.post("/role/getRoleElementsByKeyword", {
keyword,
});
};
export function fetchChatAPIProcess<T = any>(params: {
prompt: string;
options?: { conversationId?: string; parentMessageId?: string };
signal?: GenericAbortSignal;
systemMessage?: string;
temperature?: number;
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
}) {
let data: Record<string, any> = {
prompt: params.prompt,
options: params.options,
systemMessage: params.systemMessage,
temperature: params.temperature
};
return post<T>({
url: "/chat-process",
data,
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
});
}

@ -0,0 +1,84 @@
import { CommandType } from "../../../../command";
import { defineAsyncComponent } from "vue";
import ComponentOutputType = GptTerminal.ComponentOutputType;
import { roleMap } from "../role/roles";
const chatCommand: CommandType = {
func: "chat",
name: "与GPT聊天",
params: [
{
key: "message",
desc: "发送给 GPT 的内容",
required: true,
},
],
options: [
{
key: "role",
desc: "GPT 角色唯一标识",
alias: ["r"],
type: "string",
defaultValue: "default",
},
{
key: "temperature",
desc: "GPT 采样温度,介于 0 2 之间,值越大输出越随机",
alias: ["t"],
type: "string",
defaultValue: "1",
},
],
async action(options, terminal) {
const { _, role, temperature } = options;
if (_.length < 1) {
terminal.writeTextErrorResult("内容不可为空");
return;
}
if (temperature) {
if (
isNaN(temperature) ||
Number(temperature) < 0 ||
Number(temperature) > 2
) {
terminal.writeTextErrorResult("temperature 必须为 0 2 之间的整数");
return;
}
}
const message = _.join(" ");
// const res: any = await getGptOutput(message, role);
// console.log(res);
// console.log(typeof(res))
// 调用接口放在 ChatBox 内部去做,传入 ChatBox的参数为用户输入的 message
const output: ComponentOutputType = {
type: "component",
component: defineAsyncComponent(() => import("./ChatBox.vue")),
props: {
message: message,
role: role,
temperature: Number(temperature),
},
};
terminal.writeResult(output);
// if (res?.code === 0) {
// console.log("gpt响应", res);
// const output: ComponentOutputType = {
// type: "component",
// component: defineAsyncComponent(() => import("./ChatBox.vue")),
// props: {
// message: res.data,
// },
// };
// terminal.writeResult(output);
// } else {
// terminal.writeTextErrorResult(res?.message ?? "GPT请求失败");
// }
},
};
export default chatCommand;

@ -0,0 +1,115 @@
<template>
<div @keydown="handleKeyDown">
<div v-for="(output, index) in displayList" :key="index">
<span :style="{ color: index % 2 !== 0 ? '' : '#ec61ad' }">{{ output }}</span>
</div>
<div class="terminal-row">
<a-input ref="inputRef" v-if="!finished" v-model:value="input" autofocus @press-enter="doSubmit"
class="command-input white-background-text" :bordered="false">
</a-input>
</div>
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { onMounted, ref, defineEmits, toRefs } from "vue";
import { useMessagesStore } from "../../messagesStore"
const messagesStore = useMessagesStore();
const { messages } = storeToRefs(messagesStore);
//
const displayList = ref<any[]>([])
// input
const input = ref("")
interface DiyBoxProps {
keyword: string,
name: string,
description: string,
}
const props = withDefaults(defineProps<DiyBoxProps>(), {});
const { keyword, name, description } = toRefs(props);
interface RoleElement {
name: string;
content: string;
}
const roleElementList = ref<RoleElement[]>([])
const finished = ref(false)
const emit = defineEmits(['start', 'finish', 'failed']);
const inputRef = ref()
const handleKeyDown = (e: any) => {
let key = e.key;
//
if (key >= "a" && key <= "z" && !e.metaKey && !e.shiftKey && !e.ctrlKey) {
inputRef.value.focus();
return;
}
let code = e.code;
if (code === 'KeyC') {
displayList.value.push(input.value)
input.value = ''
finished.value = true
emit('finish')
}
}
const doSubmit = async () => {
if (!input.value) {
displayList.value.push("❗️角色定义不可以为空!")
return
}
messagesStore.addRole(keyword.value, name.value, description.value, input.value)
displayList.value.push(input.value)
displayList.value.push('🎉 角色已创建成功,请尽情享用吧~')
input.value = ''
finished.value = true
emit('finish')
}
onMounted(async () => {
let flag = false
messages.value.forEach((m) => {
if (m.roleKeyword == keyword.value) {
flag = true
}
})
if (flag) {
displayList.value.push("❗️当前角色已存在")
finished.value = true
} else {
window.addEventListener('keydown', handleKeyDown)
emit('start')
displayList.value.push("💌 请开始定制您的专属角色吧~")
displayList.value.push("✍️ 请定义当前角色,建议以 ‘从现在开始,你是 xxx 的格式开头")
}
});
</script>
<style scoped>
.command-input {
caret-color: white !important;
color: white !important;
}
.command-input :deep(input) {
color: white !important;
font-size: 16px;
padding: 0 10px;
}
.terminal-row {
color: white !important;
font-size: 16px;
font-family: courier-new, courier, monospace;
}
</style>

@ -0,0 +1,61 @@
import { CommandType } from "../../../../command";
import { defineAsyncComponent } from "vue";
import ComponentOutputType = GptTerminal.ComponentOutputType;
const diyCommand: CommandType = {
func: "diy",
name: "自定义 GPT 角色",
params: [],
requireAuth: true,
options: [
{
key: "keyword",
desc: "GPT 角色唯一标识",
alias: ["k"],
type: "string",
required: true,
},
{
key: "name",
desc: "GPT 角色名",
alias: ["n"],
type: "string",
required: true,
},
{
key: "desc",
desc: "GPT 角色描述",
alias: ["d"],
type: "string",
required: true,
},
],
async action(options, terminal) {
const { keyword, name, desc } = options;
// TODO:用户自定义角色后,需要包含进来
if (!keyword) {
terminal.writeTextErrorResult("角色唯一标识必填");
return;
}
if (!name) {
terminal.writeTextErrorResult("角色名称必填");
return;
}
if (!desc) {
terminal.writeTextErrorResult("角色描述必填");
return;
}
const diyBox: ComponentOutputType = {
type: "component",
component: defineAsyncComponent(() => import("./DiyBox.vue")),
props: {
keyword: keyword,
name: name,
description: desc,
},
};
terminal.writeResult(diyBox);
},
};
export default diyCommand;

@ -0,0 +1,46 @@
<template>
<div>🙋 {{ gptCommand }}</div>
<div v-html="result" class="chat-box"></div>
</template>
<script setup lang="ts">
import { computed, onMounted, toRefs } from "vue";
import { marked } from 'marked'
import hljs from "highlight.js";
marked.setOptions({
renderer: new marked.Renderer,
gfm: true,
async: false,
highlight(code: string): string {
return hljs.highlightAuto(code).value
},
})
interface RecordProps {
gptCommand: string;
gptOutput: string;
}
const props = withDefaults(defineProps<RecordProps>(), {});
const { gptCommand, gptOutput } = toRefs(props);
const result = computed(() => {
console.log("gptCommand -", gptOutput)
console.log("gptOutput -", marked(gptOutput.value))
return marked(gptOutput.value)
})
onMounted(async () => {
});
</script>
<style scoped>
.chat-box {
background-color: #292421;
margin: 10px 0 10px 0;
padding: 20px 20px 5px 20px;
}
</style>

@ -0,0 +1,71 @@
import { CommandType } from "../../../../command";
import { defineAsyncComponent } from "vue";
import ComponentOutputType = GptTerminal.ComponentOutputType;
// import { useMessagesStore } from "../../messagesStore";
// const messagesStore = useMessagesStore();
// const messages = messagesStore.$state.messages;
const historyCommand: CommandType = {
func: "history",
name: "查看过去提问与回答记录",
params: [],
options: [
{
key: "position",
desc: "聊天记录编号,输出相对应的单条问答记录",
alias: ["p"],
type: "string",
},
],
// 输入1对应0
// 输入2对应2
// 输入3对应4
// 输入4对应6
// 输入5对应8
async action(options, terminal) {
const { position } = options;
const messages = terminal.listGptHistory();
console.log("messages:", messages);
// const len = messages.reduce(
// (acc, cur) => acc + cur.messageElements.length,
// 0
// );
// 列出全部 gpt 用户提问记录
const allMessages: any = [];
messages.forEach((message) => {
message?.messageElements?.forEach((e) => {
allMessages.push({ ...e, roleKeyword: message.roleKeyword });
});
});
if (!position) {
let index = 1;
allMessages?.forEach((e: any) => {
if (e.role == "user") {
terminal.writeTextResult(
`${index} gpt chat -r ${e.roleKeyword} ${e.content}`
);
index += 1;
}
});
} else if (position < 1 || position * 2 > allMessages.length) {
terminal.writeTextErrorResult("输入序号有误,请重新输入~");
} else {
const inputMessage = allMessages[(position - 1) * 2];
const gptCommand = `gpt chat -r ${inputMessage.roleKeyword} ${inputMessage.content}`;
const outputMessage = allMessages[(position - 1) * 2 + 1];
const gptOutput = outputMessage.content;
const recordBox: ComponentOutputType = {
type: "component",
component: defineAsyncComponent(() => import("./RecordBox.vue")),
props: {
gptCommand: gptCommand,
gptOutput: gptOutput,
},
};
terminal.writeResult(recordBox);
}
},
};
export default historyCommand;

@ -0,0 +1,78 @@
<template>
<div class="image-box">
<span v-if="flag">{{ loading }}</span>
<div v-else>
<a-row :gutter="[0, 40]">
<a-col :span="4" v-for="(item, index) in imageUrlList" :key="index">
<a-image :src="item.url" />
</a-col>
</a-row>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, toRefs, ref, defineEmits, Ref } from "vue";
import { getImage } from "./imageApi";
interface ImageBoxProps {
prompt: string;
number: number;
size: string
}
const props = withDefaults(defineProps<ImageBoxProps>(), {});
const { prompt, number, size } = toRefs(props);
const flag = ref(true)
const emit = defineEmits(['start', 'finish']);
const loading = ref("图片内容较大,正在加载中...")
const imageUrlList = ref<any>([])
onMounted(async () => {
// loading
let count = 0
let loadingInterval = setInterval(() => {
if (count > 3) {
count = 0;
}
switch (count) {
case 0:
loading.value = "图片内容较大,正在加载中";
break;
case 1:
loading.value = "图片内容较大,正在加载中.";
break;
case 2:
loading.value = "图片内容较大,正在加载中..";
break;
case 3:
loading.value = "图片内容较大,正在加载中...";
break;
}
count++;
}, 500)
emit('start')
const res: any = await getImage(prompt.value, number.value, size.value)
if (res?.code !== 0) {
loading.value = res?.msg ? res.msg : "服务端请求异常"
emit('finish')
clearInterval(loadingInterval)
return;
}
flag.value = false
clearInterval(loadingInterval)
loading.value = ''
imageUrlList.value = res.data
emit('finish')
})
</script>
<style scoped>
.image-box {
background-color: #292421;
margin: 10px 0 10px 0;
padding: 20px 20px 20px 20px;
}
</style>

@ -0,0 +1,13 @@
import myAxios from "../../../../../plugins/myAxios";
export const getImage = async (
prompt: string,
number: number,
size: string
) => {
return await myAxios.post("/gpt/getImage", {
prompt,
number,
size,
});
};

@ -0,0 +1,72 @@
import { defineAsyncComponent } from "vue";
import { CommandType } from "../../../../command";
import ComponentOutputType = GptTerminal.ComponentOutputType;
const sizeMap: Map<string, string> = new Map([
["256", "256x256"],
["512", "512x512"],
["1024", "1024x1024"],
]);
const imageCommand: CommandType = {
func: "image",
name: "智能生成图片",
params: [
{
key: "prompt",
desc: "生成图片的提示文本",
required: true,
},
],
options: [
{
key: "number",
desc: "生成图片的数量",
alias: ["n"],
type: "string",
required: false,
},
{
key: "size",
desc: "生成图片的尺寸,可选 256/512/1024",
alias: ["s"],
type: "string",
required: false,
},
],
async action(options, terminal) {
const { _, number, size } = options;
if (_.length < 1) {
terminal.writeTextErrorResult("图片生成提示内容不可为空");
return;
}
if (number) {
if (isNaN(number) || Number(number) < 1 || Number(number) > 5) {
terminal.writeTextErrorResult("图片数量必须为15之间的整数");
return;
}
}
if (size && !sizeMap.has(size)) {
terminal.writeTextErrorResult("图片尺寸格式不正确");
return;
}
const prompt = _.join(" ");
// const res: any = await getImage(prompt, number, size);
// if (res?.code !== 0) {
// terminal.writeTextErrorResult(res.message);
// return;
// }
const imageBox: ComponentOutputType = {
type: "component",
component: defineAsyncComponent(() => import("./ImageBox.vue")),
props: {
prompt: prompt,
number: Number(number),
size: sizeMap.get(size),
},
};
terminal.writeResult(imageBox);
},
};
export default imageCommand;

@ -0,0 +1,73 @@
<template>
<div v-if="!isSet" @keydown="handleKeyDown" class="select-box">
<div>Current Model: <span style="color: #dae52e">{{ config.model }}</span></div>
<div v-for="(item, index) in models" :key="index">
<span v-if="current == index" style="color: dodgerblue;">> </span>
<span v-else>&nbsp;&nbsp;</span>
<span :style="current == index ? { color: 'dodgerblue' } : {}">{{ item.name }}</span>
</div>
</div>
<div v-else> Set GPT Model successfully!</div>
</template>
<script setup lang="ts">
import { onMounted, ref, onUnmounted } from 'vue'
import { modelList, Model } from './models'
import { useConfigStore } from "../../configStore"
import { storeToRefs } from "pinia";
const emit = defineEmits(['start', 'finish']);
const isSet = ref<boolean>(false)
const models = ref<Model[]>(modelList)
const current = ref<number>(0)
const configStore = useConfigStore();
const { config } = storeToRefs(configStore);
const handleKeyDown = (e: any) => {
if (e.key == 'ArrowUp' || e.key == 'ArrowDown') {
handleCurrent(e.key)
} else if (e.key == 'Enter') {
handleSubmit()
}
}
const handleSubmit = () => {
console.log('current', current.value)
configStore.changeModel(models.value[current.value].name)
isSet.value = true
emit('finish')
window.removeEventListener('keydown', handleKeyDown)
}
const handleCurrent = (key: string) => {
if (key == 'ArrowUp') {
if (current.value == 0) {
current.value = models.value.length - 1
} else {
current.value -= 1
}
} else if (key == 'ArrowDown') {
if (current.value == models.value.length - 1) {
current.value = 0
} else {
current.value += 1
}
}
}
onMounted(() => {
emit('start')
window.addEventListener('keydown', handleKeyDown)
})
</script>
<style scoped>
.select-box {
display: flex;
flex-direction: column;
}
</style>

@ -0,0 +1,20 @@
import { CommandType } from "../../../../command";
import { defineAsyncComponent } from "vue";
import ComponentOutputType = GptTerminal.ComponentOutputType;
const modelCommand: CommandType = {
func: "model",
name: "选择并设置 GPT 模型",
params: [],
options: [],
async action(options, terminal) {
const output: ComponentOutputType = {
type: "component",
component: defineAsyncComponent(() => import("./ModelSelectBox.vue")),
};
terminal.writeResult(output);
return;
},
};
export default modelCommand;

@ -0,0 +1,35 @@
export interface Model {
key: string;
name: string;
}
export const modelList: Model[] = [
{
key: "gpt-3.5-turbo",
name: "gpt-3.5-turbo",
},
{
key: "gpt-3.5-turbo-16k",
name: "gpt-3.5-turbo-16k",
},
{
key: "gpt-3.5-turbo-0613",
name: "gpt-3.5-turbo-0613",
},
{
key: "gpt-3.5-turbo-16k-0613",
name: "gpt-3.5-turbo-16k-0613",
},
{
key: "gpt-4-0613",
name: "gpt-4-0613",
},
{
key: "gpt-4-32k",
name: "gpt-4-32k",
},
{
key: "gpt-4-32k-0613",
name: "gpt-4-32k-0613",
}
]

@ -0,0 +1,108 @@
<template>
<!-- <div v-for="role in roles" :key="role.keyword">
{{ role.keyword }} - {{ role.name }} - {{ role.desc }}
</div> -->
<div style="padding: 15px;">
<table>
<thead>
<tr>
<th>keyword</th>
<th>名称</th>
<th>简介</th>
</tr>
</thead>
<tbody>
<tr v-for="role in roles" :key="role.keyword">
<td>{{ role.keyword }}</td>
<td>{{ role.name }}</td>
<td>{{ role.desc }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { storeToRefs } from "pinia";
import { useMessagesStore } from "../../messagesStore"
const messagesStore = useMessagesStore();
const { messages } = storeToRefs(messagesStore);
interface Role {
keyword?: string
name: string
desc: string
}
interface Columns {
title: string
dataIndex: string
key: string
}
const roles = ref<Role[]>([])
const columns = ref<Columns[]>([])
columns.value = [
{
title: 'keyword',
dataIndex: 'keyword',
key: 'keyword',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '简介',
dataIndex: 'desc',
key: 'desc',
},
],
onMounted(async () => {
messages.value.forEach((m) => {
roles.value.push({
keyword: m.roleKeyword,
name: m.roleName,
desc: m.roleDesc
})
})
});
</script>
<style scoped>
.chat-box {
background-color: #292421;
margin: 10px 0 10px 0;
padding: 20px 20px 5px 20px;
}
table,
th,
td {
border: 1px solid black;
}
tr {
text-align: left;
}
table {
border-collapse: collapse;
width: 100%;
}
th {
height: 10px;
}
td {
text-align: left;
height: 10px;
vertical-align: bottom;
}
</style>

@ -0,0 +1,20 @@
import { CommandType } from "../../../../command";
import { defineAsyncComponent } from "vue";
import ComponentOutputType = GptTerminal.ComponentOutputType;
const roleCommand: CommandType = {
func: "role",
name: "查看所有 GPT 角色",
params: [],
options: [],
async action(options, terminal) {
const output: ComponentOutputType = {
type: "component",
component: defineAsyncComponent(() => import("./RoleBox.vue")),
};
terminal.writeResult(output);
return;
},
};
export default roleCommand;

@ -0,0 +1,42 @@
interface Role {
name: string;
desc: string;
}
export const roleMap: Map<string, Role> = new Map([
[
"default",
{
name: "默认角色",
desc: "无任何定制的普通 GPT 聊天机器人",
},
],
[
"cli",
{
name: "命令行翻译角色",
desc: "将你的自然语言指令翻译为 Window/Unix 终端命令",
},
],
[
"translator",
{
name: "中英文互译角色",
desc: "将你所发的内容进行中英文互译",
},
],
[
"sql",
{
name: "SQL 翻译角色",
desc: "将你的自然语言指令翻译为 SQL 代码",
},
],
[
"ikun",
{
name: "忠实的 IKun",
desc: "小黑子~"
}
]
]);

@ -0,0 +1,15 @@
declare namespace Gpt {
interface MessageElement {
role: string;
content: string;
}
interface MessageType {
roleKeyword: string | "default";
roleName: string;
roleDesc: string;
systemMessage: string;
parentMessageId: string;
messageElements: MessageElement[];
}
}

@ -0,0 +1,15 @@
import { CommandType } from "../../command";
const clearCommand: CommandType = {
func: "clear",
name: "清屏",
alias: ["cl"],
options: [],
action(options, termial): void {
setTimeout(() => {
termial.clear();
}, 100);
},
};
export default clearCommand;

@ -0,0 +1,72 @@
<template>
<div>
<div>命令{{ command.name }}</div>
<div v-if="command.desc">{{ command.desc }}</div>
<div v-if="command.alias && command.alias.length > 0">
别名{{ command.alias.join(", ") }}
</div>
<div>用法{{ usageStr }}</div>
<template
v-if="command.subCommands && Object.keys(command.subCommands).length > 0"
>
<div>子命令</div>
<ul style="margin-bottom: 0">
<li
v-for="(subCommand, key, index) in command.subCommands"
:key="index"
>
{{ subCommand.func }}
{{ subCommand.name }}
{{ subCommand.desc }}
</li>
</ul>
</template>
<template v-if="command.params && command.params.length > 0">
<div>参数</div>
<ul style="margin-bottom: 0">
<li v-for="(param, index) in command.params" :key="index">
{{ param.key }}
{{ param.required ? "必填" : "可选" }}
{{ param.defaultValue ? `默认:${param.defaultValue}` : "" }}
{{ param.desc }}
</li>
</ul>
</template>
<template v-if="command.options?.length > 0">
<div>选项</div>
<ul style="margin-bottom: 0">
<li v-for="(option, index) in command.options" :key="index">
{{ getOptionKeyList(option).join(", ") }}
{{ option.required ? "必填" : "可选" }}
{{ option.defaultValue ? `默认:${option.defaultValue}` : "" }}
{{ option.desc }}
</li>
</ul>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, toRefs } from "vue";
import { CommandType } from "../../../command";
import { getUsageStr, getOptionKeyList } from "./helpUtils";
interface HelpBoxProps {
command: CommandType;
parentCommand: CommandType;
}
const props = withDefaults(defineProps<HelpBoxProps>(), {});
const { command, parentCommand } = toRefs(props);
/**
* 拼接用法字符串
*/
const usageStr = computed(() => {
return getUsageStr(command.value, parentCommand.value);
});
onMounted(() => {});
</script>
<style scoped></style>

@ -0,0 +1,24 @@
<template>
<div>
<div>
使用 [help 命令英文名] 可以查询某命令的具体用法help search
</div>
<div>命令列表</div>
<div v-for="(command, index) in commandList" :key="index">
<a-row :gutter="16">
<a-col :span="4">{{ command.func }}</a-col>
<a-col :span="4">{{ command.name }}</a-col>
<a-col>{{ command.desc }}</a-col>
</a-row>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { commandList } from "../../../commandRegister";
onMounted(() => {});
</script>
<style scoped></style>

@ -0,0 +1,58 @@
import { CommandType } from "../../../command";
import { defineAsyncComponent } from "vue";
import { commandMap } from "../../../commandRegister";
import ComponentOutputType = GptTerminal.ComponentOutputType;
/**
*
* @author yupi
*/
const helpCommand: CommandType = {
func: "help",
name: "查看帮助",
alias: ["man"],
params: [
{
key: "commandName",
desc: "命令英文名称",
},
],
options: [],
collapsible: true,
action(options, terminal, parentCommand): void {
const { _ } = options;
if (_.length < 1) {
const output: ComponentOutputType = {
type: "component",
component: defineAsyncComponent(() => import("./HelpBox.vue")),
};
terminal.writeResult(output);
return;
}
const commandName = _[0];
let commands = commandMap;
if (
parentCommand &&
parentCommand.subCommands &&
Object.keys(parentCommand.subCommands).length > 0
) {
commands = parentCommand.subCommands;
}
const command = commands[commandName];
if (!command) {
terminal.writeTextErrorResult("找不到指定命令");
return;
}
const output: ComponentOutputType = {
type: "component",
component: defineAsyncComponent(() => import("./CommandHelpBox.vue")),
props: {
command,
parentCommand,
},
};
terminal.writeResult(output);
},
};
export default helpCommand;

@ -0,0 +1,83 @@
import { CommandOptionType, CommandType } from "../../../command";
export const getUsageStr = (
command: CommandType,
parentCommand?: CommandType
) => {
if (!command) {
return "";
}
let str = "";
if (parentCommand) {
str = parentCommand.func + " ";
}
str += command.func;
if (command.params && command.params.length > 0) {
const paramsStrList: string[] = command.params.map((param) => {
let word = param.key;
if (param.desc) {
word = param.desc;
}
if (param.required) {
return `<${word}>`;
} else {
return `[${word}]`;
}
});
str += " " + paramsStrList.join(" ");
}
if (command.options?.length > 0) {
const optionStrList: string[] = command.options.map((option) => {
const optionKey = getOptionKey(option);
if (option.type === "boolean") {
let word = optionKey;
if (option.desc) {
word += ` ${option.desc}`;
}
if (option.required) {
return `<${word}>`;
} else {
return `[${word}]`;
}
} else {
let word = option.key;
if (option.desc) {
word = option.desc;
}
if (option.required) {
return `<${optionKey} ${word}>`;
} else {
return `[${optionKey} ${word}]`;
}
}
});
str += " " + optionStrList.join(" ");
}
return str;
};
/**
*
* @param option
*/
export const getOptionKey = (option: CommandOptionType) => {
// 优先用简写
if (option.alias && option.alias.length > 0) {
return "-" + option.alias[0];
}
return "--" + option.key;
};
/**
*
* @param option
*/
export const getOptionKeyList = (option: CommandOptionType) => {
const list = [];
// 优先用简写
if (option.alias && option.alias.length > 0) {
list.push("-" + option.alias[0]);
}
list.push("--" + option.key);
return list;
};

@ -0,0 +1,17 @@
import { CommandType } from "../../command";
const historyCommand: CommandType = {
func: "history",
name: "查看执行历史",
alias: ["h"],
options: [],
collapsible: true,
action(options, terminal): void {
const commandOutputTypes = terminal.listCommandHistory();
commandOutputTypes.forEach((command, index) => {
terminal.writeTextResult(`${index + 1} ${command.text}`);
});
},
};
export default historyCommand;

@ -0,0 +1,20 @@
<template>
<div>
<div> 快捷键</div>
<div v-for="(shortcut, index) in shortcutList" :key="index">
<a-row v-if="shortcut.desc" :gutter="16">
<a-col :span="4">{{ shortcut.keyDesc ?? shortcut.code }}</a-col>
<a-col :span="4">{{ shortcut.desc }}</a-col>
</a-row>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { shortcutList } from "../../../../components/gpt-terminal/shortcuts";
onMounted(() => {});
</script>
<style scoped></style>

@ -0,0 +1,26 @@
import { CommandType } from "../../../command";
import { defineAsyncComponent } from "vue";
import ComponentOutputType = GptTerminal.ComponentOutputType;
/**
*
* @author yupi
*/
const shortcutCommand: CommandType = {
func: "shortcut",
name: "快捷键",
desc: "查看快捷键",
alias: [],
params: [],
options: [],
collapsible: true,
action(options, terminal): void {
const output: ComponentOutputType = {
type: "component",
component: defineAsyncComponent(() => import("./ShortcutBox.vue")),
};
terminal.writeResult(output);
},
};
export default shortcutCommand;

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

@ -0,0 +1,23 @@
import { createApp } from "vue";
import App from "./App.vue";
import * as VueRouter from "vue-router";
import routes from "./configs/routes";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import 'highlight.js/styles/github.css'
const app = createApp(App);
// 路由
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(),
routes,
});
app.use(router);
// 状态管理
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
app.use(pinia);
app.mount("#app");

@ -0,0 +1,33 @@
<template>
<gpt-terminal ref="terminalRef" full-screen :on-submit-command="onSubmitCommand" />
</template>
<script setup lang="ts">
import { doCommandExecute } from "../core/commandExecutor";
import { onMounted, ref } from "vue";
import { storeToRefs } from "pinia";
import { useConfigStore } from "../core/commands/gpt/configStore";
const terminalRef = ref();
const onSubmitCommand = async (inputText: string) => {
if (!inputText) {
return;
}
const terminal = terminalRef.value.terminal;
await doCommandExecute(inputText, terminal);
};
const configStore = useConfigStore();
const { config } = storeToRefs(configStore);
onMounted(() => {
if (!config.value.model) {
configStore.changeModel("gpt-3.5-turbo");
}
});
</script>
<style></style>

@ -0,0 +1,29 @@
import axios from "axios";
let serverAddress = import.meta.env.VITE_SERVER_ADDRESS
const myAxios = axios.create({
baseURL: `http://${serverAddress}/api`,
});
myAxios.defaults.withCredentials = true;
myAxios.interceptors.request.use(
function (config) {
return config;
},
function (error) {
return Promise.reject(error);
}
);
myAxios.interceptors.response.use(
function (response) {
return response.data;
},
function (error) {
return Promise.reject(error);
}
);
export default myAxios;

@ -0,0 +1,32 @@
import axios, { type AxiosResponse } from 'axios'
// import { useAuthStore } from '@/store'
const service = axios.create({
baseURL: "/api",
})
service.interceptors.request.use(
(config) => {
// const token = useAuthStore().token
// if (token)
// config.headers.Authorization = `Bearer ${token}`
return config;
},
(error) => {
return Promise.reject(error.response)
},
)
service.interceptors.response.use(
(response: AxiosResponse): AxiosResponse => {
if (response.status === 200)
return response
throw new Error(response.status.toString())
},
(error) => {
return Promise.reject(error)
},
)
export default service

@ -0,0 +1,81 @@
import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios'
import request from './axios'
export interface HttpOption {
url: string
data?: any
method?: string
headers?: any
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void
signal?: GenericAbortSignal
beforeRequest?: () => void
afterRequest?: () => void
}
export interface Response<T = any> {
data: T
message: string | null
status: string
}
function http<T = any>(
{ url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,
) {
const successHandler = (res: AxiosResponse<Response<T>>) => {
if (res.data.status === 'Success' || typeof res.data === 'string')
return res.data
if (res.data.status === 'Unauthorized') {
window.location.reload()
}
return Promise.reject(res.data)
}
const failHandler = (error: Response<Error>) => {
afterRequest?.()
throw new Error(error?.message || 'Error')
}
beforeRequest?.()
method = method || 'GET'
const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {})
return method === 'GET'
? request.get(url, { params, signal, onDownloadProgress }).then(successHandler, failHandler)
: request.post(url, params, { headers, signal, onDownloadProgress }).then(successHandler, failHandler)
}
export function get<T = any>(
{ url, data, method = 'GET', onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,
): Promise<Response<T>> {
return http<T>({
url,
method,
data,
onDownloadProgress,
signal,
beforeRequest,
afterRequest,
})
}
export function post<T = any>(
{ url, data, method = 'POST', headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,
): Promise<Response<T>> {
return http<T>({
url,
method,
data,
headers,
onDownloadProgress,
signal,
beforeRequest,
afterRequest,
})
}
export default post

@ -0,0 +1,19 @@
/**
*
*/
const URL_REG =
/(((https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
/**
*
* @param text
*/
const smartText = (text?: string) => {
if (!text) {
return text;
}
const reg = new RegExp(URL_REG, "gi");
return text.replaceAll(reg, "<a href='$1' target='_blank'>$1</a>");
};
export default smartText;

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"skipLibCheck": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}

@ -0,0 +1,9 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build"
}
]
}

@ -0,0 +1,32 @@
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
// @ts-ignore
import Components from "unplugin-vue-components/vite";
// @ts-ignore
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
// https://vitejs.dev/config/
export default defineConfig((env) => {
const viteEnv = loadEnv(env.mode, process.cwd())
return {
plugins: [
vue(),
// 按需加载 ant-design-vue
Components({
resolvers: [AntDesignVueResolver()],
}),
].filter(Boolean),
server: {
host: '0.0.0.0',
port: 3001,
open: false,
proxy: {
'/api': {
target: viteEnv.VITE_APP_API_BASE_URL,
changeOrigin: true, // 允许跨域
rewrite: path => path.replace('/api/', '/'),
},
},
},
};
});

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save