Repository: memochou1993/gpt-ai-assistant
Branch: main
Commit: 926377040e63
Files: 130
Total size: 117.0 KB
Directory structure:
gitextract_29uo5e54/
├── .dockerignore
├── .eslintrc.cjs
├── .github/
│ └── ISSUE_TEMPLATE/
│ └── question.md
├── .gitignore
├── .npmrc
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.md
├── api/
│ └── index.js
├── app/
│ ├── actions/
│ │ ├── action.js
│ │ ├── index.js
│ │ └── message.js
│ ├── app.js
│ ├── commands/
│ │ ├── analyze-analyze.js
│ │ ├── analyze-literarily.js
│ │ ├── analyze-mathematically.js
│ │ ├── analyze-numerologically.js
│ │ ├── analyze-philosophically.js
│ │ ├── analyze-psychologically.js
│ │ ├── bot-activate.js
│ │ ├── bot-continue.js
│ │ ├── bot-deactivate.js
│ │ ├── bot-draw-demo.js
│ │ ├── bot-draw.js
│ │ ├── bot-forget.js
│ │ ├── bot-retry.js
│ │ ├── bot-search-demo.js
│ │ ├── bot-search.js
│ │ ├── bot-summon-demo.js
│ │ ├── bot-talk-demo.js
│ │ ├── bot-talk.js
│ │ ├── command.js
│ │ ├── index.js
│ │ ├── sum-advise.js
│ │ ├── sum-apologize.js
│ │ ├── sum-blame.js
│ │ ├── sum-comfort.js
│ │ ├── sum-complain.js
│ │ ├── sum-encourage.js
│ │ ├── sum-laugh.js
│ │ ├── sum-sum.js
│ │ ├── sys-command.js
│ │ ├── sys-deploy.js
│ │ ├── sys-doc.js
│ │ ├── sys-report.js
│ │ ├── sys-version.js
│ │ ├── translate-to-en.js
│ │ └── translate-to-ja.js
│ ├── context.js
│ ├── handlers/
│ │ ├── activate.js
│ │ ├── command.js
│ │ ├── continue.js
│ │ ├── deactivate.js
│ │ ├── deploy.js
│ │ ├── doc.js
│ │ ├── draw.js
│ │ ├── enquire.js
│ │ ├── forget.js
│ │ ├── index.js
│ │ ├── report.js
│ │ ├── retry.js
│ │ ├── search.js
│ │ ├── talk.js
│ │ └── version.js
│ ├── history/
│ │ ├── history.js
│ │ ├── index.js
│ │ └── message.js
│ ├── index.js
│ ├── messages/
│ │ ├── image.js
│ │ ├── index.js
│ │ ├── message.js
│ │ ├── template.js
│ │ └── text.js
│ ├── models/
│ │ ├── bot.js
│ │ ├── event.js
│ │ ├── index.js
│ │ └── source.js
│ ├── prompt/
│ │ ├── index.js
│ │ ├── message.js
│ │ └── prompt.js
│ └── repository/
│ ├── index.js
│ └── source.js
├── babel.config.cjs
├── config/
│ └── index.js
├── constants/
│ ├── command.js
│ └── mock.js
├── docker-compose.yaml
├── locales/
│ ├── en.js
│ ├── index.js
│ ├── ja.js
│ └── zh.js
├── middleware/
│ ├── index.js
│ └── validate-line-signature.js
├── package.json
├── services/
│ ├── line.js
│ ├── openai.js
│ ├── serpapi.js
│ ├── utils/
│ │ └── index.js
│ └── vercel.js
├── storage/
│ └── index.js
├── tests/
│ ├── activate.test.js
│ ├── command.test.js
│ ├── continue.test.js
│ ├── deactivate.test.js
│ ├── default.test.js
│ ├── draw.test.js
│ ├── enquire.test.js
│ ├── summon.test.js
│ ├── talk.test.js
│ ├── utils.js
│ └── version.test.js
├── utils/
│ ├── add-mark.js
│ ├── convert-text.js
│ ├── fetch-answer.js
│ ├── fetch-audio.js
│ ├── fetch-environment.js
│ ├── fetch-group.js
│ ├── fetch-image.js
│ ├── fetch-user.js
│ ├── fetch-version.js
│ ├── generate-completion.js
│ ├── generate-image.js
│ ├── generate-transcription.js
│ ├── get-command.js
│ ├── get-version.js
│ ├── index.js
│ ├── reply-message.js
│ └── validate-signature.js
└── vercel.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
/node_modules
================================================
FILE: .eslintrc.cjs
================================================
module.exports = {
env: {
commonjs: true,
es2021: true,
node: true,
},
extends: 'airbnb',
overrides: [
{
files: [
'config/index.js',
],
rules: {
'max-len': 'off',
},
},
],
parserOptions: {
ecmaVersion: 'latest',
},
rules: {
'import/extensions': ['error', 'always', { ignorePackages: true }],
'no-console': 'off',
'no-param-reassign': 'off',
'no-unused-vars': 'off',
'max-len': 'off',
},
};
================================================
FILE: .github/ISSUE_TEMPLATE/question.md
================================================
---
name: Question
about: Ask a question about this project
title: ''
labels: question
assignees: ''
---
================================================
FILE: .gitignore
================================================
node_modules
.vscode
.env
.env.*
================================================
FILE: .npmrc
================================================
tag-version-prefix=""
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## 4.9.1 (2024-07-10)
### Bug Fixes
- Update `talk` command
## 4.9.0 (2024-07-10)
### New Features
- Support `gpt-4o` model
## 4.8.4 (2024-07-06)
### Bug Fixes
- Update status page
## 4.8.3 (2024-02-03)
### Bug Fixes
- Fix `maxDuration` for `vercel.json`
## 4.8.2 (2024-02-03)
### Bug Fixes
- Use `gl` param for SerpApi
- Remove `SERPAPI_LANG` environment variable
## 4.8.1 (2024-02-03)
### Bug Fixes
- Add `maxDuration` for `vercel.json`
## 4.8.0 (2023-12-07)
### New Features
- Support fine-tuned models
## 4.7.6 (2023-11-18)
### Bug Fixes
- Change default max groups to 1000
- Change default max users to 1000
- Change default max prompt messages to 4
- Change default max prompt tokens to 160
- Change default completion temperature to 1
- Change default completion max tokens to 64
## 4.7.5 (2023-10-01)
### Bug Fixes
- Update status page
## 4.7.4 (2023-08-26)
### Bug Fixes
- Update status page
## 4.7.3 (2023-08-25)
### Bug Fixes
- Fix commands
## 4.7.2 (2023-08-05)
### Bug Fixes
- Fix `translate` command
## 4.7.1 (2023-08-01)
### Bug Fixes
- Optimize `search` command
- Add aliases for commands
## 4.7.0 (2023-06-08)
### New Features
- Add `OPENAI_COMPLETION_STOP_SEQUENCES` environment variable
## 4.6.0 (2023-05-03)
### New Features
- Support `gpt-4` model
## 4.5.0 (2023-04-27)
### New Features
- Support `zh_CN` locale
## 4.4.4 (2023-03-21)
### Bug Fixes
- Fix default value of `APP_MAX_GROUPS` environment variable
- Fix default value of `APP_MAX_USERS` environment variable
## 4.4.3 (2023-03-11)
### Bug Fixes
- Fix wording of `doc` and `report` commands
## 4.4.2 (2023-03-11)
### Bug Fixes
- Add `ERROR_MESSAGE_DISABLED` environment variable
- Deprecate `ERROR_TIMEOUT_DISABLED` environment variable
## 4.4.1 (2023-03-10)
### Bug Fixes
- Add default max prompt tokens for chat completion api
## 4.4.0 (2023-03-08)
### New Features
- Support snapshots of `gpt-3.5-turbo` model
## 4.3.0 (2023-03-08)
### New Features
- Add `VERCEL_TEAM_ID` environment variable
## 4.2.2 (2023-03-08)
### Bug Fixes
- Optimize error handling
## 4.2.1 (2023-03-07)
### Bug Fixes
- Fix `add-mark` util
## 4.2.0 (2023-03-05)
### New Features
- Add `APP_INIT_PROMPT` environment variable
## 4.1.3 (2023-03-05)
### Bug Fixes
- Fix `add-mark` util
## 4.1.2 (2023-03-05)
### Bug Fixes
- Update `add-mark` util
## 4.1.1 (2023-03-05)
### Bug Fixes
- End text with dot
## 4.1.0 (2023-03-05)
- Support `whisper-1` model
- Add `opencc` text converter
- Store display name and group name to storage
## 4.0.4 (2023-03-03)
### Bug Fixes
- Optimize `search` command
## 4.0.3 (2023-03-03)
### Bug Fixes
- Optimize `search` and `draw` commands
## 4.0.2 (2023-03-02)
### Bug Fixes
- Fix prompt messages
## 4.0.1 (2023-03-02)
### Bug Fixes
- Fix `enquire` command
## 4.0.0 (2023-03-02)
### New Features
- Support `gpt-3.5-turbo` model
### Bug Fixes
- Rename `APP_MAX_PROMPT_SENTENCES` environment variable to `APP_MAX_PROMPT_MESSAGES`
## 3.7.0 (2023-02-26)
### New Features
- Add demo for `search` command
- Add `SERPAPI_LOCATION` environment variable
- Add `SERPAPI_LANG` environment variable
## 3.6.0 (2023-02-26)
### New Features
- Add `APP_API_TIMEOUT` environment variable
- Add `APP_MAX_PROMPT_SENTENCES` environment variable
- Add `APP_MAX_PROMPT_TOKENS` environment variable
## 3.5.0 (2023-02-26)
### New Features
- Rename `HUMAN_BACKGROUND` environment variable to `HUMAN_INIT_PROMPT`
- Rename `BOT_BACKGROUND` environment variable to `BOT_INIT_PROMPT`
## 3.4.1 (2023-02-25)
### Bug Fixes
- Fix default bot name
## 3.4.0 (2023-02-24)
### New Features
- Add `info` endpoint
## 3.3.5 (2023-02-24)
### Bug Fixes
- Fix prompt wording
## 3.3.4 (2023-02-24)
### Bug Fixes
- Fix prompt wording
## 3.3.3 (2023-02-24)
### Bug Fixes
- Fix tests
## 3.3.2 (2023-02-23)
### Bug Fixes
- Fix prompt wording
## 3.3.1 (2023-02-23)
### Bug Fixes
- Fix prompt wording
## 3.3.0 (2023-02-23)
### New Features
- Add `BOT_TONE` environment variable
## 3.2.1 (2023-02-22)
### Bug Fixes
- Fix timeout wording
## 3.2.0 (2023-02-22)
### New Features
- Add `HUMAN_NAME` environment variable
- Add `HUMAN_BACKGROUND` environment variable
- Add `BOT_BACKGROUND` environment variable
## 3.1.0 (2023-02-21)
### New Features
- Implement `forget` command
## 3.0.0 (2023-02-18)
### New Features
- Implement `search` command
## 2.5.1 (2023-02-18)
### New Features
- Rename `BOT_TIMEOUT_DISABLED` environment variable to `ERROR_TIMEOUT_DISABLED`
## 2.5.0 (2023-02-18)
### New Features
- Add `BOT_TIMEOUT_DISABLED` environment variable
## 2.4.0 (2023-02-17)
### New Features
- Add `BOT_DEACTIVATED` environment variable
## 2.3.0 (2023-02-11)
### New Features
- Add `VERCEL_TIMEOUT` environment variable
- Add `OPENAI_TIMEOUT` environment variable
- Add `LINE_TIMEOUT` environment variable
## 2.2.0 (2023-02-04)
### New Features
- Implement `retry` command
## 2.1.4 (2023-01-15)
### Bug Fixes
- Ignore non-text message events
## 2.1.3 (2023-01-15)
### Bug Fixes
- Add command aliases
## 2.1.2 (2023-01-15)
### Bug Fixes
- Add command aliases
## 2.1.1 (2023-01-14)
### Bug Fixes
- Fix `enquire` command
## 2.1.0 (2023-01-11)
### New Features
- Add `VERCEL_PROJECT_NAME` environment variable
## 2.0.1 (2023-01-11)
### Bug Fixes
- Add logs for webhook endpoint
## 2.0.0 (2023-01-10)
### New Features
- Implement `sum` command
- Implement `analyze` command
- Implement `translate` command
- Add `BOT_NAME` environment variable
- Add `APP_MAX_GROUPS` environment variable
- Add `APP_MAX_USERS` environment variable
### Bug Fixes
- Remove `SETTING_AI_NAME` environment variable
- Remove `SETTING_AI_ACTIVATED` environment variable
- Refactor `storage` module
- Refactor `prompt` module
- Refactor `history` module
## 1.12.4 (2022-12-31)
### Bug Fixes
- Rename `chat` command to `talk`
## 1.12.3 (2022-12-31)
### Bug Fixes
- Update command template
## 1.12.2 (2022-12-30)
### Bug Fixes
- Fix summarize request wording
## 1.12.1 (2022-12-30)
### Bug Fixes
- Handle non-text messages
## 1.12.0 (2022-12-30)
### New Features
- Implement `summarize` command
## 1.11.3 (2022-12-29)
### Bug Fixes
- Add command aliases
## 1.11.2 (2022-12-26)
### Bug Fixes
- Handle error messages in every commands
## 1.11.1 (2022-12-26)
### Bug Fixes
- Trim AI Name when sending prompt
## 1.11.0 (2022-12-26)
### New Features
- Implement `call` command
- Add `SETTING_AI_NAME` environment variable
- Add `SETTING_AI_ACTIVATED` environment variable
### Bug Fixes
- Remove `APP_STORAGE` environment variable
## 1.10.2 (2022-12-25)
### Bug Fixes
- Rename methods
## 1.10.1 (2022-12-25)
### Bug Fixes
- Fix wording of commands
## 1.10.0 (2022-12-25)
### New Features
- Add `OPENAI_IMAGE_GENERATION_SIZE` environment variable
### Bug Fixes
- Remove `SETTING_IMAGE_GENERATION_SIZE` setting
## 1.9.1 (2022-12-24)
### Bug Fixes
- Rename functions and variables
## 1.9.0 (2022-12-24)
### New Features
- Implement dynamic configuration
- Implement `configure` command
- Add `SETTING_IMAGE_GENERATION_SIZE` setting
## 1.8.0 (2022-12-24)
### New Features
- Implement `doc` command
### Bug Fixes
- Rename `settings` command to `command`
## 1.7.1 (2022-12-23)
### Bug Fixes
- Fix wording of commands
## 1.7.0 (2022-12-23)
### New Features
- Implement localization
- Implement command aliases
### Bug Fixes
- Rename `OPENAI_COMPLETION_INIT_LANG` environment variable to `APP_LANG`
## 1.6.0 (2022-12-23)
### New Features
- Implement `settings` command
### Bug Fixes
- Rename `chat --auto-reply off` command to `deactivate`
- Rename `chat --auto-reply on` command to `activate`
- Rename `CHAT_AUTO_REPLY` setting to `AI_ACTIVATED`
## 1.5.0 (2022-12-22)
### New Features
- Implement `continue` command with quick reply feature
### Bug Fixes
- Change default max completion tokens to 160
- Change default max prompt messages to 16
## 1.4.6 (2022-12-20)
### Bug Fixes
- Add comments
## 1.4.5 (2022-12-19)
### Bug Fixes
- Add `ja` initial language
- Add `ai` alias for `chat` command
## 1.4.4 (2022-12-18)
### Bug Fixes
- Rename `AI_AUTO_REPLY` setting to `CHAT_AUTO_REPLY`
- Fix case sensitivity of command issues
## 1.4.3 (2022-12-18)
### Bug Fixes
- Rename `ai` command to `chat`
- Rename `ai --auto-reply off` command to `chat --auto-reply off`
- Rename `ai --auto-reply on` command to `chat --auto-reply on`
- Rename `image` command to `draw`
## 1.4.2 (2022-12-18)
### Bug Fixes
- Refactor commands
## 1.4.1 (2022-12-18)
### Bug Fixes
- Refactor tests
## 1.4.0 (2022-12-18)
### New Features
- Implement `image` command
## 1.3.1 (2022-12-18)
### Bug Fixes
- Rename `VERCEL_WEBHOOK_URL` environment variable to `VERCEL_DEPLOY_HOOK_URL`
## 1.3.0 (2022-12-18)
### New Features
- Implement custom webhook path
- Add `APP_WEBHOOK_PATH` environment variable
## 1.2.1 (2022-12-18)
### Bug Fixes
- Refactor main functions
## 1.2.0 (2022-12-17)
### New Features
- Implement `deploy` command
- Add `VERCEL_WEBHOOK_URL` environment variable
## 1.1.3 (2022-12-17)
### Bug Fixes
- Fix storage module
- Fix `ai --auto-reply off` command
- Fix `ai --auto-reply on` command
## 1.1.2 (2022-12-16)
### Bug Fixes
- Refactor utility functions
## 1.1.1 (2022-12-16)
### Bug Fixes
- Rename `VERCEL_API_KEY` environment variable to `VERCEL_ACCESS_TOKEN`
- Rename `LINE_API_KEY` environment variable to `LINE_CHANNEL_ACCESS_TOKEN`
- Rename `LINE_API_SECRET` environment variable to `LINE_CHANNEL_SECRET`
## 1.1.0 (2022-12-16)
### New Features
- Implement `version` command
- Implement `ai` command
- Implement `ai --auto-reply off` command
- Implement `ai --auto-reply on` command
- Add Vercel API module
- Add `VERCEL_API_KEY` environment variable
- Add `LINE_API_SECRET` environment variable
### Bug Fixes
- Fix timeout issues
## 1.0.0 (2022-12-11)
### New Features
- Implement chat feature
- Add OpenAI API module
- Add LINE API module
================================================
FILE: Dockerfile
================================================
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm ci --only=production
CMD [ "npm", "start" ]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 Memo Chou
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# GPT AI Assistant
[](LICENSE) [](https://GitHub.com/memochou1993/gpt-ai-assistant/releases/)
GPT AI Assistant is an application that is implemented using the OpenAI API and LINE Messaging API. Through the installation process, you can start chatting with your own AI assistant using the LINE mobile app.
## News
- 2024-07-10: The `4.9` version now support `gpt-4o` OpenAI model. :fire:
- 2023-05-03: The `4.6` version now support `gpt-4` OpenAI model.
- 2023-03-05: The `4.1` version now support the audio message of LINE and `whisper-1` OpenAI model.
- 2023-03-02: The `4.0` version now support `gpt-3.5-turbo` OpenAI model.
## Documentations
- 中文
- English
## Credits
- [jayer95](https://github.com/jayer95) - Debugging and testing
- [kkdai](https://github.com/kkdai) - Idea of `sum` command
- [Dayu0815](https://github.com/Dayu0815) - Idea of `search` command
- [mics8128](https://github.com/mics8128) - Implementing new features
- [myh-st](https://github.com/myh-st) - Implementing new features
- [Jakevin](https://github.com/Jakevin) - Implementing new features
- [cdcd72](https://github.com/cdcd72) - Implementing new features
- [All other contributors](https://github.com/memochou1993/gpt-ai-assistant/graphs/contributors)
## Contact
If there is any question, please contact me at memochou1993@gmail.com. Thank you.
## Changelog
Detailed changes for each release are documented in the [release notes](https://github.com/memochou1993/gpt-ai-assistant/releases).
## License
[MIT](LICENSE)
================================================
FILE: api/index.js
================================================
import express from 'express';
import { handleEvents, printPrompts } from '../app/index.js';
import config from '../config/index.js';
import { validateLineSignature } from '../middleware/index.js';
import storage from '../storage/index.js';
import { fetchVersion, getVersion } from '../utils/index.js';
const app = express();
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
},
}));
app.get('/', async (req, res) => {
if (config.APP_URL) {
res.redirect(config.APP_URL);
return;
}
const currentVersion = getVersion();
const latestVersion = await fetchVersion();
res.status(200).send({ status: 'OK', currentVersion, latestVersion });
});
app.post(config.APP_WEBHOOK_PATH, validateLineSignature, async (req, res) => {
try {
await storage.initialize();
await handleEvents(req.body.events);
res.sendStatus(200);
} catch (err) {
console.error(err.message);
res.sendStatus(500);
}
if (config.APP_DEBUG) printPrompts();
});
if (config.APP_PORT) {
app.listen(config.APP_PORT);
}
export default app;
================================================
FILE: app/actions/action.js
================================================
class Action {
type;
}
export default Action;
================================================
FILE: app/actions/index.js
================================================
import Action from './action.js';
import MessageAction from './message.js';
export {
Action,
MessageAction,
};
================================================
FILE: app/actions/message.js
================================================
import { ACTION_TYPE_MESSAGE } from '../../services/line.js';
import Action from './action.js';
class MessageAction extends Action {
type = ACTION_TYPE_MESSAGE;
label;
text;
constructor({
label,
text,
}) {
super();
this.label = label;
this.text = text;
}
}
export default MessageAction;
================================================
FILE: app/app.js
================================================
import { replyMessage } from '../utils/index.js';
import {
activateHandler,
commandHandler,
continueHandler,
deactivateHandler,
deployHandler,
docHandler,
drawHandler,
forgetHandler,
enquireHandler,
reportHandler,
retryHandler,
searchHandler,
talkHandler,
versionHandler,
} from './handlers/index.js';
import Context from './context.js';
import Event from './models/event.js';
/**
* @param {Context} context
* @returns {Promise}
*/
const handleContext = async (context) => (
activateHandler(context)
|| commandHandler(context)
|| continueHandler(context)
|| deactivateHandler(context)
|| deployHandler(context)
|| docHandler(context)
|| drawHandler(context)
|| forgetHandler(context)
|| enquireHandler(context)
|| reportHandler(context)
|| retryHandler(context)
|| searchHandler(context)
|| versionHandler(context)
|| talkHandler(context)
|| context
);
const handleEvents = async (events = []) => (
(Promise.all(
(await Promise.all(
(await Promise.all(
events
.map((event) => new Event(event))
.filter((event) => event.isMessage)
.filter((event) => event.isText || event.isAudio || event.isImage)
.map((event) => new Context(event))
.map((context) => context.initialize()),
))
.map((context) => (context.error ? context : handleContext(context))),
))
.filter((context) => context.messages.length > 0)
.map((context) => replyMessage(context)),
))
);
export default handleEvents;
================================================
FILE: app/commands/analyze-analyze.js
================================================
import { TYPE_ANALYZE } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_ANALYZE,
label: t('__COMMAND_ANALYZE_ANALYZE_LABEL'),
text: t('__COMMAND_ANALYZE_ANALYZE_TEXT'),
prompt: t('__COMMAND_ANALYZE_ANALYZE_PROMPT'),
aliases: [
'/analyze',
'Analyze',
],
});
================================================
FILE: app/commands/analyze-literarily.js
================================================
import { TYPE_ANALYZE } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_ANALYZE,
label: t('__COMMAND_ANALYZE_LITERARILY_LABEL'),
text: t('__COMMAND_ANALYZE_LITERARILY_TEXT'),
prompt: t('__COMMAND_ANALYZE_LITERARILY_PROMPT'),
aliases: [
'/analyze-literarily',
'Analyze literarily',
],
});
================================================
FILE: app/commands/analyze-mathematically.js
================================================
import { TYPE_ANALYZE } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_ANALYZE,
label: t('__COMMAND_ANALYZE_MATHEMATICALLY_LABEL'),
text: t('__COMMAND_ANALYZE_MATHEMATICALLY_TEXT'),
prompt: t('__COMMAND_ANALYZE_MATHEMATICALLY_PROMPT'),
aliases: [
'/analyze-mathematically',
'Analyze mathematically',
],
});
================================================
FILE: app/commands/analyze-numerologically.js
================================================
import { TYPE_ANALYZE } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_ANALYZE,
label: t('__COMMAND_ANALYZE_NUMEROLOGICALLY_LABEL'),
text: t('__COMMAND_ANALYZE_NUMEROLOGICALLY_TEXT'),
prompt: t('__COMMAND_ANALYZE_NUMEROLOGICALLY_PROMPT'),
aliases: [
'/analyze-numerologically',
'Analyze numerologically',
],
});
================================================
FILE: app/commands/analyze-philosophically.js
================================================
import { TYPE_ANALYZE } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_ANALYZE,
label: t('__COMMAND_ANALYZE_PHILOSOPHICALLY_LABEL'),
text: t('__COMMAND_ANALYZE_PHILOSOPHICALLY_TEXT'),
prompt: t('__COMMAND_ANALYZE_PHILOSOPHICALLY_PROMPT'),
aliases: [
'/analyze-philosophically',
'Analyze philosophically',
],
});
================================================
FILE: app/commands/analyze-psychologically.js
================================================
import { TYPE_ANALYZE } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_ANALYZE,
label: t('__COMMAND_ANALYZE_PSYCHOLOGICALLY_LABEL'),
text: t('__COMMAND_ANALYZE_PSYCHOLOGICALLY_TEXT'),
prompt: t('__COMMAND_ANALYZE_PSYCHOLOGICALLY_PROMPT'),
aliases: [
'/analyze-psychologically',
'Analyze psychologically',
],
});
================================================
FILE: app/commands/bot-activate.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_BOT_ACTIVATE_LABEL'),
text: t('__COMMAND_BOT_ACTIVATE_TEXT'),
reply: t('__COMMAND_BOT_ACTIVATE_REPLY'),
aliases: [
...t('__COMMAND_BOT_ACTIVATE_ALIASES'),
'/activate',
'Activate',
],
});
================================================
FILE: app/commands/bot-continue.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_BOT_CONTINUE_LABEL'),
text: t('__COMMAND_BOT_CONTINUE_TEXT'),
aliases: [
...t('__COMMAND_BOT_CONTINUE_ALIASES'),
'/continue',
'Continue',
],
});
================================================
FILE: app/commands/bot-deactivate.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_BOT_DEACTIVATE_LABEL'),
text: t('__COMMAND_BOT_DEACTIVATE_TEXT'),
reply: t('__COMMAND_BOT_DEACTIVATE_REPLY'),
aliases: [
...t('__COMMAND_BOT_DEACTIVATE_ALIASES'),
'/deactivate',
'Deactivate',
],
});
================================================
FILE: app/commands/bot-draw-demo.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_BOT_DRAW_DEMO_LABEL'),
text: t('__COMMAND_BOT_DRAW_DEMO_TEXT'),
});
================================================
FILE: app/commands/bot-draw.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_BOT_DRAW_LABEL'),
text: t('__COMMAND_BOT_DRAW_TEXT'),
prompt: t('__COMMAND_BOT_DRAW_PROMPT'),
aliases: [
...t('__COMMAND_BOT_DRAW_ALIASES'),
'/draw',
'Draw',
],
});
================================================
FILE: app/commands/bot-forget.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_BOT_FORGET_LABEL'),
text: t('__COMMAND_BOT_FORGET_TEXT'),
reply: t('__COMMAND_BOT_FORGET_REPLY'),
aliases: [
...t('__COMMAND_BOT_FORGET_ALIASES'),
'/forget',
'Forget',
],
});
================================================
FILE: app/commands/bot-retry.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_BOT_RETRY_LABEL'),
text: t('__COMMAND_BOT_RETRY_TEXT'),
});
================================================
FILE: app/commands/bot-search-demo.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_BOT_SEARCH_DEMO_LABEL'),
text: t('__COMMAND_BOT_SEARCH_DEMO_TEXT'),
});
================================================
FILE: app/commands/bot-search.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_BOT_SEARCH_LABEL'),
text: t('__COMMAND_BOT_SEARCH_TEXT'),
aliases: [
...t('__COMMAND_BOT_SEARCH_ALIASES'),
'/search',
'Search',
],
});
================================================
FILE: app/commands/bot-summon-demo.js
================================================
import config from '../../config/index.js';
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_BOT_SUMMON_DEMO_LABEL'),
text: `${config.BOT_NAME} ${t('__COMMAND_BOT_SUMMON_DEMO_TEXT')}`,
});
================================================
FILE: app/commands/bot-talk-demo.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_BOT_TALK_DEMO_LABEL'),
text: t('__COMMAND_BOT_TALK_DEMO_TEXT'),
});
================================================
FILE: app/commands/bot-talk.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_BOT_TALK_LABEL'),
text: t('__COMMAND_BOT_TALK_TEXT'),
aliases: [
...t('__COMMAND_BOT_TALK_ALIASES'),
'/talk',
'Talk',
],
});
================================================
FILE: app/commands/command.js
================================================
class Command {
type;
label;
text;
reply;
prompt;
aliases;
constructor({
type,
label,
text,
reply = '',
prompt = '',
aliases = [],
}) {
this.type = type;
this.label = label;
this.text = text;
this.reply = reply;
this.prompt = prompt;
this.aliases = aliases;
}
}
export default Command;
================================================
FILE: app/commands/index.js
================================================
import { TYPE_ANALYZE, TYPE_SUM, TYPE_TRANSLATE } from '../../constants/command.js';
import COMMAND_ANALYZE_ANALYZE from './analyze-analyze.js';
import COMMAND_ANALYZE_LITERARILY from './analyze-literarily.js';
import COMMAND_ANALYZE_MATHEMATICALLY from './analyze-mathematically.js';
import COMMAND_ANALYZE_NUMEROLOGICALLY from './analyze-numerologically.js';
import COMMAND_ANALYZE_PHILOSOPHICALLY from './analyze-philosophically.js';
import COMMAND_ANALYZE_PSYCHOLOGICALLY from './analyze-psychologically.js';
import COMMAND_BOT_ACTIVATE from './bot-activate.js';
import COMMAND_BOT_CONTINUE from './bot-continue.js';
import COMMAND_BOT_DEACTIVATE from './bot-deactivate.js';
import COMMAND_BOT_DRAW_DEMO from './bot-draw-demo.js';
import COMMAND_BOT_DRAW from './bot-draw.js';
import COMMAND_BOT_FORGET from './bot-forget.js';
import COMMAND_BOT_RETRY from './bot-retry.js';
import COMMAND_BOT_SEARCH from './bot-search.js';
import COMMAND_BOT_SEARCH_DEMO from './bot-search-demo.js';
import COMMAND_BOT_SUMMON_DEMO from './bot-summon-demo.js';
import COMMAND_BOT_TALK_DEMO from './bot-talk-demo.js';
import COMMAND_BOT_TALK from './bot-talk.js';
import Command from './command.js';
import COMMAND_SUM_ADVISE from './sum-advise.js';
import COMMAND_SUM_APOLOGIZE from './sum-apologize.js';
import COMMAND_SUM_BLAME from './sum-blame.js';
import COMMAND_SUM_COMFORT from './sum-comfort.js';
import COMMAND_SUM_COMPLAIN from './sum-complain.js';
import COMMAND_SUM_ENCOURAGE from './sum-encourage.js';
import COMMAND_SUM_LAUGH from './sum-laugh.js';
import COMMAND_SUM_SUM from './sum-sum.js';
import COMMAND_SYS_COMMAND from './sys-command.js';
import COMMAND_SYS_DEPLOY from './sys-deploy.js';
import COMMAND_SYS_DOC from './sys-doc.js';
import COMMAND_SYS_REPORT from './sys-report.js';
import COMMAND_SYS_VERSION from './sys-version.js';
import COMMAND_TRANSLATE_TO_EN from './translate-to-en.js';
import COMMAND_TRANSLATE_TO_JA from './translate-to-ja.js';
export const ALL_COMMANDS = [
COMMAND_ANALYZE_ANALYZE,
COMMAND_ANALYZE_LITERARILY,
COMMAND_ANALYZE_MATHEMATICALLY,
COMMAND_ANALYZE_NUMEROLOGICALLY,
COMMAND_ANALYZE_PHILOSOPHICALLY,
COMMAND_ANALYZE_PSYCHOLOGICALLY,
COMMAND_BOT_ACTIVATE,
COMMAND_BOT_CONTINUE,
COMMAND_BOT_DEACTIVATE,
COMMAND_BOT_DRAW_DEMO,
COMMAND_BOT_DRAW,
COMMAND_BOT_FORGET,
COMMAND_BOT_RETRY,
COMMAND_BOT_SEARCH,
COMMAND_BOT_SUMMON_DEMO,
COMMAND_BOT_TALK_DEMO,
COMMAND_BOT_TALK,
COMMAND_SUM_ADVISE,
COMMAND_SUM_APOLOGIZE,
COMMAND_SUM_BLAME,
COMMAND_SUM_COMFORT,
COMMAND_SUM_COMPLAIN,
COMMAND_SUM_ENCOURAGE,
COMMAND_SUM_LAUGH,
COMMAND_SUM_SUM,
COMMAND_SYS_COMMAND,
COMMAND_SYS_DEPLOY,
COMMAND_SYS_DOC,
COMMAND_SYS_REPORT,
COMMAND_SYS_VERSION,
COMMAND_TRANSLATE_TO_EN,
COMMAND_TRANSLATE_TO_JA,
];
export const INFO_COMMANDS = [
COMMAND_SYS_VERSION,
COMMAND_SYS_DOC,
COMMAND_SYS_REPORT,
];
export const GENERAL_COMMANDS = [
COMMAND_SYS_COMMAND,
COMMAND_BOT_SUMMON_DEMO,
COMMAND_BOT_TALK_DEMO,
COMMAND_BOT_DRAW_DEMO,
COMMAND_TRANSLATE_TO_EN,
COMMAND_TRANSLATE_TO_JA,
COMMAND_BOT_SEARCH_DEMO,
COMMAND_BOT_FORGET,
COMMAND_SUM_SUM,
COMMAND_ANALYZE_ANALYZE,
];
export const ENQUIRE_COMMANDS = ALL_COMMANDS.filter(({ type }) => (
type === TYPE_SUM
|| type === TYPE_ANALYZE
|| type === TYPE_TRANSLATE
));
export {
COMMAND_ANALYZE_ANALYZE,
COMMAND_ANALYZE_LITERARILY,
COMMAND_ANALYZE_MATHEMATICALLY,
COMMAND_ANALYZE_NUMEROLOGICALLY,
COMMAND_ANALYZE_PHILOSOPHICALLY,
COMMAND_ANALYZE_PSYCHOLOGICALLY,
COMMAND_BOT_ACTIVATE,
COMMAND_BOT_CONTINUE,
COMMAND_BOT_DEACTIVATE,
COMMAND_BOT_DRAW_DEMO,
COMMAND_BOT_DRAW,
COMMAND_BOT_FORGET,
COMMAND_BOT_RETRY,
COMMAND_BOT_SEARCH,
COMMAND_BOT_SUMMON_DEMO,
COMMAND_BOT_TALK_DEMO,
COMMAND_BOT_TALK,
Command,
COMMAND_SUM_ADVISE,
COMMAND_SUM_APOLOGIZE,
COMMAND_SUM_BLAME,
COMMAND_SUM_COMFORT,
COMMAND_SUM_COMPLAIN,
COMMAND_SUM_ENCOURAGE,
COMMAND_SUM_LAUGH,
COMMAND_SUM_SUM,
COMMAND_SYS_COMMAND,
COMMAND_SYS_DEPLOY,
COMMAND_SYS_DOC,
COMMAND_SYS_REPORT,
COMMAND_SYS_VERSION,
COMMAND_TRANSLATE_TO_EN,
COMMAND_TRANSLATE_TO_JA,
};
================================================
FILE: app/commands/sum-advise.js
================================================
import { TYPE_SUM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SUM,
label: t('__COMMAND_SUM_ADVISE_LABEL'),
text: t('__COMMAND_SUM_ADVISE_TEXT'),
prompt: t('__COMMAND_SUM_ADVISE_PROMPT'),
aliases: [
'/advise',
'Advise',
],
});
================================================
FILE: app/commands/sum-apologize.js
================================================
import { TYPE_SUM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SUM,
label: t('__COMMAND_SUM_APOLOGIZE_LABEL'),
text: t('__COMMAND_SUM_APOLOGIZE_TEXT'),
prompt: t('__COMMAND_SUM_APOLOGIZE_PROMPT'),
aliases: [
'/apologize',
'Apologize',
],
});
================================================
FILE: app/commands/sum-blame.js
================================================
import { TYPE_SUM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SUM,
label: t('__COMMAND_SUM_BLAME_LABEL'),
text: t('__COMMAND_SUM_BLAME_TEXT'),
prompt: t('__COMMAND_SUM_BLAME_PROMPT'),
aliases: [
'/blame',
'Blame',
],
});
================================================
FILE: app/commands/sum-comfort.js
================================================
import { TYPE_SUM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SUM,
label: t('__COMMAND_SUM_COMFORT_LABEL'),
text: t('__COMMAND_SUM_COMFORT_TEXT'),
prompt: t('__COMMAND_SUM_COMFORT_PROMPT'),
aliases: [
'/comfort',
'Comfort',
],
});
================================================
FILE: app/commands/sum-complain.js
================================================
import { TYPE_SUM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SUM,
label: t('__COMMAND_SUM_COMPLAIN_LABEL'),
text: t('__COMMAND_SUM_COMPLAIN_TEXT'),
prompt: t('__COMMAND_SUM_COMPLAIN_PROMPT'),
aliases: [
'/complain',
'Complain',
],
});
================================================
FILE: app/commands/sum-encourage.js
================================================
import { TYPE_SUM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SUM,
label: t('__COMMAND_SUM_ENCOURAGE_LABEL'),
text: t('__COMMAND_SUM_ENCOURAGE_TEXT'),
prompt: t('__COMMAND_SUM_ENCOURAGE_PROMPT'),
aliases: [
'/encourage',
'Encourage',
],
});
================================================
FILE: app/commands/sum-laugh.js
================================================
import { TYPE_SUM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SUM,
label: t('__COMMAND_SUM_LAUGH_LABEL'),
text: t('__COMMAND_SUM_LAUGH_TEXT'),
prompt: t('__COMMAND_SUM_LAUGH_PROMPT'),
aliases: [
'/laugh',
'Laugh',
],
});
================================================
FILE: app/commands/sum-sum.js
================================================
import { TYPE_SUM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SUM,
label: t('__COMMAND_SUM_SUM_LABEL'),
text: t('__COMMAND_SUM_SUM_TEXT'),
prompt: t('__COMMAND_SUM_SUM_PROMPT'),
aliases: [
'/sum',
'Sum',
],
});
================================================
FILE: app/commands/sys-command.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_SYS_COMMAND_LABEL'),
text: t('__COMMAND_SYS_COMMAND_TEXT'),
aliases: [
'/command',
'Command',
],
});
================================================
FILE: app/commands/sys-deploy.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_SYS_DEPLOY_LABEL'),
text: t('__COMMAND_SYS_DEPLOY_TEXT'),
reply: t('__COMMAND_SYS_DEPLOY_REPLY'),
aliases: [
'/restart',
'Restart',
],
});
================================================
FILE: app/commands/sys-doc.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_SYS_DOC_LABEL'),
text: t('__COMMAND_SYS_DOC_TEXT'),
reply: t('__COMMAND_SYS_DOC_REPLY'),
aliases: [
'/doc',
'Doc',
],
});
================================================
FILE: app/commands/sys-report.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_SYS_REPORT_LABEL'),
text: t('__COMMAND_SYS_REPORT_TEXT'),
aliases: [
'/report',
'Report',
],
});
================================================
FILE: app/commands/sys-version.js
================================================
import { TYPE_SYSTEM } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_SYSTEM,
label: t('__COMMAND_SYS_VERSION_LABEL'),
text: t('__COMMAND_SYS_VERSION_TEXT'),
reply: t('__COMMAND_SYS_VERSION_REPLY'),
aliases: [
'/version',
'Version',
],
});
================================================
FILE: app/commands/translate-to-en.js
================================================
import { TYPE_TRANSLATE } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_TRANSLATE,
label: t('__COMMAND_TRANSLATE_TO_EN_LABEL'),
text: t('__COMMAND_TRANSLATE_TO_EN_TEXT'),
prompt: t('__COMMAND_TRANSLATE_TO_EN_PROMPT'),
aliases: [
'/translate-to-en',
'Translate to English',
'Translate to EN',
],
});
================================================
FILE: app/commands/translate-to-ja.js
================================================
import { TYPE_TRANSLATE } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import Command from './command.js';
export default new Command({
type: TYPE_TRANSLATE,
label: t('__COMMAND_TRANSLATE_TO_JA_LABEL'),
text: t('__COMMAND_TRANSLATE_TO_JA_TEXT'),
prompt: t('__COMMAND_TRANSLATE_TO_JA_PROMPT'),
aliases: [
'/translate-to-ja',
'Translate to Japanese',
'Translate to JA',
],
});
================================================
FILE: app/context.js
================================================
import { AxiosError } from 'axios';
import fs from 'fs';
import config from '../config/index.js';
import { t } from '../locales/index.js';
import {
MESSAGE_TYPE_IMAGE, MESSAGE_TYPE_TEXT, SOURCE_TYPE_GROUP, SOURCE_TYPE_USER,
} from '../services/line.js';
import {
addMark,
convertText,
fetchAudio,
fetchImage,
fetchGroup,
fetchUser,
generateTranscription,
} from '../utils/index.js';
import { Command, COMMAND_BOT_FORGET, COMMAND_BOT_RETRY } from './commands/index.js';
import { updateHistory } from './history/index.js';
import {
ImageMessage, Message, TemplateMessage, TextMessage,
} from './messages/index.js';
import { Bot, Event, Source } from './models/index.js';
import { getSources, setSources } from './repository/index.js';
class Context {
/**
* @type {Event}
*/
event;
/**
* @type {Source}
*/
source;
/**
* @type {string}
*/
transcription;
/**
* @type {Array}
*/
messages = [];
/**
* @param {Event} event
*/
constructor(event) {
this.event = event;
}
get id() {
if (this.event.isGroup) return this.event.source.groupId;
return this.event.source.userId;
}
/**
* @returns {string}
*/
get replyToken() {
return this.event.replyToken;
}
/**
* @returns {string}
*/
get groupId() {
return this.event.groupId;
}
/**
* @returns {string}
*/
get userId() {
return this.event.userId;
}
/**
* @returns {string}
*/
get trimmedText() {
if (this.event.isText) {
const text = this.event.text.replaceAll(' ', ' ').replace(config.BOT_NAME, '').trim();
return addMark(text);
}
if (this.event.isAudio) {
const text = this.transcription.replace(config.BOT_NAME, '').trim();
return addMark(text);
}
if (this.event.isImage) {
return this.transcription.trim();
}
return '?';
}
get hasBotName() {
if (this.event.isText) {
const text = this.event.text.replaceAll(' ', ' ').trim().toLowerCase();
return text.startsWith(config.BOT_NAME.toLowerCase());
}
if (this.event.isAudio) {
const text = this.transcription.toLowerCase();
return text.startsWith(config.BOT_NAME.toLowerCase());
}
if (this.event.isImage) {
const text = this.transcription.toLowerCase();
return text.startsWith(config.BOT_NAME.toLowerCase());
}
return false;
}
async initialize() {
try {
this.validate();
await this.register();
} catch (err) {
return this.pushError(err);
}
if (this.event.isAudio) {
try {
await this.transcribeAudio();
} catch (err) {
return this.pushError(err);
}
}
if (this.event.isImage) {
try {
await this.transcribeImage();
} catch (err) {
return this.pushError(err);
}
}
updateHistory(this.id, (history) => history.write(this.source.name, this.trimmedText));
return this;
}
/**
* @throws {Error}
*/
validate() {
const sources = getSources();
const groups = Object.values(sources).filter(({ type }) => type === SOURCE_TYPE_GROUP);
const users = Object.values(sources).filter(({ type }) => type === SOURCE_TYPE_USER);
if (this.event.isGroup && !sources[this.groupId] && groups.length >= config.APP_MAX_GROUPS) {
throw new Error(t('__ERROR_MAX_GROUPS_REACHED'));
}
if (!sources[this.userId] && users.length >= config.APP_MAX_USERS) {
throw new Error(t('__ERROR_MAX_USERS_REACHED'));
}
}
async register() {
const sources = getSources();
const newSources = {};
if (this.event.isGroup && !sources[this.groupId]) {
const { groupName } = await fetchGroup(this.groupId);
newSources[this.groupId] = new Source({
type: SOURCE_TYPE_GROUP,
name: groupName,
bot: new Bot({
isActivated: !config.BOT_DEACTIVATED,
}),
});
}
if (!sources[this.userId]) {
const { displayName } = await fetchUser(this.userId);
newSources[this.userId] = new Source({
type: SOURCE_TYPE_USER,
name: displayName,
bot: new Bot({
isActivated: !config.BOT_DEACTIVATED,
}),
});
}
Object.assign(sources, newSources);
if (Object.keys(newSources).length > 0) await setSources(sources);
this.source = new Source(sources[this.id]);
}
async transcribeAudio() {
const buffer = await fetchAudio(this.event.messageId);
const file = `/tmp/${this.event.messageId}.m4a`;
fs.writeFileSync(file, buffer);
const { text } = await generateTranscription({ file, buffer });
this.transcription = convertText(text);
}
async transcribeImage() {
const base64String = await fetchImage(this.event.messageId);
this.transcription = base64String;
}
/**
* @param {Object} param
* @param {string} param.text
* @param {Array} param.aliases
* @returns {boolean}
*/
hasCommand({
text,
aliases,
}) {
const content = this.trimmedText.toLowerCase();
if (aliases.some((alias) => content.startsWith(alias.toLowerCase()))) return true;
if (content.startsWith(text.toLowerCase())) return true;
return false;
}
/**
* @param {string} text
* @param {Array} actions
* @returns {Context}
*/
pushText(text, actions = []) {
if (!text) return this;
const message = new TextMessage({
type: MESSAGE_TYPE_TEXT,
text: convertText(text),
});
message.setQuickReply(actions);
this.messages.push(message);
return this;
}
/**
* @param {string} url
* @param {Array} actions
* @returns {Context}
*/
pushImage(url, actions = []) {
if (!url) return this;
const message = new ImageMessage({
type: MESSAGE_TYPE_IMAGE,
originalContentUrl: url,
previewImageUrl: url,
});
message.setQuickReply(actions);
this.messages.push(message);
return this;
}
/**
* @param {string} url
* @param {Array} buttons
* @param {Array} actions
* @returns {Context}
*/
pushTemplate(text, buttons = [], actions = []) {
if (!text) return this;
const message = new TemplateMessage({
text,
actions: buttons,
});
message.setQuickReply(actions);
this.messages.push(message);
return this;
}
/**
* @param {AxiosError} err
* @returns {Context}
*/
pushError(err) {
this.error = err;
console.log(this.error.message);
if (err.code === 'ECONNABORTED') {
if (config.ERROR_MESSAGE_DISABLED) return this;
return this.pushText(t('__ERROR_ECONNABORTED'), [COMMAND_BOT_RETRY, COMMAND_BOT_FORGET]);
}
if (err.response?.status >= 500) {
if (config.ERROR_MESSAGE_DISABLED) return this;
return this.pushText(t('__ERROR_UNKNOWN'), [COMMAND_BOT_RETRY, COMMAND_BOT_FORGET]);
}
if (err.config?.baseURL) this.pushText(`${err.config.method.toUpperCase()} ${err.config.baseURL}${err.config.url}`);
if (err.response) this.pushText(`Request failed with status code ${err.response.status}`);
this.pushText(err.message);
return this;
}
}
export default Context;
================================================
FILE: app/handlers/activate.js
================================================
import config from '../../config/index.js';
import { t } from '../../locales/index.js';
import { COMMAND_BOT_ACTIVATE, GENERAL_COMMANDS } from '../commands/index.js';
import Context from '../context.js';
import { updateHistory } from '../history/index.js';
import { updateSources } from '../repository/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => context.hasCommand(COMMAND_BOT_ACTIVATE);
/**
* @param {Context} context
* @returns {Context}
*/
const exec = (context) => check(context) && (
async () => {
updateHistory(context.id, (history) => history.erase());
if (!config.VERCEL_ACCESS_TOKEN) context.pushText(t('__ERROR_MISSING_ENV')('VERCEL_ACCESS_TOKEN'));
try {
await updateSources(context.id, (source) => {
source.bot.isActivated = true;
});
context.pushText(COMMAND_BOT_ACTIVATE.reply, GENERAL_COMMANDS);
} catch (err) {
context.pushError(err);
}
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/command.js
================================================
import {
COMMAND_BOT_ACTIVATE,
COMMAND_SYS_COMMAND,
COMMAND_BOT_DEACTIVATE,
GENERAL_COMMANDS,
INFO_COMMANDS,
} from '../commands/index.js';
import Context from '../context.js';
import { updateHistory } from '../history/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => context.hasCommand(COMMAND_SYS_COMMAND);
/**
* @param {Context} context
* @returns {Context}
*/
const exec = (context) => check(context) && (
async () => {
updateHistory(context.id, (history) => history.erase());
try {
const buttons = [...INFO_COMMANDS];
buttons.splice(2, 0, context.source.bot.isActivated ? COMMAND_BOT_DEACTIVATE : COMMAND_BOT_ACTIVATE);
context.pushTemplate(COMMAND_SYS_COMMAND.label, buttons, GENERAL_COMMANDS);
} catch (err) {
context.pushError(err);
}
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/continue.js
================================================
import { generateCompletion } from '../../utils/index.js';
import { ALL_COMMANDS, COMMAND_BOT_CONTINUE } from '../commands/index.js';
import Context from '../context.js';
import { updateHistory } from '../history/index.js';
import { getPrompt, setPrompt } from '../prompt/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => context.hasCommand(COMMAND_BOT_CONTINUE);
/**
* @param {Context} context
* @returns {Promise}
*/
const exec = (context) => check(context) && (
async () => {
updateHistory(context.id, (history) => history.erase());
const prompt = getPrompt(context.userId);
const { lastMessage } = prompt;
if (lastMessage.isEnquiring) prompt.erase();
try {
const { text, isFinishReasonStop } = await generateCompletion({ prompt });
prompt.patch(text);
if (lastMessage.isEnquiring && !isFinishReasonStop) prompt.write('', lastMessage.content);
setPrompt(context.userId, prompt);
if (!lastMessage.isEnquiring) updateHistory(context.id, (history) => history.patch(text));
const defaultActions = ALL_COMMANDS.filter(({ type }) => type === lastMessage.content);
const actions = isFinishReasonStop ? defaultActions : [COMMAND_BOT_CONTINUE];
context.pushText(text, actions);
} catch (err) {
context.pushError(err);
}
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/deactivate.js
================================================
import config from '../../config/index.js';
import { t } from '../../locales/index.js';
import { COMMAND_BOT_DEACTIVATE, GENERAL_COMMANDS } from '../commands/index.js';
import Context from '../context.js';
import { updateHistory } from '../history/index.js';
import { updateSources } from '../repository/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => context.hasCommand(COMMAND_BOT_DEACTIVATE);
/**
* @param {Context} context
* @returns {Promise}
*/
const exec = (context) => check(context) && (
async () => {
updateHistory(context.id, (history) => history.erase());
if (!config.VERCEL_ACCESS_TOKEN) context.pushText(t('__ERROR_MISSING_ENV')('VERCEL_ACCESS_TOKEN'));
try {
await updateSources(context.id, (source) => {
source.bot.isActivated = false;
});
context.pushText(COMMAND_BOT_DEACTIVATE.reply, GENERAL_COMMANDS);
} catch (err) {
context.pushError(err);
}
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/deploy.js
================================================
import config from '../../config/index.js';
import { t } from '../../locales/index.js';
import { deploy } from '../../services/vercel.js';
import { COMMAND_SYS_DEPLOY } from '../commands/index.js';
import Context from '../context.js';
import { updateHistory } from '../history/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => context.hasCommand(COMMAND_SYS_DEPLOY);
/**
* @param {Context} context
* @returns {Promise}
*/
const exec = (context) => check(context) && (
async () => {
updateHistory(context.id, (history) => history.erase());
if (!config.VERCEL_DEPLOY_HOOK_URL) context.pushText(t('__ERROR_MISSING_ENV')('VERCEL_DEPLOY_HOOK_URL'));
try {
await deploy();
context.pushText(COMMAND_SYS_DEPLOY.reply);
} catch (err) {
context.pushError(err);
}
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/doc.js
================================================
import { COMMAND_SYS_DOC, GENERAL_COMMANDS } from '../commands/index.js';
import Context from '../context.js';
import { updateHistory } from '../history/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => context.hasCommand(COMMAND_SYS_DOC);
/**
* @param {Context} context
* @returns {Promise}
*/
const exec = (context) => check(context) && (
async () => {
updateHistory(context.id, (history) => history.erase());
context.pushText('https://memochou1993.github.io/gpt-ai-assistant-docs/', GENERAL_COMMANDS);
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/draw.js
================================================
import config from '../../config/index.js';
import { MOCK_TEXT_OK } from '../../constants/mock.js';
import { ROLE_AI, ROLE_HUMAN } from '../../services/openai.js';
import { generateImage } from '../../utils/index.js';
import { COMMAND_BOT_DRAW } from '../commands/index.js';
import Context from '../context.js';
import { updateHistory } from '../history/index.js';
import { getPrompt, setPrompt } from '../prompt/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => context.hasCommand(COMMAND_BOT_DRAW);
/**
* @param {Context} context
* @returns {Promise}
*/
const exec = (context) => check(context) && (
async () => {
const prompt = getPrompt(context.userId);
prompt.write(ROLE_HUMAN, `${context.trimmedText}`).write(ROLE_AI);
try {
const trimmedText = context.trimmedText.replace(COMMAND_BOT_DRAW.text, '');
const { url } = await generateImage({ prompt: trimmedText });
prompt.patch(MOCK_TEXT_OK);
setPrompt(context.userId, prompt);
updateHistory(context.id, (history) => history.write(config.BOT_NAME, MOCK_TEXT_OK));
context.pushImage(url);
} catch (err) {
context.pushError(err);
}
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/enquire.js
================================================
import { TYPE_TRANSLATE } from '../../constants/command.js';
import { t } from '../../locales/index.js';
import { ROLE_AI, ROLE_HUMAN } from '../../services/openai.js';
import { generateCompletion, getCommand } from '../../utils/index.js';
import { ALL_COMMANDS, COMMAND_BOT_CONTINUE, ENQUIRE_COMMANDS } from '../commands/index.js';
import Context from '../context.js';
import { getHistory, updateHistory } from '../history/index.js';
import { getPrompt, setPrompt, Prompt } from '../prompt/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => (
[...ENQUIRE_COMMANDS]
.sort((a, b) => b.text.length - a.text.length)
.some((command) => context.hasCommand(command))
);
/**
* @param {Context} context
* @returns {Promise}
*/
const exec = (context) => check(context) && (
async () => {
updateHistory(context.id, (history) => history.erase());
const command = getCommand(context.trimmedText);
const history = getHistory(context.id);
if (!history.lastMessage) return context;
const reference = command.type === TYPE_TRANSLATE ? history.lastMessage.content : history.toString();
const content = `${command.prompt}\n${t('__COMPLETION_QUOTATION_MARK_OPENING')}\n${reference}\n${t('__COMPLETION_QUOTATION_MARK_CLOSING')}`;
const partial = (new Prompt()).write(ROLE_HUMAN, content);
const prompt = getPrompt(context.userId);
prompt.write(ROLE_HUMAN, content).write(ROLE_AI);
try {
const { text, isFinishReasonStop } = await generateCompletion({ prompt: partial });
prompt.patch(text);
if (!isFinishReasonStop) prompt.write('', command.type);
setPrompt(context.userId, prompt);
const defaultActions = ALL_COMMANDS.filter(({ type }) => type === command.type);
const actions = isFinishReasonStop ? defaultActions : [COMMAND_BOT_CONTINUE];
context.pushText(text, actions);
} catch (err) {
context.pushError(err);
}
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/forget.js
================================================
import { COMMAND_BOT_FORGET } from '../commands/index.js';
import Context from '../context.js';
import { removeHistory } from '../history/index.js';
import { removePrompt } from '../prompt/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => context.hasCommand(COMMAND_BOT_FORGET);
/**
* @param {Context} context
* @returns {Promise}
*/
const exec = (context) => check(context) && (
async () => {
removePrompt(context.userId);
removeHistory(context.userId);
context.pushText(COMMAND_BOT_FORGET.reply);
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/index.js
================================================
import activateHandler from './activate.js';
import commandHandler from './command.js';
import continueHandler from './continue.js';
import deactivateHandler from './deactivate.js';
import deployHandler from './deploy.js';
import docHandler from './doc.js';
import drawHandler from './draw.js';
import forgetHandler from './forget.js';
import enquireHandler from './enquire.js';
import reportHandler from './report.js';
import retryHandler from './retry.js';
import searchHandler from './search.js';
import talkHandler from './talk.js';
import versionHandler from './version.js';
export {
activateHandler,
commandHandler,
continueHandler,
deactivateHandler,
deployHandler,
docHandler,
drawHandler,
forgetHandler,
enquireHandler,
reportHandler,
retryHandler,
searchHandler,
talkHandler,
versionHandler,
};
================================================
FILE: app/handlers/report.js
================================================
import { COMMAND_SYS_REPORT, GENERAL_COMMANDS } from '../commands/index.js';
import Context from '../context.js';
import { updateHistory } from '../history/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => context.hasCommand(COMMAND_SYS_REPORT);
/**
* @param {Context} context
* @returns {Promise}
*/
const exec = (context) => check(context) && (
async () => {
updateHistory(context.id, (history) => history.erase());
context.pushText('https://github.com/memochou1993/gpt-ai-assistant/issues', GENERAL_COMMANDS);
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/retry.js
================================================
import config from '../../config/index.js';
import { ROLE_AI } from '../../services/openai.js';
import { generateCompletion } from '../../utils/index.js';
import { COMMAND_BOT_CONTINUE, COMMAND_BOT_RETRY, GENERAL_COMMANDS } from '../commands/index.js';
import Context from '../context.js';
import { updateHistory } from '../history/index.js';
import { getPrompt, setPrompt } from '../prompt/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => context.hasCommand(COMMAND_BOT_RETRY);
/**
* @param {Context} context
* @returns {Promise}
*/
const exec = (context) => check(context) && (
async () => {
updateHistory(context.id, (history) => history.erase());
const prompt = getPrompt(context.userId);
prompt.erase().write(ROLE_AI);
try {
const { text, isFinishReasonStop } = await generateCompletion({ prompt });
prompt.patch(text);
setPrompt(context.userId, prompt);
updateHistory(context.id, (history) => history.write(config.BOT_NAME, text));
const actions = isFinishReasonStop ? [] : [COMMAND_BOT_CONTINUE];
context.pushText(text, actions);
} catch (err) {
context.pushError(err);
}
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/search.js
================================================
import config from '../../config/index.js';
import { t } from '../../locales/index.js';
import { ROLE_AI, ROLE_HUMAN } from '../../services/openai.js';
import { fetchAnswer, generateCompletion } from '../../utils/index.js';
import { COMMAND_BOT_CONTINUE, COMMAND_BOT_SEARCH } from '../commands/index.js';
import Context from '../context.js';
import { updateHistory } from '../history/index.js';
import { getPrompt, setPrompt } from '../prompt/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => context.hasCommand(COMMAND_BOT_SEARCH);
/**
* @param {Context} context
* @returns {Promise}
*/
const exec = (context) => check(context) && (
async () => {
let trimmedText = context.trimmedText.replace(COMMAND_BOT_SEARCH.text, '');
const prompt = getPrompt(context.userId);
if (!config.SERPAPI_API_KEY) context.pushText(t('__ERROR_MISSING_ENV')('SERPAPI_API_KEY'));
try {
const { answer } = await fetchAnswer(trimmedText);
trimmedText = `${t('__COMPLETION_SEARCH')(answer || t('__COMPLETION_SEARCH_NOT_FOUND'), trimmedText)}`;
} catch (err) {
return context.pushError(err);
}
prompt.write(ROLE_HUMAN, `${trimmedText}`).write(ROLE_AI);
try {
const { text, isFinishReasonStop } = await generateCompletion({ prompt });
prompt.patch(text);
setPrompt(context.userId, prompt);
updateHistory(context.id, (history) => history.write(config.BOT_NAME, text));
const actions = isFinishReasonStop ? [] : [COMMAND_BOT_CONTINUE];
context.pushText(text, actions);
} catch (err) {
context.pushError(err);
}
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/talk.js
================================================
import config from '../../config/index.js';
import { t } from '../../locales/index.js';
import { ROLE_AI, ROLE_HUMAN } from '../../services/openai.js';
import { generateCompletion } from '../../utils/index.js';
import { COMMAND_BOT_CONTINUE, COMMAND_BOT_FORGET, COMMAND_BOT_TALK } from '../commands/index.js';
import Context from '../context.js';
import { updateHistory } from '../history/index.js';
import { getPrompt, setPrompt } from '../prompt/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => (
context.hasCommand(COMMAND_BOT_TALK)
|| context.hasBotName
|| context.source.bot.isActivated
);
/**
* @param {Context} context
* @returns {Promise}
*/
const exec = (context) => check(context) && (
async () => {
const prompt = getPrompt(context.userId);
try {
if (context.event.isText) {
prompt.write(ROLE_HUMAN, `${t('__COMPLETION_DEFAULT_AI_TONE')(config.BOT_TONE)}${context.trimmedText}`).write(ROLE_AI);
}
if (context.event.isImage) {
const { trimmedText } = context;
prompt.writeImage(ROLE_HUMAN, trimmedText).write(ROLE_AI);
}
const { text, isFinishReasonStop } = await generateCompletion({ prompt });
prompt.patch(text);
setPrompt(context.userId, prompt);
updateHistory(context.id, (history) => history.write(config.BOT_NAME, text));
const actions = isFinishReasonStop ? [COMMAND_BOT_FORGET] : [COMMAND_BOT_CONTINUE];
context.pushText(text, actions);
} catch (err) {
context.pushError(err);
}
return context;
}
)();
export default exec;
================================================
FILE: app/handlers/version.js
================================================
import { t } from '../../locales/index.js';
import { fetchVersion, getVersion } from '../../utils/index.js';
import { COMMAND_SYS_VERSION, GENERAL_COMMANDS } from '../commands/index.js';
import Context from '../context.js';
import { updateHistory } from '../history/index.js';
/**
* @param {Context} context
* @returns {boolean}
*/
const check = (context) => context.hasCommand(COMMAND_SYS_VERSION);
/**
* @param {Context} context
* @returns {Promise}
*/
const exec = (context) => check(context) && (
async () => {
updateHistory(context.id, (history) => history.erase());
const current = getVersion();
const latest = await fetchVersion();
const isLatest = current === latest;
const text = t('__COMMAND_SYS_VERSION_REPLY')(current, isLatest);
context.pushText(text, GENERAL_COMMANDS);
if (!isLatest) context.pushText(t('__MESSAGE_NEW_VERSION_AVAILABLE')(latest));
return context;
}
)();
export default exec;
================================================
FILE: app/history/history.js
================================================
import { encode } from 'gpt-3-encoder';
import config from '../../config/index.js';
import { t } from '../../locales/index.js';
import { addMark } from '../../utils/index.js';
import Message from './message.js';
const MAX_MESSAGES = config.APP_MAX_PROMPT_MESSAGES / 2;
const MAX_TOKENS = config.APP_MAX_PROMPT_TOKENS / 2;
class History {
messages = [];
/**
* @returns {Message}
*/
get lastMessage() {
return this.messages.length > 0 ? this.messages[this.messages.length - 1] : null;
}
get tokenCount() {
const encoded = encode(this.toString());
return encoded.length;
}
erase() {
if (this.messages.length > 0) {
this.messages.pop();
}
return this;
}
/**
* @param {string} role
* @param {string} content
*/
write(role, content) {
if (this.messages.length >= MAX_MESSAGES || this.tokenCount >= MAX_TOKENS) {
this.messages.shift();
}
this.messages.push(new Message({ role, content: addMark(content) }));
return this;
}
/**
* @param {string} role
* @param {string} content
*/
writeImage(role, content = '') {
const imageContent = [
{
type: 'text',
text: t('__COMPLETION_VISION'),
},
{
type: 'image',
image_url: {
url: content,
},
},
];
this.messages.push(new Message({ role, content: imageContent }));
return this;
}
/**
* @param {string} content
*/
patch(content) {
if (this.messages.length < 1) return;
this.messages[this.messages.length - 1].content += content;
}
toString() {
return this.messages.map((record) => record.toString()).join('\n');
}
}
export default History;
================================================
FILE: app/history/index.js
================================================
import History from './history.js';
const histories = new Map();
/**
* @param {string} contextId
* @returns {History}
*/
const getHistory = (contextId) => histories.get(contextId) || new History();
/**
* @param {string} contextId
* @param {History} history
* @returns {History}
*/
const setHistory = (contextId, history) => histories.set(contextId, history);
/**
* @param {string} contextId
* @param {function(History)} callback
*/
const updateHistory = (contextId, callback) => {
const history = getHistory(contextId);
callback(history);
setHistory(contextId, history);
};
/**
* @param {string} userId
*/
const removeHistory = (userId) => {
histories.delete(userId);
};
const printHistories = () => {
const messages = Array.from(histories.keys())
.filter((contextId) => getHistory(contextId).messages.length > 0)
.map((contextId) => `\n=== ${contextId.slice(0, 6)} ===\n\n${getHistory(contextId).toString()}\n`);
if (messages.length < 1) return;
console.info(messages.join(''));
};
export {
getHistory,
setHistory,
updateHistory,
removeHistory,
printHistories,
};
export default histories;
================================================
FILE: app/history/message.js
================================================
class Message {
role;
content;
constructor({
role,
content,
}) {
this.role = role;
this.content = content;
}
toString() {
return `${this.role}: ${this.content}`;
}
}
export default Message;
================================================
FILE: app/index.js
================================================
import handleEvents from './app.js';
import { printHistories } from './history/index.js';
import {
getPrompt, printPrompts, removePrompt, setPrompt,
} from './prompt/index.js';
export {
handleEvents,
printHistories,
getPrompt,
printPrompts,
removePrompt,
setPrompt,
};
================================================
FILE: app/messages/image.js
================================================
import { MESSAGE_TYPE_IMAGE } from '../../services/line.js';
import Message from './message.js';
class ImageMessage extends Message {
type = MESSAGE_TYPE_IMAGE;
originalContentUrl;
previewImageUrl;
constructor({
originalContentUrl,
previewImageUrl,
}) {
super();
this.originalContentUrl = originalContentUrl;
this.previewImageUrl = previewImageUrl;
}
}
export default ImageMessage;
================================================
FILE: app/messages/index.js
================================================
import Message from './message.js';
import ImageMessage from './image.js';
import TemplateMessage from './template.js';
import TextMessage from './text.js';
export {
Message,
ImageMessage,
TemplateMessage,
TextMessage,
};
================================================
FILE: app/messages/message.js
================================================
import { QUICK_REPLY_TYPE_ACTION } from '../../services/line.js';
import { MessageAction } from '../actions/index.js';
import { Command } from '../commands/index.js';
class Message {
type;
quickReply;
/**
* @param {Array} actions
*/
setQuickReply(actions = []) {
if (actions.length < 1) return;
this.quickReply = {
items: actions.map((action) => ({
type: QUICK_REPLY_TYPE_ACTION,
action: new MessageAction(action),
})),
};
}
}
export default Message;
================================================
FILE: app/messages/template.js
================================================
import { MESSAGE_TYPE_TEMPLATE, TEMPLATE_TYPE_BUTTONS } from '../../services/line.js';
import { MessageAction } from '../actions/index.js';
import Message from './message.js';
class TemplateMessage extends Message {
type = MESSAGE_TYPE_TEMPLATE;
altText;
template;
constructor({
text,
actions,
}) {
super();
this.altText = text;
this.template = {
type: TEMPLATE_TYPE_BUTTONS,
text,
actions: actions.map((action) => new MessageAction(action)),
};
}
}
export default TemplateMessage;
================================================
FILE: app/messages/text.js
================================================
import { MESSAGE_TYPE_TEXT } from '../../services/line.js';
import Message from './message.js';
class TextMessage extends Message {
type = MESSAGE_TYPE_TEXT;
text;
constructor({
text,
}) {
super();
this.text = text;
}
}
export default TextMessage;
================================================
FILE: app/models/bot.js
================================================
class Bot {
isActivated;
constructor({
isActivated,
}) {
this.isActivated = isActivated;
}
}
export default Bot;
================================================
FILE: app/models/event.js
================================================
import {
EVENT_TYPE_MESSAGE,
MESSAGE_TYPE_AUDIO,
MESSAGE_TYPE_STICKER,
MESSAGE_TYPE_TEXT,
MESSAGE_TYPE_IMAGE,
SOURCE_TYPE_GROUP,
} from '../../services/line.js';
class Event {
type;
replyToken;
source;
message;
constructor({
type,
replyToken,
source,
message,
}) {
this.type = type;
this.replyToken = replyToken;
this.source = source;
this.message = message;
}
/**
* @returns {boolean}
*/
get isMessage() {
return this.type === EVENT_TYPE_MESSAGE;
}
/**
* @returns {boolean}
*/
get isGroup() {
return this.source.type === SOURCE_TYPE_GROUP;
}
/**
* @returns {boolean}
*/
get isText() {
return this.message.type === MESSAGE_TYPE_TEXT;
}
/**
* @returns {boolean}
*/
get isSticker() {
return this.message.type === MESSAGE_TYPE_STICKER;
}
/**
* @returns {boolean}
*/
get isAudio() {
return this.message.type === MESSAGE_TYPE_AUDIO;
}
/**
* @returns {boolean}
*/
get isImage() {
return this.message.type === MESSAGE_TYPE_IMAGE;
}
/**
* @returns {string}
*/
get groupId() {
return this.source.groupId;
}
/**
* @returns {string}
*/
get userId() {
return this.source.userId;
}
/**
* @returns {string}
*/
get messageId() {
return this.message.id;
}
/**
* @returns {string}
*/
get text() {
return this.message.text;
}
}
export default Event;
================================================
FILE: app/models/index.js
================================================
import Bot from './bot.js';
import Event from './event.js';
import Source from './source.js';
export {
Bot,
Event,
Source,
};
export default null;
================================================
FILE: app/models/source.js
================================================
import { t } from '../../locales/index.js';
import { SOURCE_TYPE_GROUP } from '../../services/line.js';
class Source {
type;
name;
bot;
createdAt;
constructor({
type,
name,
bot,
}) {
this.type = type;
this.name = name || (type === SOURCE_TYPE_GROUP ? t('__SOURCE_NAME_SOME_GROUP') : t('__SOURCE_NAME_SOMEONE'));
this.bot = bot;
this.createdAt = Math.floor(Date.now() / 1000);
}
}
export default Source;
================================================
FILE: app/prompt/index.js
================================================
import Prompt from './prompt.js';
const prompts = new Map();
/**
* @param {string} userId
* @returns {Prompt}
*/
const getPrompt = (userId) => prompts.get(userId) || new Prompt();
/**
* @param {string} userId
* @param {Prompt} prompt
*/
const setPrompt = (userId, prompt) => {
prompts.set(userId, prompt);
};
/**
* @param {string} userId
*/
const removePrompt = (userId) => {
prompts.delete(userId);
};
const printPrompts = () => {
if (Array.from(prompts.keys()).length < 1) return;
const content = Array.from(prompts.keys()).map((userId) => `\n=== ${userId.slice(0, 6)} ===\n${getPrompt(userId)}\n`).join('');
console.info(content);
};
export {
Prompt,
getPrompt,
setPrompt,
removePrompt,
printPrompts,
};
export default prompts;
================================================
FILE: app/prompt/message.js
================================================
import { TYPE_ANALYZE, TYPE_SUM, TYPE_TRANSLATE } from '../../constants/command.js';
class Message {
role;
content;
constructor({
role,
content,
}) {
this.role = role;
this.content = content;
}
get isEnquiring() {
return this.content === TYPE_SUM
|| this.content === TYPE_ANALYZE
|| this.content === TYPE_TRANSLATE;
}
toString() {
if (Array.isArray(this.content)) {
return `\n${this.role}: ${this.content[0].text}`;
}
return this.role ? `\n${this.role}: ${this.content}` : this.content;
}
}
export default Message;
================================================
FILE: app/prompt/prompt.js
================================================
import { encode } from 'gpt-3-encoder';
import config from '../../config/index.js';
import { t } from '../../locales/index.js';
import { ROLE_AI, ROLE_HUMAN, ROLE_SYSTEM } from '../../services/openai.js';
import { addMark } from '../../utils/index.js';
import Message from './message.js';
const MAX_MESSAGES = config.APP_MAX_PROMPT_MESSAGES + 3;
const MAX_TOKENS = config.APP_MAX_PROMPT_TOKENS;
class Prompt {
messages = [];
constructor() {
this
.write(ROLE_SYSTEM, config.APP_INIT_PROMPT || t('__COMPLETION_DEFAULT_SYSTEM_PROMPT'))
.write(ROLE_HUMAN, `${t('__COMPLETION_DEFAULT_HUMAN_PROMPT')(config.HUMAN_NAME)}${config.HUMAN_INIT_PROMPT}`)
.write(ROLE_AI, `${t('__COMPLETION_DEFAULT_AI_PROMPT')(config.BOT_NAME)}${config.BOT_INIT_PROMPT}`);
}
/**
* @returns {Message}
*/
get lastMessage() {
return this.messages.length > 0 ? this.messages[this.messages.length - 1] : null;
}
get tokenCount() {
const encoded = encode(this.toString());
return encoded.length;
}
erase() {
if (this.messages.length > 0) {
this.messages.pop();
}
return this;
}
/**
* @param {string} role
* @param {string} content
*/
write(role, content = '') {
if (this.messages.length >= MAX_MESSAGES || this.tokenCount >= MAX_TOKENS) {
this.messages.splice(3, 1);
}
this.messages.push(new Message({ role, content: addMark(content) }));
return this;
}
/**
* @param {string} role
* @param {string} content
*/
writeImage(role, content = '') {
const imageContent = [
{
type: 'text',
text: t('__COMPLETION_VISION'),
},
{
type: 'image_url',
image_url: {
url: content,
},
},
];
this.messages.push(new Message({ role, content: imageContent }));
return this;
}
/**
* @param {string} content
*/
patch(content) {
this.messages[this.messages.length - 1].content += content;
}
toString() {
return this.messages.map((sentence) => sentence.toString()).join('');
}
}
export default Prompt;
================================================
FILE: app/repository/index.js
================================================
import { getSources, updateSources, setSources } from './source.js';
export {
getSources,
updateSources,
setSources,
};
export default null;
================================================
FILE: app/repository/source.js
================================================
import storage from '../../storage/index.js';
import { Source } from '../models/index.js';
const FIELD_KEY = 'sources';
/**
* @returns {Object.}
*/
const getSources = () => storage.getItem(FIELD_KEY) || {};
/**
* @param {Object.} sources
*/
const setSources = (sources) => storage.setItem(FIELD_KEY, sources);
/**
* @param {string} contextId
* @param {function(Source)} callback
*/
const updateSources = async (contextId, callback) => {
const sources = getSources();
callback(sources[contextId]);
await setSources(sources);
};
export {
getSources,
setSources,
updateSources,
};
================================================
FILE: babel.config.cjs
================================================
module.exports = {
presets: [
'@babel/preset-env',
],
};
================================================
FILE: config/index.js
================================================
import dotenv from 'dotenv';
const { env } = process;
dotenv.config({
path: env.NODE_ENV ? `.env.${env.NODE_ENV}` : '.env',
});
const config = Object.freeze({
APP_ENV: env.NODE_ENV || 'production',
APP_DEBUG: env.APP_DEBUG === 'true' || false,
APP_URL: env.APP_URL || null,
APP_PORT: env.APP_PORT || null,
APP_LANG: env.APP_LANG || 'zh_TW',
APP_WEBHOOK_PATH: env.APP_WEBHOOK_PATH || '/webhook',
APP_API_TIMEOUT: env.APP_API_TIMEOUT || 9000,
APP_MAX_GROUPS: Number(env.APP_MAX_GROUPS) || 1000,
APP_MAX_USERS: Number(env.APP_MAX_USERS) || 1000,
APP_MAX_PROMPT_MESSAGES: Number(env.APP_MAX_PROMPT_MESSAGES) || 4,
APP_MAX_PROMPT_TOKENS: Number(env.APP_MAX_PROMPT_TOKENS) || 256,
APP_INIT_PROMPT: env.APP_INIT_PROMPT || '',
HUMAN_NAME: env.HUMAN_NAME || '',
HUMAN_INIT_PROMPT: env.HUMAN_INIT_PROMPT || '',
BOT_NAME: env.BOT_NAME || 'AI',
BOT_INIT_PROMPT: env.BOT_INIT_PROMPT || '',
BOT_TONE: env.BOT_TONE || '',
BOT_DEACTIVATED: env.BOT_DEACTIVATED === 'true' || false,
ERROR_MESSAGE_DISABLED: env.ERROR_MESSAGE_DISABLED === 'true' || false,
VERCEL_ENV: env.VERCEL_ENV || null,
VERCEL_TIMEOUT: env.VERCEL_TIMEOUT || env.APP_API_TIMEOUT,
VERCEL_PROJECT_NAME: env.VERCEL_PROJECT_NAME || env.VERCEL_GIT_REPO_SLUG || null,
VERCEL_TEAM_ID: env.VERCEL_TEAM_ID || null,
VERCEL_ACCESS_TOKEN: env.VERCEL_ACCESS_TOKEN || null,
VERCEL_DEPLOY_HOOK_URL: env.VERCEL_DEPLOY_HOOK_URL || null,
OPENAI_TIMEOUT: env.OPENAI_TIMEOUT || env.APP_API_TIMEOUT,
OPENAI_API_KEY: env.OPENAI_API_KEY || null,
OPENAI_BASE_URL: env.OPENAI_BASE_URL || 'https://api.openai.com',
OPENAI_COMPLETION_MODEL: env.OPENAI_COMPLETION_MODEL || 'gpt-3.5-turbo',
OPENAI_COMPLETION_TEMPERATURE: Number(env.OPENAI_COMPLETION_TEMPERATURE) || 1,
OPENAI_COMPLETION_MAX_TOKENS: Number(env.OPENAI_COMPLETION_MAX_TOKENS) || 64,
OPENAI_COMPLETION_FREQUENCY_PENALTY: Number(env.OPENAI_COMPLETION_FREQUENCY_PENALTY) || 0,
OPENAI_COMPLETION_PRESENCE_PENALTY: Number(env.OPENAI_COMPLETION_PRESENCE_PENALTY) || 0.6,
OPENAI_COMPLETION_STOP_SEQUENCES: env.OPENAI_COMPLETION_STOP_SEQUENCES ? String(env.OPENAI_COMPLETION_STOP_SEQUENCES).split(',') : [' assistant:', ' user:'],
OPENAI_IMAGE_GENERATION_MODEL: env.OPENAI_IMAGE_GENERATION_MODEL || 'dall-e-2',
OPENAI_IMAGE_GENERATION_SIZE: env.OPENAI_IMAGE_GENERATION_SIZE || '256x256',
OPENAI_IMAGE_GENERATION_QUALITY: env.OPENAI_IMAGE_GENERATION_QUALITY || 'standard',
OPENAI_VISION_MODEL: env.OPENAI_VISION_MODEL || 'gpt-4o',
LINE_TIMEOUT: env.LINE_TIMEOUT || env.APP_API_TIMEOUT,
LINE_CHANNEL_ACCESS_TOKEN: env.LINE_CHANNEL_ACCESS_TOKEN || null,
LINE_CHANNEL_SECRET: env.LINE_CHANNEL_SECRET || null,
SERPAPI_TIMEOUT: env.SERPAPI_TIMEOUT || env.APP_API_TIMEOUT,
SERPAPI_API_KEY: env.SERPAPI_API_KEY || null,
SERPAPI_LOCATION: env.SERPAPI_LOCATION || 'tw',
});
export default config;
================================================
FILE: constants/command.js
================================================
export const TYPE_SUM = 'sum';
export const TYPE_ANALYZE = 'analyze';
export const TYPE_SYSTEM = 'system';
export const TYPE_TRANSLATE = 'translate';
================================================
FILE: constants/mock.js
================================================
export const MOCK_TEXT_OK = 'OK!';
export const MOCK_GROUP_01 = '000001';
export const MOCK_GROUP_02 = '000002';
export const MOCK_USER_01 = '000001';
export const MOCK_USER_02 = '000002';
const mockGroups = {};
mockGroups[MOCK_GROUP_01] = { groupName: 'group' };
mockGroups[MOCK_USER_02] = { groupName: 'group 2' };
const mockUsers = {};
mockUsers[MOCK_USER_01] = { displayName: 'user' };
mockUsers[MOCK_USER_02] = { displayName: 'user 2' };
export {
mockGroups,
mockUsers,
};
================================================
FILE: docker-compose.yaml
================================================
version: "3"
services:
app:
container_name: gpt-ai-assistant
build: .
restart: always
ports:
- "${APP_PORT}:${APP_PORT}"
================================================
FILE: locales/en.js
================================================
const en = {
__COMMAND_ANALYZE_ANALYZE_LABEL: 'Analyze',
__COMMAND_ANALYZE_ANALYZE_TEXT: 'Analyze',
__COMMAND_ANALYZE_ANALYZE_PROMPT: 'Please analyze the following statements.',
__COMMAND_ANALYZE_LITERARILY_LABEL: 'Literarily',
__COMMAND_ANALYZE_LITERARILY_TEXT: 'Analyze literarily',
__COMMAND_ANALYZE_LITERARILY_PROMPT: 'Please analyze the following statements literarily.',
__COMMAND_ANALYZE_MATHEMATICALLY_LABEL: 'Mathematically',
__COMMAND_ANALYZE_MATHEMATICALLY_TEXT: 'Analyze mathematically',
__COMMAND_ANALYZE_MATHEMATICALLY_PROMPT: 'Please analyze the following statements mathematically.',
__COMMAND_ANALYZE_NUMEROLOGICALLY_LABEL: 'Numerologically',
__COMMAND_ANALYZE_NUMEROLOGICALLY_TEXT: 'Analyze numerologically',
__COMMAND_ANALYZE_NUMEROLOGICALLY_PROMPT: 'Please analyze the following statements numerologically.',
__COMMAND_ANALYZE_PHILOSOPHICALLY_LABEL: 'Philosophically',
__COMMAND_ANALYZE_PHILOSOPHICALLY_TEXT: 'Analyze philosophically',
__COMMAND_ANALYZE_PHILOSOPHICALLY_PROMPT: 'Please analyze the following statements philosophically.',
__COMMAND_ANALYZE_PSYCHOLOGICALLY_LABEL: 'Psychologically',
__COMMAND_ANALYZE_PSYCHOLOGICALLY_TEXT: 'Analyze psychologically',
__COMMAND_ANALYZE_PSYCHOLOGICALLY_PROMPT: 'Please analyze the following statements psychologically.',
__COMMAND_BOT_ACTIVATE_LABEL: 'Activate',
__COMMAND_BOT_ACTIVATE_TEXT: 'Activate',
__COMMAND_BOT_ACTIVATE_ALIASES: [],
__COMMAND_BOT_ACTIVATE_REPLY: 'Activated',
__COMMAND_BOT_CONTINUE_LABEL: 'Continue',
__COMMAND_BOT_CONTINUE_TEXT: 'Continue',
__COMMAND_BOT_CONTINUE_ALIASES: [],
__COMMAND_BOT_DEACTIVATE_LABEL: 'Deactivate',
__COMMAND_BOT_DEACTIVATE_TEXT: 'Deactivate',
__COMMAND_BOT_DEACTIVATE_ALIASES: [],
__COMMAND_BOT_DEACTIVATE_REPLY: 'Deactivated',
__COMMAND_BOT_DRAW_LABEL: 'Draw',
__COMMAND_BOT_DRAW_TEXT: 'Draw',
__COMMAND_BOT_DRAW_ALIASES: [],
__COMMAND_BOT_DRAW_DEMO_LABEL: 'Draw',
__COMMAND_BOT_DRAW_DEMO_TEXT: 'Draw a cat',
__COMMAND_BOT_FORGET_LABEL: 'Forget',
__COMMAND_BOT_FORGET_TEXT: 'Forget',
__COMMAND_BOT_FORGET_ALIASES: [],
__COMMAND_BOT_FORGET_REPLY: 'Forgot',
__COMMAND_BOT_RETRY_LABEL: 'Retry',
__COMMAND_BOT_RETRY_TEXT: 'Retry',
__COMMAND_BOT_RETRY_ALIASES: [],
__COMMAND_BOT_SEARCH_LABEL: 'Search',
__COMMAND_BOT_SEARCH_TEXT: 'Search',
__COMMAND_BOT_SEARCH_ALIASES: [],
__COMMAND_BOT_SEARCH_DEMO_LABEL: 'Search',
__COMMAND_BOT_SEARCH_DEMO_TEXT: 'Search date',
__COMMAND_BOT_SUMMON_DEMO_LABEL: 'Summon',
__COMMAND_BOT_SUMMON_DEMO_TEXT: 'What\'s up?',
__COMMAND_BOT_TALK_LABEL: 'Talk',
__COMMAND_BOT_TALK_TEXT: 'Talk',
__COMMAND_BOT_TALK_ALIASES: [],
__COMMAND_BOT_TALK_DEMO_LABEL: 'Talk',
__COMMAND_BOT_TALK_DEMO_TEXT: 'Talk me about yourself',
__COMMAND_SUM_ADVISE_LABEL: 'Advise',
__COMMAND_SUM_ADVISE_TEXT: 'Advise',
__COMMAND_SUM_ADVISE_PROMPT: 'Please summarize the following content briefly and advise appropriately.',
__COMMAND_SUM_APOLOGIZE_LABEL: 'Apologize',
__COMMAND_SUM_APOLOGIZE_TEXT: 'Apologize',
__COMMAND_SUM_APOLOGIZE_PROMPT: 'Please summarize the following content briefly and apologize sincerely.',
__COMMAND_SUM_BLAME_LABEL: 'Blame',
__COMMAND_SUM_BLAME_TEXT: 'Blame',
__COMMAND_SUM_BLAME_PROMPT: 'Please summarize the following content briefly and blame strongly.',
__COMMAND_SUM_COMFORT_LABEL: 'Comfort',
__COMMAND_SUM_COMFORT_TEXT: 'Comfort',
__COMMAND_SUM_COMFORT_PROMPT: 'Please summarize the following content briefly and comfort warmly.',
__COMMAND_SUM_COMPLAIN_LABEL: 'Complain',
__COMMAND_SUM_COMPLAIN_TEXT: 'Complain',
__COMMAND_SUM_COMPLAIN_PROMPT: 'Please summarize the following content briefly and complain gently.',
__COMMAND_SUM_ENCOURAGE_LABEL: 'Encourage',
__COMMAND_SUM_ENCOURAGE_TEXT: 'Encourage',
__COMMAND_SUM_ENCOURAGE_PROMPT: 'Please summarize the following content briefly and encourage enthusiastically.',
__COMMAND_SUM_LAUGH_LABEL: 'Laugh',
__COMMAND_SUM_LAUGH_TEXT: 'Laugh',
__COMMAND_SUM_LAUGH_PROMPT: 'Please summarize the following content briefly and laugh rudely.',
__COMMAND_SUM_SUM_LABEL: 'Sum',
__COMMAND_SUM_SUM_TEXT: 'Sum',
__COMMAND_SUM_SUM_PROMPT: 'Please summarize the following content and provide some details.',
__COMMAND_SYS_COMMAND_LABEL: 'Command',
__COMMAND_SYS_COMMAND_TEXT: 'Command',
__COMMAND_SYS_DOC_LABEL: 'Documentation',
__COMMAND_SYS_DOC_TEXT: 'Documentation',
__COMMAND_SYS_DEPLOY_LABEL: 'Restart',
__COMMAND_SYS_DEPLOY_TEXT: 'Restart',
__COMMAND_SYS_DEPLOY_REPLY: 'Restarting',
__COMMAND_SYS_REPORT_LABEL: 'Report',
__COMMAND_SYS_REPORT_TEXT: 'Report',
__COMMAND_SYS_VERSION_LABEL: 'Version',
__COMMAND_SYS_VERSION_TEXT: 'Version',
__COMMAND_SYS_VERSION_REPLY: (version, isLatest) => `Your version is ${isLatest ? 'up-to-date' : version}.`,
__COMMAND_TRANSLATE_TO_EN_LABEL: '翻成英文', // TODO
__COMMAND_TRANSLATE_TO_EN_TEXT: '翻成英文', // TODO
__COMMAND_TRANSLATE_TO_EN_PROMPT: '請將以下內容翻譯成英文。', // TODO
__COMMAND_TRANSLATE_TO_JA_LABEL: '翻成日文', // TODO
__COMMAND_TRANSLATE_TO_JA_TEXT: '翻成日文', // TODO
__COMMAND_TRANSLATE_TO_JA_PROMPT: '請將以下內容翻譯成日文。', // TODO
__COMPLETION_DEFAULT_SYSTEM_PROMPT: '', // TODO
__COMPLETION_DEFAULT_HUMAN_PROMPT: (name) => (name ? `I am ${name}` : 'Hello'),
__COMPLETION_DEFAULT_AI_PROMPT: (name) => (name ? `I am ${name}` : 'Hello'),
__COMPLETION_DEFAULT_AI_TONE: (tone) => (tone ? `以${tone}的語氣回應我:` : ''), // TODO
__COMPLETION_SEARCH: (a, q) => `根據「${a}」查詢結果,回答「${q}」問題`, // TODO
__COMPLETION_SEARCH_NOT_FOUND: '查無資料', // TODO
__COMPLETION_QUOTATION_MARK_OPENING: '"',
__COMPLETION_QUOTATION_MARK_CLOSING: '"',
__COMPLETION_VISION: 'What\'s in this image?',
__ERROR_ECONNABORTED: 'Timed out',
__ERROR_UNKNOWN: 'Something went wrong',
__ERROR_MAX_GROUPS_REACHED: 'Maximum groups reached',
__ERROR_MAX_USERS_REACHED: 'Maximum users reached',
__ERROR_MISSING_ENV: (v) => `Missing environment variable: ${v}`,
__MESSAGE_NEW_VERSION_AVAILABLE: (version) => `A new version ${version} is now available!`,
__SOURCE_NAME_SOME_GROUP: 'Someone Group',
__SOURCE_NAME_SOMEONE: 'Someone',
};
export default en;
================================================
FILE: locales/index.js
================================================
import config from '../config/index.js';
import en from './en.js';
import ja from './ja.js';
import zh from './zh.js';
const locales = {
en,
ja,
zh,
zh_TW: zh,
zh_CN: zh,
};
const t = (key) => locales[config.APP_LANG][key];
export {
t,
};
export default null;
================================================
FILE: locales/ja.js
================================================
const ja = {
__COMMAND_ANALYZE_ANALYZE_LABEL: '分析して',
__COMMAND_ANALYZE_ANALYZE_TEXT: '分析して',
__COMMAND_ANALYZE_ANALYZE_PROMPT: '以下の内容を詳しく分析してください。',
__COMMAND_ANALYZE_LITERARILY_LABEL: '文学的に分析して',
__COMMAND_ANALYZE_LITERARILY_TEXT: '文学的に分析して',
__COMMAND_ANALYZE_LITERARILY_PROMPT: '以下の内容を文学的に詳しく分析してください。',
__COMMAND_ANALYZE_MATHEMATICALLY_LABEL: '数学的に分析して',
__COMMAND_ANALYZE_MATHEMATICALLY_TEXT: '数学的に分析して',
__COMMAND_ANALYZE_MATHEMATICALLY_PROMPT: '以下の内容を数学的に詳しく分析してください。',
__COMMAND_ANALYZE_NUMEROLOGICALLY_LABEL: '算命学的に分析して',
__COMMAND_ANALYZE_NUMEROLOGICALLY_TEXT: '算命学的に分析して',
__COMMAND_ANALYZE_NUMEROLOGICALLY_PROMPT: '以下の内容を算命学的に詳しく分析してください。',
__COMMAND_ANALYZE_PHILOSOPHICALLY_LABEL: '哲学的に分析して',
__COMMAND_ANALYZE_PHILOSOPHICALLY_TEXT: '哲学的に分析して',
__COMMAND_ANALYZE_PHILOSOPHICALLY_PROMPT: '以下の内容を哲学的に詳しく分析してください。',
__COMMAND_ANALYZE_PSYCHOLOGICALLY_LABEL: '心理学的に分析して',
__COMMAND_ANALYZE_PSYCHOLOGICALLY_TEXT: '心理学的に分析して',
__COMMAND_ANALYZE_PSYCHOLOGICALLY_PROMPT: '以下の内容を心理学的に詳しく分析してください。',
__COMMAND_BOT_ACTIVATE_LABEL: '自動応答をオンにする',
__COMMAND_BOT_ACTIVATE_TEXT: '自動応答をオンにする',
__COMMAND_BOT_ACTIVATE_ALIASES: [],
__COMMAND_BOT_ACTIVATE_REPLY: 'オンにしました',
__COMMAND_BOT_CONTINUE_LABEL: '続いて',
__COMMAND_BOT_CONTINUE_TEXT: '続いて',
__COMMAND_BOT_CONTINUE_ALIASES: [],
__COMMAND_BOT_DEACTIVATE_LABEL: '自動応答をオフにする',
__COMMAND_BOT_DEACTIVATE_TEXT: '自動応答をオフにする',
__COMMAND_BOT_DEACTIVATE_ALIASES: [],
__COMMAND_BOT_DEACTIVATE_REPLY: 'オフにしました',
__COMMAND_BOT_DRAW_LABEL: '描いて',
__COMMAND_BOT_DRAW_TEXT: '描いて',
__COMMAND_BOT_DRAW_ALIASES: [],
__COMMAND_BOT_DRAW_DEMO_LABEL: '描いて',
__COMMAND_BOT_DRAW_DEMO_TEXT: '猫を描いて',
__COMMAND_BOT_FORGET_LABEL: '忘記', // TODO
__COMMAND_BOT_FORGET_TEXT: '忘記', // TODO
__COMMAND_BOT_FORGET_ALIASES: [],
__COMMAND_BOT_FORGET_REPLY: '已忘記', // TODO
__COMMAND_BOT_RETRY_LABEL: 'リトライ',
__COMMAND_BOT_RETRY_TEXT: 'リトライ',
__COMMAND_BOT_RETRY_ALIASES: [],
__COMMAND_BOT_SEARCH_LABEL: '查詢', // TODO
__COMMAND_BOT_SEARCH_TEXT: '查詢', // TODO
__COMMAND_BOT_SEARCH_ALIASES: [],
__COMMAND_BOT_SEARCH_DEMO_LABEL: '查詢', // TODO
__COMMAND_BOT_SEARCH_DEMO_TEXT: '查詢日期', // TODO
__COMMAND_BOT_SUMMON_DEMO_LABEL: 'サモン',
__COMMAND_BOT_SUMMON_DEMO_TEXT: '元気?',
__COMMAND_BOT_TALK_LABEL: '話して',
__COMMAND_BOT_TALK_TEXT: '話して',
__COMMAND_BOT_TALK_ALIASES: [],
__COMMAND_BOT_TALK_DEMO_LABEL: '話して',
__COMMAND_BOT_TALK_DEMO_TEXT: '自分のことを話して',
__COMMAND_SUM_ADVISE_LABEL: 'アドバイスをして',
__COMMAND_SUM_ADVISE_TEXT: 'アドバイスをして',
__COMMAND_SUM_ADVISE_PROMPT: '以下の内容を要約し、いいアドバイスをください。',
__COMMAND_SUM_APOLOGIZE_LABEL: '謝って',
__COMMAND_SUM_APOLOGIZE_TEXT: '謝って',
__COMMAND_SUM_APOLOGIZE_PROMPT: '以下の内容を要約し、ちゃんと謝ってください。',
__COMMAND_SUM_BLAME_LABEL: '責めて',
__COMMAND_SUM_BLAME_TEXT: '責めて',
__COMMAND_SUM_BLAME_PROMPT: '以下の内容を要約し、強く非難してください。',
__COMMAND_SUM_COMFORT_LABEL: '慰めて',
__COMMAND_SUM_COMFORT_TEXT: '慰めて',
__COMMAND_SUM_COMFORT_PROMPT: '以下の内容を要約し、暖かく慰めてください。',
__COMMAND_SUM_ENCOURAGE_LABEL: '励んで',
__COMMAND_SUM_ENCOURAGE_TEXT: '励んで',
__COMMAND_SUM_ENCOURAGE_PROMPT: '以下の内容を要約し、熱心に励んでください。',
__COMMAND_SUM_COMPLAIN_LABEL: '愚痴を言って',
__COMMAND_SUM_COMPLAIN_TEXT: '愚痴を言って',
__COMMAND_SUM_COMPLAIN_PROMPT: '以下の内容を要約し、軽く愚痴を言ってください。',
__COMMAND_SUM_LAUGH_LABEL: '笑って',
__COMMAND_SUM_LAUGH_TEXT: '笑って',
__COMMAND_SUM_LAUGH_PROMPT: '以下の内容を要約し、面白く笑ってください。',
__COMMAND_SUM_SUM_LABEL: '要約して',
__COMMAND_SUM_SUM_TEXT: '要約して',
__COMMAND_SUM_SUM_PROMPT: '以下の内容を要約し、説明してください。',
__COMMAND_SYS_COMMAND_LABEL: 'コマンド',
__COMMAND_SYS_COMMAND_TEXT: 'コマンド',
__COMMAND_SYS_DOC_LABEL: 'ドキュメンテーション',
__COMMAND_SYS_DOC_TEXT: 'ドキュメンテーション',
__COMMAND_SYS_DEPLOY_LABEL: '再起動',
__COMMAND_SYS_DEPLOY_TEXT: '再起動',
__COMMAND_SYS_DEPLOY_REPLY: '再起動しています',
__COMMAND_SYS_REPORT_LABEL: 'バグレポート',
__COMMAND_SYS_REPORT_TEXT: 'バグレポート',
__COMMAND_SYS_VERSION_LABEL: 'バージョン',
__COMMAND_SYS_VERSION_TEXT: 'バージョン',
__COMMAND_SYS_VERSION_REPLY: (version, isLatest) => `Your version is ${isLatest ? 'up-to-date' : version}.`,
__COMMAND_TRANSLATE_TO_EN_LABEL: '翻成英文', // TODO
__COMMAND_TRANSLATE_TO_EN_TEXT: '翻成英文', // TODO
__COMMAND_TRANSLATE_TO_EN_PROMPT: '請將以下內容翻譯成英文。', // TODO
__COMMAND_TRANSLATE_TO_JA_LABEL: '翻成日文', // TODO
__COMMAND_TRANSLATE_TO_JA_TEXT: '翻成日文', // TODO
__COMMAND_TRANSLATE_TO_JA_PROMPT: '請將以下內容翻譯成日文。', // TODO
__COMPLETION_DEFAULT_SYSTEM_PROMPT: '', // TODO
__COMPLETION_DEFAULT_HUMAN_PROMPT: (name) => (name ? `私は${name}です` : 'こんにちは'),
__COMPLETION_DEFAULT_AI_PROMPT: (name) => (name ? `私は${name}です` : 'こんにちは'),
__COMPLETION_DEFAULT_AI_TONE: (tone) => (tone ? `以${tone}的語氣回應我:` : ''), // TODO
__COMPLETION_SEARCH: (a, q) => `根據「${a}」查詢結果,回答「${q}」問題`, // TODO
__COMPLETION_SEARCH_NOT_FOUND: '查無資料', // TODO
__COMPLETION_QUOTATION_MARK_OPENING: '「',
__COMPLETION_QUOTATION_MARK_CLOSING: '」',
__COMPLETION_VISION: 'この画像には何がありますか?',
__ERROR_ECONNABORTED: '接続がタイムアウトしました。',
__ERROR_UNKNOWN: '技術的な問題が発生しています。',
__ERROR_MAX_GROUPS_REACHED: '最大ユーザー数に達しています。',
__ERROR_MAX_USERS_REACHED: '最大グループ数に達しています。',
__ERROR_MISSING_ENV: (v) => `「${v}」環境変数が見つかりません。`,
__MESSAGE_NEW_VERSION_AVAILABLE: (version) => `A new version ${version} is now available!`,
__SOURCE_NAME_SOME_GROUP: 'あるグループ',
__SOURCE_NAME_SOMEONE: 'あるユーザー',
};
export default ja;
================================================
FILE: locales/zh.js
================================================
const zh = {
__COMMAND_ANALYZE_ANALYZE_LABEL: '分析',
__COMMAND_ANALYZE_ANALYZE_TEXT: '分析',
__COMMAND_ANALYZE_ANALYZE_PROMPT: '分析以下內容,並給予細節。',
__COMMAND_ANALYZE_LITERARILY_LABEL: '文學分析',
__COMMAND_ANALYZE_LITERARILY_TEXT: '文學分析',
__COMMAND_ANALYZE_LITERARILY_PROMPT: '使用文學的角度分析以下內容,並給予細節。',
__COMMAND_ANALYZE_MATHEMATICALLY_LABEL: '數學分析',
__COMMAND_ANALYZE_MATHEMATICALLY_TEXT: '數學分析',
__COMMAND_ANALYZE_MATHEMATICALLY_PROMPT: '使用數學的角度分析以下內容,並給予細節。',
__COMMAND_ANALYZE_NUMEROLOGICALLY_LABEL: '命理學分析',
__COMMAND_ANALYZE_NUMEROLOGICALLY_TEXT: '命理學分析',
__COMMAND_ANALYZE_NUMEROLOGICALLY_PROMPT: '使用命理學的角度分析以下內容,並給予細節。',
__COMMAND_ANALYZE_PHILOSOPHICALLY_LABEL: '哲學分析',
__COMMAND_ANALYZE_PHILOSOPHICALLY_TEXT: '哲學分析',
__COMMAND_ANALYZE_PHILOSOPHICALLY_PROMPT: '使用哲學的角度分析以下內容,並給予細節。',
__COMMAND_ANALYZE_PSYCHOLOGICALLY_LABEL: '心理學分析',
__COMMAND_ANALYZE_PSYCHOLOGICALLY_TEXT: '心理學分析',
__COMMAND_ANALYZE_PSYCHOLOGICALLY_PROMPT: '使用心理學的角度分析以下內容,並給予細節。',
__COMMAND_BOT_ACTIVATE_LABEL: '開啟自動回覆',
__COMMAND_BOT_ACTIVATE_TEXT: '開啟自動回覆',
__COMMAND_BOT_ACTIVATE_ALIASES: ['說話', '開始說話'],
__COMMAND_BOT_ACTIVATE_REPLY: '已開啟自動回覆',
__COMMAND_BOT_CONTINUE_LABEL: '繼續',
__COMMAND_BOT_CONTINUE_TEXT: '繼續',
__COMMAND_BOT_CONTINUE_ALIASES: [],
__COMMAND_BOT_DEACTIVATE_LABEL: '關閉自動回覆',
__COMMAND_BOT_DEACTIVATE_TEXT: '關閉自動回覆',
__COMMAND_BOT_DEACTIVATE_ALIASES: ['閉嘴', '停止說話'],
__COMMAND_BOT_DEACTIVATE_REPLY: '已關閉自動回覆',
__COMMAND_BOT_DRAW_LABEL: '請畫',
__COMMAND_BOT_DRAW_TEXT: '請畫',
__COMMAND_BOT_DRAW_ALIASES: ['畫', '描繪'],
__COMMAND_BOT_DRAW_DEMO_LABEL: '請畫',
__COMMAND_BOT_DRAW_DEMO_TEXT: '請畫貓咪',
__COMMAND_BOT_FORGET_LABEL: '忘記',
__COMMAND_BOT_FORGET_TEXT: '忘記',
__COMMAND_BOT_FORGET_ALIASES: ['重來'],
__COMMAND_BOT_FORGET_REPLY: '已忘記',
__COMMAND_BOT_RETRY_LABEL: '重試',
__COMMAND_BOT_RETRY_TEXT: '重試',
__COMMAND_BOT_RETRY_ALIASES: [],
__COMMAND_BOT_SEARCH_LABEL: '查詢',
__COMMAND_BOT_SEARCH_TEXT: '查詢',
__COMMAND_BOT_SEARCH_ALIASES: ['搜尋', '查找'],
__COMMAND_BOT_SEARCH_DEMO_LABEL: '查詢',
__COMMAND_BOT_SEARCH_DEMO_TEXT: '查詢日期',
__COMMAND_BOT_SUMMON_DEMO_LABEL: '呼叫',
__COMMAND_BOT_SUMMON_DEMO_TEXT: '你好嗎?',
__COMMAND_BOT_TALK_LABEL: '請問',
__COMMAND_BOT_TALK_TEXT: '請問',
__COMMAND_BOT_TALK_ALIASES: ['呼叫'],
__COMMAND_BOT_TALK_DEMO_LABEL: '請問',
__COMMAND_BOT_TALK_DEMO_TEXT: '請問你好嗎',
__COMMAND_SUM_ADVISE_LABEL: '建議',
__COMMAND_SUM_ADVISE_TEXT: '建議',
__COMMAND_SUM_ADVISE_PROMPT: '總結以下內容,並給予適當的建議。',
__COMMAND_SUM_APOLOGIZE_LABEL: '道歉',
__COMMAND_SUM_APOLOGIZE_TEXT: '道歉',
__COMMAND_SUM_APOLOGIZE_PROMPT: '總結以下內容,並給予誠懇的道歉。',
__COMMAND_SUM_BLAME_LABEL: '譴責',
__COMMAND_SUM_BLAME_TEXT: '譴責',
__COMMAND_SUM_BLAME_PROMPT: '總結以下內容,並給予嚴厲的譴責。',
__COMMAND_SUM_COMFORT_LABEL: '安慰',
__COMMAND_SUM_COMFORT_TEXT: '安慰',
__COMMAND_SUM_COMFORT_PROMPT: '總結以下內容,並給予溫暖的安慰。',
__COMMAND_SUM_COMPLAIN_LABEL: '抱怨',
__COMMAND_SUM_COMPLAIN_TEXT: '抱怨',
__COMMAND_SUM_COMPLAIN_PROMPT: '總結以下內容,並給予輕微的抱怨。',
__COMMAND_SUM_ENCOURAGE_LABEL: '鼓勵',
__COMMAND_SUM_ENCOURAGE_TEXT: '鼓勵',
__COMMAND_SUM_ENCOURAGE_PROMPT: '總結以下內容,並給予熱烈的鼓勵。',
__COMMAND_SUM_LAUGH_LABEL: '嘲諷',
__COMMAND_SUM_LAUGH_TEXT: '嘲諷',
__COMMAND_SUM_LAUGH_PROMPT: '總結以下內容,並給予刻薄的嘲諷。',
__COMMAND_SUM_SUM_LABEL: '總結',
__COMMAND_SUM_SUM_TEXT: '總結',
__COMMAND_SUM_SUM_PROMPT: '總結以下內容,並給予細節。',
__COMMAND_SYS_COMMAND_LABEL: '指令',
__COMMAND_SYS_COMMAND_TEXT: '指令',
__COMMAND_SYS_DOC_LABEL: '查看文件',
__COMMAND_SYS_DOC_TEXT: '文件',
__COMMAND_SYS_DEPLOY_LABEL: '重新啟動',
__COMMAND_SYS_DEPLOY_TEXT: '重新啟動',
__COMMAND_SYS_DEPLOY_REPLY: '正在重新啟動',
__COMMAND_SYS_REPORT_LABEL: '回報問題',
__COMMAND_SYS_REPORT_TEXT: '回報',
__COMMAND_SYS_VERSION_LABEL: '檢查更新',
__COMMAND_SYS_VERSION_TEXT: '版本',
__COMMAND_SYS_VERSION_REPLY: (version, isLatest) => `目前版本為 ${version}${isLatest ? ',已更新到最新版本' : ''}。`,
__COMMAND_TRANSLATE_TO_EN_LABEL: '上一句翻成英文',
__COMMAND_TRANSLATE_TO_EN_TEXT: '上一句翻成英文',
__COMMAND_TRANSLATE_TO_EN_PROMPT: '將以下內容翻譯成英文。',
__COMMAND_TRANSLATE_TO_JA_LABEL: '上一句翻成日文',
__COMMAND_TRANSLATE_TO_JA_TEXT: '上一句翻成日文',
__COMMAND_TRANSLATE_TO_JA_PROMPT: '將以下內容翻譯成日文。',
__COMPLETION_DEFAULT_SYSTEM_PROMPT: '以下將使用繁體中文進行對話。',
__COMPLETION_DEFAULT_HUMAN_PROMPT: (name) => (name ? `我是${name}` : '哈囉'),
__COMPLETION_DEFAULT_AI_PROMPT: (name) => (name ? `我是${name}` : '哈囉'),
__COMPLETION_DEFAULT_AI_TONE: (tone) => (tone ? `以${tone}的語氣回應我:` : ''),
__COMPLETION_SEARCH: (a, q) => `根據「${a}」查詢結果,回答「${q}」問題`,
__COMPLETION_SEARCH_NOT_FOUND: '查無資料',
__COMPLETION_QUOTATION_MARK_OPENING: '「',
__COMPLETION_QUOTATION_MARK_CLOSING: '」',
__COMPLETION_VISION: '這張圖片裡有什麼?',
__ERROR_ECONNABORTED: '這個問題太複雜了',
__ERROR_UNKNOWN: '系統出了點狀況',
__ERROR_MAX_GROUPS_REACHED: '群組數量到達上限了',
__ERROR_MAX_USERS_REACHED: '用戶數量到達上限了',
__ERROR_MISSING_ENV: (v) => `缺少環境變數:${v}`,
__MESSAGE_NEW_VERSION_AVAILABLE: (version) => `最新版本為 ${version},請從 GitHub 更新。`,
__SOURCE_NAME_SOME_GROUP: '某群組',
__SOURCE_NAME_SOMEONE: '某用戶',
};
export default zh;
================================================
FILE: middleware/index.js
================================================
import validateLineSignature from './validate-line-signature.js';
export {
validateLineSignature,
};
export default null;
================================================
FILE: middleware/validate-line-signature.js
================================================
import config from '../config/index.js';
import { validateSignature } from '../utils/index.js';
const validateLineSignature = (req, res, next) => {
const secret = config.LINE_CHANNEL_SECRET || '';
const signature = req.header('x-line-signature');
if (!validateSignature(req.rawBody, secret, signature)) {
res.sendStatus(403);
return;
}
next();
};
export default validateLineSignature;
================================================
FILE: package.json
================================================
{
"name": "gpt-ai-assistant",
"version": "4.9.1",
"type": "module",
"scripts": {
"dev": "nodemon api/index.js",
"start": "node api/index.js",
"test": "jest"
},
"dependencies": {
"axios": "^1.2.1",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"form-data": "^4.0.0",
"gpt-3-encoder": "^1.1.3",
"opencc-js": "^1.0.5"
},
"devDependencies": {
"@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"@jest/globals": "^29.3.1",
"babel-jest": "^29.3.1",
"eslint": "^8.29.0",
"eslint-config-airbnb": "^19.0.4",
"jest": "^29.3.1",
"nodemon": "^2.0.20"
}
}
================================================
FILE: services/line.js
================================================
import axios from 'axios';
import config from '../config/index.js';
import { handleFulfilled, handleRejected, handleRequest } from './utils/index.js';
export const EVENT_TYPE_MESSAGE = 'message';
export const EVENT_TYPE_POSTBACK = 'postback';
export const SOURCE_TYPE_USER = 'user';
export const SOURCE_TYPE_GROUP = 'group';
export const MESSAGE_TYPE_TEXT = 'text';
export const MESSAGE_TYPE_STICKER = 'sticker';
export const MESSAGE_TYPE_AUDIO = 'audio';
export const MESSAGE_TYPE_IMAGE = 'image';
export const MESSAGE_TYPE_TEMPLATE = 'template';
export const TEMPLATE_TYPE_BUTTONS = 'buttons';
export const ACTION_TYPE_MESSAGE = 'message';
export const ACTION_TYPE_POSTBACK = 'postback';
export const QUICK_REPLY_TYPE_ACTION = 'action';
const client = axios.create({
baseURL: 'https://api.line.me',
timeout: config.LINE_TIMEOUT,
headers: {
'Accept-Encoding': 'gzip, deflate, compress',
},
});
client.interceptors.request.use((c) => {
c.headers.Authorization = `Bearer ${config.LINE_CHANNEL_ACCESS_TOKEN}`;
return handleRequest(c);
});
client.interceptors.response.use(handleFulfilled, (err) => {
if (err.response?.data?.message) {
err.message = err.response.data.message;
}
return handleRejected(err);
});
const reply = ({
replyToken,
messages,
}) => client.post('/v2/bot/message/reply', {
replyToken,
messages,
});
const fetchGroupSummary = ({
groupId,
}) => client.get(`/v2/bot/group/${groupId}/summary`);
const fetchProfile = ({
userId,
}) => client.get(`/v2/bot/profile/${userId}`);
const dataClient = axios.create({
baseURL: 'https://api-data.line.me',
timeout: config.LINE_TIMEOUT,
headers: {
'Accept-Encoding': 'gzip, deflate, compress',
},
});
dataClient.interceptors.request.use((c) => {
c.headers.Authorization = `Bearer ${config.LINE_CHANNEL_ACCESS_TOKEN}`;
return handleRequest(c);
});
dataClient.interceptors.response.use(handleFulfilled, (err) => {
if (err.response?.data?.message) {
err.message = err.response.data.message;
}
return handleRejected(err);
});
const fetchContent = ({
messageId,
}) => dataClient.get(`/v2/bot/message/${messageId}/content`, {
responseType: 'arraybuffer',
});
export {
reply,
fetchGroupSummary,
fetchProfile,
fetchContent,
};
================================================
FILE: services/openai.js
================================================
import axios from 'axios';
import FormData from 'form-data';
import config from '../config/index.js';
import { handleFulfilled, handleRejected, handleRequest } from './utils/index.js';
export const ROLE_SYSTEM = 'system';
export const ROLE_AI = 'assistant';
export const ROLE_HUMAN = 'user';
export const FINISH_REASON_STOP = 'stop';
export const FINISH_REASON_LENGTH = 'length';
export const IMAGE_SIZE_256 = '256x256';
export const IMAGE_SIZE_512 = '512x512';
export const IMAGE_SIZE_1024 = '1024x1024';
export const MODEL_GPT_3_5_TURBO = 'gpt-3.5-turbo';
export const MODEL_GPT_4_OMNI = 'gpt-4o';
export const MODEL_WHISPER_1 = 'whisper-1';
export const MODEL_DALL_E_3 = 'dall-e-3';
const client = axios.create({
baseURL: config.OPENAI_BASE_URL,
timeout: config.OPENAI_TIMEOUT,
headers: {
'Accept-Encoding': 'gzip, deflate, compress',
},
});
client.interceptors.request.use((c) => {
c.headers.Authorization = `Bearer ${config.OPENAI_API_KEY}`;
return handleRequest(c);
});
client.interceptors.response.use(handleFulfilled, (err) => {
if (err.response?.data?.error?.message) {
err.message = err.response.data.error.message;
}
return handleRejected(err);
});
const hasImage = ({ messages }) => (
messages.some(({ content }) => (
Array.isArray(content) && content.some((item) => item.image_url)
))
);
const createChatCompletion = ({
model = config.OPENAI_COMPLETION_MODEL,
messages,
temperature = config.OPENAI_COMPLETION_TEMPERATURE,
maxTokens = config.OPENAI_COMPLETION_MAX_TOKENS,
frequencyPenalty = config.OPENAI_COMPLETION_FREQUENCY_PENALTY,
presencePenalty = config.OPENAI_COMPLETION_PRESENCE_PENALTY,
}) => {
const body = {
model: hasImage({ messages }) ? config.OPENAI_VISION_MODEL : model,
messages,
temperature,
max_tokens: maxTokens,
frequency_penalty: frequencyPenalty,
presence_penalty: presencePenalty,
};
return client.post('/v1/chat/completions', body);
};
const createImage = ({
model = config.OPENAI_IMAGE_GENERATION_MODEL,
prompt,
size = config.OPENAI_IMAGE_GENERATION_SIZE,
quality = config.OPENAI_IMAGE_GENERATION_QUALITY,
n = 1,
}) => {
// set image size to 1024 when using the DALL-E 3 model and the requested size is 256 or 512.
if (model === MODEL_DALL_E_3 && [IMAGE_SIZE_256, IMAGE_SIZE_512].includes(size)) {
size = IMAGE_SIZE_1024;
}
return client.post('/v1/images/generations', {
model,
prompt,
size,
quality,
n,
});
};
const createAudioTranscriptions = ({
buffer,
file,
model = MODEL_WHISPER_1,
}) => {
const formData = new FormData();
formData.append('file', buffer, file);
formData.append('model', model);
return client.post('/v1/audio/transcriptions', formData.getBuffer(), {
headers: formData.getHeaders(),
});
};
export {
createAudioTranscriptions,
createChatCompletion,
createImage,
};
================================================
FILE: services/serpapi.js
================================================
import axios from 'axios';
import config from '../config/index.js';
import { handleFulfilled, handleRejected, handleRequest } from './utils/index.js';
const client = axios.create({
baseURL: 'https://serpapi.com',
timeout: config.SERPAPI_TIMEOUT,
headers: {
'Accept-Encoding': 'gzip, deflate, compress',
},
});
client.interceptors.request.use((c) => {
c.params = {
key: config.SERPAPI_API_KEY,
...c.params,
};
return handleRequest(c);
});
client.interceptors.response.use(handleFulfilled, (err) => {
if (err.response?.data?.error) {
err.message = err.response.data.error;
}
return handleRejected(err);
});
const search = ({
gl = config.SERPAPI_LOCATION,
q,
}) => client.get('/search', {
params: {
gl,
q,
},
});
export {
search,
};
export default null;
================================================
FILE: services/utils/index.js
================================================
import config from '../../config/index.js';
const handleRequest = (c) => {
c.metadata = { startTime: new Date() };
return c;
};
const handleFulfilled = (response) => {
if (config.APP_DEBUG) console.info(`[${response.status}] ${response.config.method.toUpperCase()} ${response.config.baseURL}${response.config.url} (${(new Date() - response.config.metadata.startTime)}ms)`);
return response;
};
const handleRejected = (err) => {
if (config.APP_DEBUG) console.info(`[${err.response?.status || 500}] ${err.config.method.toUpperCase()} ${err.config.baseURL}${err.config.url} (${(new Date() - err.config.metadata.startTime)}ms)`);
return Promise.reject(err);
};
export {
handleRequest,
handleFulfilled,
handleRejected,
};
================================================
FILE: services/vercel.js
================================================
import axios from 'axios';
import config from '../config/index.js';
import { handleFulfilled, handleRejected, handleRequest } from './utils/index.js';
export const ENV_TYPE_ENCRYPTED = 'encrypted';
export const ENV_TYPE_PLAIN = 'plain';
export const ENV_TARGET_PRODUCTION = 'production';
export const ENV_TARGET_PREVIEW = 'preview';
export const ENV_TARGET_DEVELOPMENT = 'development';
const client = axios.create({
baseURL: 'https://api.vercel.com',
timeout: config.VERCEL_TIMEOUT,
headers: {
'Accept-Encoding': 'gzip, deflate, compress',
},
});
client.interceptors.request.use((c) => {
c.headers.Authorization = `Bearer ${config.VERCEL_ACCESS_TOKEN}`;
return handleRequest(c);
});
client.interceptors.response.use(handleFulfilled, (err) => {
if (err.response?.data?.error?.message) {
err.message = err.response.data.error.message;
}
return handleRejected(err);
});
const fetchEnvironments = () => client.get(`/v9/projects/${config.VERCEL_PROJECT_NAME}/env`, {
params: {
...(config.VERCEL_TEAM_ID ? { teamId: config.VERCEL_TEAM_ID } : {}),
},
});
const createEnvironment = ({
key,
value,
type = ENV_TYPE_ENCRYPTED,
target = [ENV_TARGET_PRODUCTION, ENV_TARGET_PREVIEW, ENV_TARGET_DEVELOPMENT],
}) => client.post(`/v10/projects/${config.VERCEL_PROJECT_NAME}/env`, {
key: String(key),
value: String(value),
type,
target,
}, {
params: {
...(config.VERCEL_TEAM_ID ? { teamId: config.VERCEL_TEAM_ID } : {}),
},
});
const updateEnvironment = ({
id,
value,
type = ENV_TYPE_ENCRYPTED,
target = [ENV_TARGET_PRODUCTION, ENV_TARGET_PREVIEW, ENV_TARGET_DEVELOPMENT],
}) => client.patch(`/v9/projects/${config.VERCEL_PROJECT_NAME}/env/${id}`, {
value: String(value),
type,
target,
}, {
params: {
...(config.VERCEL_TEAM_ID ? { teamId: config.VERCEL_TEAM_ID } : {}),
},
});
const deploy = () => axios.post(config.VERCEL_DEPLOY_HOOK_URL);
export {
fetchEnvironments,
createEnvironment,
updateEnvironment,
deploy,
};
================================================
FILE: storage/index.js
================================================
import config from '../config/index.js';
import { createEnvironment, ENV_TYPE_PLAIN, updateEnvironment } from '../services/vercel.js';
import { fetchEnvironment } from '../utils/index.js';
const ENV_KEY = 'APP_STORAGE';
class Storage {
env;
data = {};
async initialize() {
if (!config.VERCEL_ACCESS_TOKEN) return;
this.env = await fetchEnvironment(ENV_KEY);
if (!this.env) {
const { data } = await createEnvironment({
key: ENV_KEY,
value: JSON.stringify(this.data),
type: ENV_TYPE_PLAIN,
});
this.env = data.created;
}
this.data = JSON.parse(this.env.value);
}
/**
* @param {string} key
* @returns {string}
*/
getItem(key) {
return this.data[key];
}
/**
* @param {string} key
* @param {string} value
*/
async setItem(key, value) {
this.data[key] = value;
if (!config.VERCEL_ACCESS_TOKEN) return;
await updateEnvironment({
id: this.env.id,
value: JSON.stringify(this.data, null, config.VERCEL_ENV ? 0 : 2),
type: ENV_TYPE_PLAIN,
});
}
}
const storage = new Storage();
export default storage;
================================================
FILE: tests/activate.test.js
================================================
import {
afterEach, beforeEach, expect, test,
} from '@jest/globals';
import { getPrompt, handleEvents, removePrompt } from '../app/index.js';
import { COMMAND_BOT_ACTIVATE, COMMAND_BOT_DEACTIVATE } from '../app/commands/index.js';
import { t } from '../locales/index.js';
import {
createEvents, MOCK_TEXT_OK, MOCK_USER_01, TIMEOUT,
} from './utils.js';
beforeEach(async () => {
const events = [
...createEvents([COMMAND_BOT_DEACTIVATE.text]),
];
await handleEvents(events);
});
afterEach(() => {
removePrompt(MOCK_USER_01);
});
test('COMMAND_BOT_ACTIVATE', async () => {
const events = [
...createEvents(['嗨!']), // should be ignored
...createEvents([COMMAND_BOT_ACTIVATE.text]),
...createEvents(['嗨!']),
];
let results;
try {
results = await handleEvents(events);
} catch (err) {
console.error(err);
}
expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5);
const replies = results.map(({ messages }) => messages.map(({ text }) => text));
expect(replies).toEqual(
[
[
t('__ERROR_MISSING_ENV')('VERCEL_ACCESS_TOKEN'),
COMMAND_BOT_ACTIVATE.reply,
],
[MOCK_TEXT_OK],
],
);
}, TIMEOUT);
================================================
FILE: tests/command.test.js
================================================
import {
afterEach,
beforeEach,
expect,
test,
} from '@jest/globals';
import { getPrompt, handleEvents, removePrompt } from '../app/index.js';
import { COMMAND_SYS_COMMAND } from '../app/commands/index.js';
import { createEvents, TIMEOUT, MOCK_USER_01 } from './utils.js';
beforeEach(() => {
//
});
afterEach(() => {
removePrompt(MOCK_USER_01);
});
test('COMMAND_SYS_COMMAND', async () => {
const events = [
...createEvents([`${COMMAND_SYS_COMMAND.text}`]),
];
let results;
try {
results = await handleEvents(events);
} catch (err) {
console.error(err);
}
expect(getPrompt(MOCK_USER_01).messages.length).toEqual(3);
const replies = results.map(({ messages }) => messages.map(({ altText }) => altText));
expect(replies).toEqual(
[
[COMMAND_SYS_COMMAND.label],
],
);
}, TIMEOUT);
================================================
FILE: tests/continue.test.js
================================================
import {
afterEach, beforeEach, expect, test,
} from '@jest/globals';
import { getPrompt, handleEvents, removePrompt } from '../app/index.js';
import { COMMAND_BOT_CONTINUE } from '../app/commands/index.js';
import {
createEvents, TIMEOUT, MOCK_USER_01, MOCK_TEXT_OK,
} from './utils.js';
beforeEach(() => {
//
});
afterEach(() => {
removePrompt(MOCK_USER_01);
});
test('COMMAND_BOT_CONTINUE', async () => {
const events = [
...createEvents([COMMAND_BOT_CONTINUE.text]),
];
let results;
try {
results = await handleEvents(events);
} catch (err) {
console.error(err);
}
expect(getPrompt(MOCK_USER_01).messages.length).toEqual(3);
const replies = results.map(({ messages }) => messages.map(({ text }) => text));
expect(replies).toEqual(
[
[MOCK_TEXT_OK],
],
);
}, TIMEOUT);
================================================
FILE: tests/deactivate.test.js
================================================
import {
afterEach, beforeEach, expect, test,
} from '@jest/globals';
import { getPrompt, handleEvents, removePrompt } from '../app/index.js';
import { COMMAND_BOT_ACTIVATE, COMMAND_BOT_DEACTIVATE } from '../app/commands/index.js';
import { t } from '../locales/index.js';
import storage from '../storage/index.js';
import {
createEvents, MOCK_TEXT_OK, MOCK_USER_01, TIMEOUT,
} from './utils.js';
beforeEach(async () => {
const events = [
...createEvents([COMMAND_BOT_ACTIVATE.text]),
];
await handleEvents(events);
});
afterEach(() => {
removePrompt(MOCK_USER_01);
});
test('COMMAND_BOT_DEACTIVATE', async () => {
const events = [
...createEvents(['嗨!']),
...createEvents([COMMAND_BOT_DEACTIVATE.text]),
...createEvents(['嗨!']), // should be ignored
];
let results;
try {
results = await handleEvents(events);
} catch (err) {
console.error(err);
}
expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5);
const replies = results.map(({ messages }) => messages.map(({ text }) => text));
expect(replies).toEqual(
[
[MOCK_TEXT_OK],
[
t('__ERROR_MISSING_ENV')('VERCEL_ACCESS_TOKEN'),
COMMAND_BOT_DEACTIVATE.reply,
],
],
);
}, TIMEOUT);
================================================
FILE: tests/default.test.js
================================================
import {
afterEach, beforeEach, expect, test,
} from '@jest/globals';
import {
getPrompt, handleEvents, removePrompt, printHistories,
} from '../app/index.js';
import config from '../config/index.js';
import {
createEvents, TIMEOUT, MOCK_USER_01, MOCK_TEXT_OK,
} from './utils.js';
beforeEach(() => {
//
});
afterEach(() => {
removePrompt(MOCK_USER_01);
});
test('DEFAULT', async () => {
const events = [
...createEvents(['嗨!']),
];
let results;
try {
results = await handleEvents(events);
} catch (err) {
console.error(err);
}
if (config.APP_DEBUG) printHistories();
expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5);
const replies = results.map(({ messages }) => messages.map(({ text }) => text));
expect(replies).toEqual(
[
[MOCK_TEXT_OK],
],
);
}, TIMEOUT);
================================================
FILE: tests/draw.test.js
================================================
import {
afterEach, beforeEach, expect, test,
} from '@jest/globals';
import { handleEvents, getPrompt, removePrompt } from '../app/index.js';
import { COMMAND_BOT_DRAW } from '../app/commands/index.js';
import {
createEvents, TIMEOUT, MOCK_USER_01, MOCK_TEXT_OK,
} from './utils.js';
beforeEach(() => {
//
});
afterEach(() => {
removePrompt(MOCK_USER_01);
});
test('COMMAND_BOT_DRAW', async () => {
const events = [
...createEvents([`${COMMAND_BOT_DRAW.text}人工智慧`]),
];
let results;
try {
results = await handleEvents(events);
} catch (err) {
console.error(err);
}
expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5);
const replies = results.map(({ messages }) => messages.map(({ originalContentUrl }) => originalContentUrl));
expect(replies).toEqual(
[
[MOCK_TEXT_OK],
],
);
}, TIMEOUT);
================================================
FILE: tests/enquire.test.js
================================================
import {
afterEach, beforeEach, expect, test,
} from '@jest/globals';
import { COMMAND_BOT_TALK, COMMAND_SUM_SUM } from '../app/commands/index.js';
import { getPrompt, handleEvents, removePrompt } from '../app/index.js';
import { MOCK_GROUP_01 } from '../constants/mock.js';
import {
createEvents, MOCK_TEXT_OK, MOCK_USER_01, MOCK_USER_02, TIMEOUT,
} from './utils.js';
beforeEach(async () => {
//
});
afterEach(() => {
removePrompt(MOCK_USER_01);
removePrompt(MOCK_USER_02);
});
test('COMMAND_ENQUIRE', async () => {
try {
await handleEvents(createEvents([`${COMMAND_BOT_TALK.text}人工智慧`], MOCK_GROUP_01, MOCK_USER_01));
} catch (err) {
console.error(err);
}
const events = [
...createEvents([`${COMMAND_SUM_SUM.text}`], MOCK_GROUP_01, MOCK_USER_02),
];
let results;
try {
results = await handleEvents(events);
} catch (err) {
console.error(err);
}
expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5);
expect(getPrompt(MOCK_USER_02).messages.length).toEqual(6);
const replies = results.map(({ messages }) => messages.map(({ text }) => text));
expect(replies).toEqual(
[
[MOCK_TEXT_OK],
],
);
}, TIMEOUT);
================================================
FILE: tests/summon.test.js
================================================
import {
afterEach, beforeEach, expect, test,
} from '@jest/globals';
import { getPrompt, handleEvents, removePrompt } from '../app/index.js';
import config from '../config/index.js';
import {
createEvents, MOCK_TEXT_OK, MOCK_USER_01, TIMEOUT,
} from './utils.js';
beforeEach(() => {
//
});
afterEach(() => {
removePrompt(MOCK_USER_01);
});
test('COMMAND_BOT_SUMMON', async () => {
const events = [
...createEvents([`${config.BOT_NAME} 你好`]),
];
let results;
try {
results = await handleEvents(events);
} catch (err) {
console.error(err);
}
expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5);
const replies = results.map(({ messages }) => messages.map(({ text }) => text));
expect(replies).toEqual(
[
[MOCK_TEXT_OK],
],
);
}, TIMEOUT);
================================================
FILE: tests/talk.test.js
================================================
import {
afterEach, beforeEach, expect, test,
} from '@jest/globals';
import { getPrompt, handleEvents, removePrompt } from '../app/index.js';
import { COMMAND_BOT_TALK } from '../app/commands/index.js';
import {
createEvents, TIMEOUT, MOCK_USER_01, MOCK_TEXT_OK,
} from './utils.js';
beforeEach(() => {
//
});
afterEach(() => {
removePrompt(MOCK_USER_01);
});
test('COMMAND_BOT_TALK', async () => {
const events = [
...createEvents([`${COMMAND_BOT_TALK.text}人工智慧`]),
];
let results;
try {
results = await handleEvents(events);
} catch (err) {
console.error(err);
}
expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5);
const replies = results.map(({ messages }) => messages.map(({ text }) => text));
expect(replies).toEqual(
[
[MOCK_TEXT_OK],
],
);
}, TIMEOUT);
================================================
FILE: tests/utils.js
================================================
import Event from '../app/models/event.js';
import { MOCK_TEXT_OK, MOCK_USER_01, MOCK_USER_02 } from '../constants/mock.js';
import {
EVENT_TYPE_MESSAGE, MESSAGE_TYPE_TEXT, SOURCE_TYPE_GROUP, SOURCE_TYPE_USER,
} from '../services/line.js';
export const TIMEOUT = 9 * 1000;
const createEvents = (
messages,
groupId,
userId = MOCK_USER_01,
) => messages.map((text) => new Event({
replyToken: '',
type: EVENT_TYPE_MESSAGE,
source: { type: groupId ? SOURCE_TYPE_GROUP : SOURCE_TYPE_USER, userId, groupId },
message: { type: MESSAGE_TYPE_TEXT, text },
}));
export {
MOCK_TEXT_OK,
MOCK_USER_01,
MOCK_USER_02,
createEvents,
};
================================================
FILE: tests/version.test.js
================================================
import {
afterEach, beforeEach, expect, test,
} from '@jest/globals';
import { getPrompt, handleEvents, removePrompt } from '../app/index.js';
import { COMMAND_SYS_VERSION } from '../app/commands/index.js';
import { t } from '../locales/index.js';
import { fetchVersion, getVersion } from '../utils/index.js';
import { createEvents, MOCK_USER_01, TIMEOUT } from './utils.js';
beforeEach(() => {
//
});
afterEach(() => {
removePrompt(MOCK_USER_01);
});
test('COMMAND_SYS_VERSION', async () => {
const events = [
...createEvents([COMMAND_SYS_VERSION.text]),
];
let results;
try {
results = await handleEvents(events);
} catch (err) {
console.error(err);
}
const current = getVersion();
const latest = await fetchVersion();
const isLatest = current === latest;
expect(getPrompt(MOCK_USER_01).messages.length).toEqual(3);
const replies = results.map(({ messages }) => messages.map(({ text }) => text));
expect(replies).toEqual(
[
[t('__COMMAND_SYS_VERSION_REPLY')(current, isLatest)],
],
);
}, TIMEOUT);
================================================
FILE: utils/add-mark.js
================================================
import config from '../config/index.js';
const addMark = (text) => {
if (!text) return text;
const marks = ['?', '。', '!', '?', '.', '!'];
if (marks.some((mark) => text.endsWith(mark))) {
return text;
}
switch (config.APP_LANG) {
case 'zh':
case 'zh_TW':
case 'zh_CN':
case 'ja':
return `${text}。`;
default:
return `${text}.`;
}
};
export default addMark;
================================================
FILE: utils/convert-text.js
================================================
import * as OpenCC from 'opencc-js';
import config from '../config/index.js';
const convertText = (text) => {
if (config.APP_LANG === 'zh_TW') {
const converter = OpenCC.Converter({ from: 'cn', to: 'tw' });
return converter(text);
}
if (config.APP_LANG === 'zh_CN') {
const converter = OpenCC.Converter({ from: 'tw', to: 'cn' });
return converter(text);
}
return text;
};
export default convertText;
================================================
FILE: utils/fetch-answer.js
================================================
import config from '../config/index.js';
import { search } from '../services/serpapi.js';
class OrganicResult {
answer;
constructor({
answer,
} = {}) {
this.answer = answer;
}
}
const fetchAnswer = async (q) => {
if (config.APP_ENV !== 'production' || !config.SERPAPI_API_KEY) return new OrganicResult();
const res = await search({ q });
const { answer_box: answerBox, knowledge_graph: knowledgeGraph, organic_results: organicResults } = res.data;
let answer = organicResults[0].snippet;
if (answerBox?.answer) answer += answerBox.answer;
if (answerBox?.result) answer += answerBox.result;
if (answerBox?.snippet) answer += answerBox.snippet;
if (knowledgeGraph?.description) answer += `${knowledgeGraph.title} - ${knowledgeGraph.description}`;
return new OrganicResult({ answer });
};
export default fetchAnswer;
================================================
FILE: utils/fetch-audio.js
================================================
import { fetchContent } from '../services/line.js';
/**
* @param {string} messageId
* @returns {Promise}
*/
const fetchAudio = async (messageId) => {
const { data } = await fetchContent({ messageId });
return Buffer.from(data, 'binary');
};
export default fetchAudio;
================================================
FILE: utils/fetch-environment.js
================================================
import { fetchEnvironments } from '../services/vercel.js';
const fetchEnvironment = async (key) => {
const { data } = await fetchEnvironments();
return data.envs.find((env) => env.key === key);
};
export default fetchEnvironment;
================================================
FILE: utils/fetch-group.js
================================================
import config from '../config/index.js';
import { mockGroups } from '../constants/mock.js';
import { t } from '../locales/index.js';
import { fetchGroupSummary } from '../services/line.js';
class Group {
groupName;
constructor({
groupName,
}) {
this.groupName = groupName;
}
}
/**
* @param {string} groupId
* @returns {Promise}
*/
const fetchGroup = async (groupId) => {
if (config.APP_ENV !== 'production') return new Group(mockGroups[groupId]);
try {
const { data } = await fetchGroupSummary({ groupId });
return new Group(data);
} catch {
return new Group({ groupName: t('__SOURCE_NAME_SOME_GROUP') });
}
};
export default fetchGroup;
================================================
FILE: utils/fetch-image.js
================================================
import { fetchContent } from '../services/line.js';
/**
* @param {string} messageId
* @returns {Promise}
*/
const fetchImage = async (messageId) => {
const { data } = await fetchContent({ messageId });
return `data:image/jpeg;base64,${Buffer.from(data, 'binary').toString('base64')}`;
};
export default fetchImage;
================================================
FILE: utils/fetch-user.js
================================================
import config from '../config/index.js';
import { mockUsers } from '../constants/mock.js';
import { t } from '../locales/index.js';
import { fetchProfile } from '../services/line.js';
class User {
displayName;
constructor({
displayName,
}) {
this.displayName = displayName;
}
}
/**
* @param {string} userId
* @returns {Promise}
*/
const fetchUser = async (userId) => {
if (config.APP_ENV !== 'production') return new User(mockUsers[userId]);
try {
const { data } = await fetchProfile({ userId });
return new User(data);
} catch {
return new User({ displayName: t('__SOURCE_NAME_SOMEONE') });
}
};
export default fetchUser;
================================================
FILE: utils/fetch-version.js
================================================
import axios from 'axios';
/**
* @returns {Promise}
*/
const fetchVersion = async () => {
const { data } = await axios.get('https://raw.githubusercontent.com/memochou1993/gpt-ai-assistant/main/package.json');
return data.version;
};
export default fetchVersion;
================================================
FILE: utils/generate-completion.js
================================================
import config from '../config/index.js';
import { MOCK_TEXT_OK } from '../constants/mock.js';
import { createChatCompletion, FINISH_REASON_STOP } from '../services/openai.js';
class Completion {
text;
finishReason;
constructor({
text,
finishReason,
}) {
this.text = text;
this.finishReason = finishReason;
}
get isFinishReasonStop() {
return this.finishReason === FINISH_REASON_STOP;
}
}
/**
* @param {Object} param
* @param {Prompt} param.prompt
* @returns {Promise}
*/
const generateCompletion = async ({
prompt,
}) => {
if (config.APP_ENV !== 'production') return new Completion({ text: MOCK_TEXT_OK });
const { data } = await createChatCompletion({ messages: prompt.messages });
const [choice] = data.choices;
return new Completion({
text: choice.message.content.trim(),
finishReason: choice.finish_reason,
});
};
export default generateCompletion;
================================================
FILE: utils/generate-image.js
================================================
import config from '../config/index.js';
import { MOCK_TEXT_OK } from '../constants/mock.js';
import { createImage } from '../services/openai.js';
class Image {
url;
constructor({
url,
}) {
this.url = url;
}
}
/**
* @param {Object} param
* @param {string} param.prompt
* @returns {Promise}
*/
const generateImage = async ({
prompt,
}) => {
if (config.APP_ENV !== 'production') return new Image({ url: MOCK_TEXT_OK });
const { data } = await createImage({ prompt });
const [image] = data.data;
return new Image(image);
};
export default generateImage;
================================================
FILE: utils/generate-transcription.js
================================================
import { createAudioTranscriptions } from '../services/openai.js';
class Transcription {
text;
constructor({
text,
}) {
this.text = text;
}
}
/**
* @param {Object} param
* @param {Buffer} param.buffer
* @param {string} param.file
* @returns {Promise}
*/
const generateTranscription = async ({
buffer,
file,
}) => {
const { data } = await createAudioTranscriptions({ buffer, file });
return new Transcription(data);
};
export default generateTranscription;
================================================
FILE: utils/get-command.js
================================================
import { Command, ALL_COMMANDS } from '../app/commands/index.js';
/**
* @param {string} text
* @returns {Command}
*/
const getCommand = (text) => (
Object.values(ALL_COMMANDS)
.sort((a, b) => b.text.length - a.text.length)
.find((c) => (
c.aliases.includes(text) || text.toLowerCase().includes(c.text.toLowerCase())
))
);
export default getCommand;
================================================
FILE: utils/get-version.js
================================================
import fs from 'fs';
/**
* @returns {string}
*/
const getVersion = () => JSON.parse(fs.readFileSync('package.json')).version;
export default getVersion;
================================================
FILE: utils/index.js
================================================
import addMark from './add-mark.js';
import convertText from './convert-text.js';
import fetchAnswer from './fetch-answer.js';
import fetchAudio from './fetch-audio.js';
import fetchImage from './fetch-image.js';
import fetchEnvironment from './fetch-environment.js';
import fetchGroup from './fetch-group.js';
import fetchUser from './fetch-user.js';
import fetchVersion from './fetch-version.js';
import generateCompletion from './generate-completion.js';
import generateImage from './generate-image.js';
import generateTranscription from './generate-transcription.js';
import getCommand from './get-command.js';
import getVersion from './get-version.js';
import replyMessage from './reply-message.js';
import validateSignature from './validate-signature.js';
export {
addMark,
convertText,
fetchAnswer,
fetchAudio,
fetchImage,
fetchEnvironment,
fetchGroup,
fetchUser,
fetchVersion,
generateCompletion,
generateImage,
generateTranscription,
getCommand,
getVersion,
replyMessage,
validateSignature,
};
================================================
FILE: utils/reply-message.js
================================================
import config from '../config/index.js';
import { reply } from '../services/line.js';
const replyMessage = ({
replyToken,
messages,
}) => {
if (config.APP_ENV !== 'production') return { replyToken, messages };
return reply({ replyToken, messages });
};
export default replyMessage;
================================================
FILE: utils/validate-signature.js
================================================
import {
createHmac,
timingSafeEqual,
} from 'crypto';
const s2b = (str, encoding) => Buffer.from(str, encoding);
const safeCompare = (a, b) => {
if (a.length !== b.length) {
return false;
}
return timingSafeEqual(a, b);
};
const validateSignature = (
body,
secret,
signature,
) => safeCompare(
createHmac('SHA256', secret).update(body).digest(),
s2b(signature, 'base64'),
);
export default validateSignature;
================================================
FILE: vercel.json
================================================
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/api"
}
],
"functions": {
"api/**/*": {
"maxDuration": 10
}
}
}