├── .dockerignore ├── .npmrc ├── .gitignore ├── demo └── labot.png ├── app ├── actions │ ├── action.js │ ├── index.js │ └── message.js ├── models │ ├── bot.js │ ├── index.js │ ├── source.js │ └── event.js ├── repository │ ├── index.js │ └── source.js ├── messages │ ├── index.js │ ├── text.js │ ├── image.js │ ├── message.js │ └── template.js ├── commands │ ├── bot-retry.js │ ├── bot-draw-demo.js │ ├── bot-talk-demo.js │ ├── bot-search-demo.js │ ├── sys-report.js │ ├── sys-command.js │ ├── bot-summon-demo.js │ ├── sum-sum.js │ ├── sys-doc.js │ ├── bot-talk.js │ ├── sum-blame.js │ ├── sum-laugh.js │ ├── sum-advise.js │ ├── bot-search.js │ ├── sum-comfort.js │ ├── sys-deploy.js │ ├── sys-version.js │ ├── bot-continue.js │ ├── sum-complain.js │ ├── sum-apologize.js │ ├── sum-encourage.js │ ├── analyze-analyze.js │ ├── bot-draw.js │ ├── bot-forget.js │ ├── analyze-literarily.js │ ├── bot-activate.js │ ├── bot-deactivate.js │ ├── command.js │ ├── translate-to-en.js │ ├── translate-to-ja.js │ ├── analyze-mathematically.js │ ├── analyze-numerologically.js │ ├── analyze-philosophically.js │ ├── analyze-psychologically.js │ └── index.js ├── history │ ├── message.js │ ├── index.js │ └── history.js ├── index.js ├── prompt │ ├── message.js │ ├── index.js │ └── prompt.js ├── handlers │ ├── doc.js │ ├── forget.js │ ├── report.js │ ├── index.js │ ├── deploy.js │ ├── command.js │ ├── version.js │ ├── activate.js │ ├── deactivate.js │ ├── draw.js │ ├── retry.js │ ├── continue.js │ ├── talk.js │ ├── search.js │ └── enquire.js ├── app.js └── context.js ├── babel.config.cjs ├── Dockerfile ├── middleware ├── index.js └── validate-line-signature.js ├── constants ├── command.js └── mock.js ├── .github └── ISSUE_TEMPLATE │ └── question.md ├── docker-compose.yaml ├── utils ├── get-version.js ├── fetch-environment.js ├── fetch-version.js ├── fetch-audio.js ├── reply-message.js ├── fetch-image.js ├── get-command.js ├── add-mark.js ├── convert-text.js ├── validate-signature.js ├── generate-transcription.js ├── generate-image.js ├── fetch-user.js ├── fetch-group.js ├── fetch-answer.js ├── generate-completion.js └── index.js ├── vercel.json ├── locales ├── index.js ├── zh.js ├── ja.js └── en.js ├── .eslintrc.cjs ├── package.json ├── tests ├── utils.js ├── summon.test.js ├── talk.test.js ├── continue.test.js ├── command.test.js ├── default.test.js ├── draw.test.js ├── version.test.js ├── enquire.test.js ├── activate.test.js └── deactivate.test.js ├── services ├── utils │ └── index.js ├── serpapi.js ├── vercel.js ├── line.js └── openai.js ├── .env.example ├── LICENSE ├── api └── index.js ├── storage └── index.js ├── README.md ├── config └── index.js └── CHANGELOG.md /.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .env 4 | .env.* 5 | -------------------------------------------------------------------------------- /demo/labot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todo/gpt-ai-assistant/main/demo/labot.png -------------------------------------------------------------------------------- /app/actions/action.js: -------------------------------------------------------------------------------- 1 | class Action { 2 | type; 3 | } 4 | 5 | export default Action; 6 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN npm ci --only=production 8 | 9 | CMD [ "npm", "start" ] 10 | -------------------------------------------------------------------------------- /app/actions/index.js: -------------------------------------------------------------------------------- 1 | import Action from './action.js'; 2 | import MessageAction from './message.js'; 3 | 4 | export { 5 | Action, 6 | MessageAction, 7 | }; 8 | -------------------------------------------------------------------------------- /middleware/index.js: -------------------------------------------------------------------------------- 1 | import validateLineSignature from './validate-line-signature.js'; 2 | 3 | export { 4 | validateLineSignature, 5 | }; 6 | 7 | export default null; 8 | -------------------------------------------------------------------------------- /constants/command.js: -------------------------------------------------------------------------------- 1 | export const TYPE_SUM = 'sum'; 2 | export const TYPE_ANALYZE = 'analyze'; 3 | export const TYPE_SYSTEM = 'system'; 4 | export const TYPE_TRANSLATE = 'translate'; 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about this project 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/models/bot.js: -------------------------------------------------------------------------------- 1 | class Bot { 2 | isActivated; 3 | 4 | constructor({ 5 | isActivated, 6 | }) { 7 | this.isActivated = isActivated; 8 | } 9 | } 10 | 11 | export default Bot; 12 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | app: 5 | container_name: gpt-ai-assistant 6 | build: . 7 | restart: always 8 | ports: 9 | - "${APP_PORT}:${APP_PORT}" 10 | -------------------------------------------------------------------------------- /utils/get-version.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | /** 4 | * @returns {string} 5 | */ 6 | const getVersion = () => JSON.parse(fs.readFileSync('package.json')).version; 7 | 8 | export default getVersion; 9 | -------------------------------------------------------------------------------- /app/repository/index.js: -------------------------------------------------------------------------------- 1 | import { getSources, updateSources, setSources } from './source.js'; 2 | 3 | export { 4 | getSources, 5 | updateSources, 6 | setSources, 7 | }; 8 | 9 | export default null; 10 | -------------------------------------------------------------------------------- /app/models/index.js: -------------------------------------------------------------------------------- 1 | import Bot from './bot.js'; 2 | import Event from './event.js'; 3 | import Source from './source.js'; 4 | 5 | export { 6 | Bot, 7 | Event, 8 | Source, 9 | }; 10 | 11 | export default null; 12 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/api" 6 | } 7 | ], 8 | "functions": { 9 | "api/**/*": { 10 | "maxDuration": 10 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /utils/fetch-environment.js: -------------------------------------------------------------------------------- 1 | import { fetchEnvironments } from '../services/vercel.js'; 2 | 3 | const fetchEnvironment = async (key) => { 4 | const { data } = await fetchEnvironments(); 5 | return data.envs.find((env) => env.key === key); 6 | }; 7 | 8 | export default fetchEnvironment; 9 | -------------------------------------------------------------------------------- /app/messages/index.js: -------------------------------------------------------------------------------- 1 | import Message from './message.js'; 2 | import ImageMessage from './image.js'; 3 | import TemplateMessage from './template.js'; 4 | import TextMessage from './text.js'; 5 | 6 | export { 7 | Message, 8 | ImageMessage, 9 | TemplateMessage, 10 | TextMessage, 11 | }; 12 | -------------------------------------------------------------------------------- /app/commands/bot-retry.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_BOT_RETRY_LABEL'), 8 | text: t('__COMMAND_BOT_RETRY_TEXT'), 9 | }); 10 | -------------------------------------------------------------------------------- /app/history/message.js: -------------------------------------------------------------------------------- 1 | class Message { 2 | role; 3 | 4 | content; 5 | 6 | constructor({ 7 | role, 8 | content, 9 | }) { 10 | this.role = role; 11 | this.content = content; 12 | } 13 | 14 | toString() { 15 | return `${this.role}: ${this.content}`; 16 | } 17 | } 18 | 19 | export default Message; 20 | -------------------------------------------------------------------------------- /app/commands/bot-draw-demo.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_BOT_DRAW_DEMO_LABEL'), 8 | text: t('__COMMAND_BOT_DRAW_DEMO_TEXT'), 9 | }); 10 | -------------------------------------------------------------------------------- /app/commands/bot-talk-demo.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_BOT_TALK_DEMO_LABEL'), 8 | text: t('__COMMAND_BOT_TALK_DEMO_TEXT'), 9 | }); 10 | -------------------------------------------------------------------------------- /utils/fetch-version.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | /** 4 | * @returns {Promise} 5 | */ 6 | const fetchVersion = async () => { 7 | const { data } = await axios.get('https://raw.githubusercontent.com/memochou1993/gpt-ai-assistant/main/package.json'); 8 | return data.version; 9 | }; 10 | 11 | export default fetchVersion; 12 | -------------------------------------------------------------------------------- /app/commands/bot-search-demo.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_BOT_SEARCH_DEMO_LABEL'), 8 | text: t('__COMMAND_BOT_SEARCH_DEMO_TEXT'), 9 | }); 10 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import handleEvents from './app.js'; 2 | import { printHistories } from './history/index.js'; 3 | import { 4 | getPrompt, printPrompts, removePrompt, setPrompt, 5 | } from './prompt/index.js'; 6 | 7 | export { 8 | handleEvents, 9 | printHistories, 10 | getPrompt, 11 | printPrompts, 12 | removePrompt, 13 | setPrompt, 14 | }; 15 | -------------------------------------------------------------------------------- /utils/fetch-audio.js: -------------------------------------------------------------------------------- 1 | import { fetchContent } from '../services/line.js'; 2 | 3 | /** 4 | * @param {string} messageId 5 | * @returns {Promise} 6 | */ 7 | const fetchAudio = async (messageId) => { 8 | const { data } = await fetchContent({ messageId }); 9 | return Buffer.from(data, 'binary'); 10 | }; 11 | 12 | export default fetchAudio; 13 | -------------------------------------------------------------------------------- /utils/reply-message.js: -------------------------------------------------------------------------------- 1 | import config from '../config/index.js'; 2 | import { reply } from '../services/line.js'; 3 | 4 | const replyMessage = ({ 5 | replyToken, 6 | messages, 7 | }) => { 8 | if (config.APP_ENV !== 'production') return { replyToken, messages }; 9 | return reply({ replyToken, messages }); 10 | }; 11 | 12 | export default replyMessage; 13 | -------------------------------------------------------------------------------- /app/messages/text.js: -------------------------------------------------------------------------------- 1 | import { MESSAGE_TYPE_TEXT } from '../../services/line.js'; 2 | import Message from './message.js'; 3 | 4 | class TextMessage extends Message { 5 | type = MESSAGE_TYPE_TEXT; 6 | 7 | text; 8 | 9 | constructor({ 10 | text, 11 | }) { 12 | super(); 13 | this.text = text; 14 | } 15 | } 16 | 17 | export default TextMessage; 18 | -------------------------------------------------------------------------------- /locales/index.js: -------------------------------------------------------------------------------- 1 | import config from '../config/index.js'; 2 | import en from './en.js'; 3 | import ja from './ja.js'; 4 | import zh from './zh.js'; 5 | 6 | const locales = { 7 | en, 8 | ja, 9 | zh, 10 | zh_TW: zh, 11 | zh_CN: zh, 12 | }; 13 | 14 | const t = (key) => locales[config.APP_LANG][key]; 15 | 16 | export { 17 | t, 18 | }; 19 | 20 | export default null; 21 | -------------------------------------------------------------------------------- /utils/fetch-image.js: -------------------------------------------------------------------------------- 1 | import { fetchContent } from '../services/line.js'; 2 | 3 | /** 4 | * @param {string} messageId 5 | * @returns {Promise} 6 | */ 7 | const fetchImage = async (messageId) => { 8 | const { data } = await fetchContent({ messageId }); 9 | return `data:image/jpeg;base64,${Buffer.from(data, 'binary').toString('base64')}`; 10 | }; 11 | 12 | export default fetchImage; 13 | -------------------------------------------------------------------------------- /app/commands/sys-report.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_SYS_REPORT_LABEL'), 8 | text: t('__COMMAND_SYS_REPORT_TEXT'), 9 | aliases: [ 10 | '/report', 11 | 'Report', 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /app/commands/sys-command.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_SYS_COMMAND_LABEL'), 8 | text: t('__COMMAND_SYS_COMMAND_TEXT'), 9 | aliases: [ 10 | '/command', 11 | 'Command', 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /app/commands/bot-summon-demo.js: -------------------------------------------------------------------------------- 1 | import config from '../../config/index.js'; 2 | import { TYPE_SYSTEM } from '../../constants/command.js'; 3 | import { t } from '../../locales/index.js'; 4 | import Command from './command.js'; 5 | 6 | export default new Command({ 7 | type: TYPE_SYSTEM, 8 | label: t('__COMMAND_BOT_SUMMON_DEMO_LABEL'), 9 | text: `${config.BOT_NAME} ${t('__COMMAND_BOT_SUMMON_DEMO_TEXT')}`, 10 | }); 11 | -------------------------------------------------------------------------------- /app/commands/sum-sum.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SUM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SUM, 7 | label: t('__COMMAND_SUM_SUM_LABEL'), 8 | text: t('__COMMAND_SUM_SUM_TEXT'), 9 | prompt: t('__COMMAND_SUM_SUM_PROMPT'), 10 | aliases: [ 11 | '/sum', 12 | 'Sum', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/sys-doc.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_SYS_DOC_LABEL'), 8 | text: t('__COMMAND_SYS_DOC_TEXT'), 9 | reply: t('__COMMAND_SYS_DOC_REPLY'), 10 | aliases: [ 11 | '/doc', 12 | 'Doc', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/actions/message.js: -------------------------------------------------------------------------------- 1 | import { ACTION_TYPE_MESSAGE } from '../../services/line.js'; 2 | import Action from './action.js'; 3 | 4 | class MessageAction extends Action { 5 | type = ACTION_TYPE_MESSAGE; 6 | 7 | label; 8 | 9 | text; 10 | 11 | constructor({ 12 | label, 13 | text, 14 | }) { 15 | super(); 16 | this.label = label; 17 | this.text = text; 18 | } 19 | } 20 | 21 | export default MessageAction; 22 | -------------------------------------------------------------------------------- /app/commands/bot-talk.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_BOT_TALK_LABEL'), 8 | text: t('__COMMAND_BOT_TALK_TEXT'), 9 | aliases: [ 10 | ...t('__COMMAND_BOT_TALK_ALIASES'), 11 | '/talk', 12 | 'Talk', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/sum-blame.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SUM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SUM, 7 | label: t('__COMMAND_SUM_BLAME_LABEL'), 8 | text: t('__COMMAND_SUM_BLAME_TEXT'), 9 | prompt: t('__COMMAND_SUM_BLAME_PROMPT'), 10 | aliases: [ 11 | '/blame', 12 | 'Blame', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/sum-laugh.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SUM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SUM, 7 | label: t('__COMMAND_SUM_LAUGH_LABEL'), 8 | text: t('__COMMAND_SUM_LAUGH_TEXT'), 9 | prompt: t('__COMMAND_SUM_LAUGH_PROMPT'), 10 | aliases: [ 11 | '/laugh', 12 | 'Laugh', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/sum-advise.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SUM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SUM, 7 | label: t('__COMMAND_SUM_ADVISE_LABEL'), 8 | text: t('__COMMAND_SUM_ADVISE_TEXT'), 9 | prompt: t('__COMMAND_SUM_ADVISE_PROMPT'), 10 | aliases: [ 11 | '/advise', 12 | 'Advise', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/bot-search.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_BOT_SEARCH_LABEL'), 8 | text: t('__COMMAND_BOT_SEARCH_TEXT'), 9 | aliases: [ 10 | ...t('__COMMAND_BOT_SEARCH_ALIASES'), 11 | '/search', 12 | 'Search', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/sum-comfort.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SUM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SUM, 7 | label: t('__COMMAND_SUM_COMFORT_LABEL'), 8 | text: t('__COMMAND_SUM_COMFORT_TEXT'), 9 | prompt: t('__COMMAND_SUM_COMFORT_PROMPT'), 10 | aliases: [ 11 | '/comfort', 12 | 'Comfort', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/sys-deploy.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_SYS_DEPLOY_LABEL'), 8 | text: t('__COMMAND_SYS_DEPLOY_TEXT'), 9 | reply: t('__COMMAND_SYS_DEPLOY_REPLY'), 10 | aliases: [ 11 | '/restart', 12 | 'Restart', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/sys-version.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_SYS_VERSION_LABEL'), 8 | text: t('__COMMAND_SYS_VERSION_TEXT'), 9 | reply: t('__COMMAND_SYS_VERSION_REPLY'), 10 | aliases: [ 11 | '/version', 12 | 'Version', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/bot-continue.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_BOT_CONTINUE_LABEL'), 8 | text: t('__COMMAND_BOT_CONTINUE_TEXT'), 9 | aliases: [ 10 | ...t('__COMMAND_BOT_CONTINUE_ALIASES'), 11 | '/continue', 12 | 'Continue', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/sum-complain.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SUM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SUM, 7 | label: t('__COMMAND_SUM_COMPLAIN_LABEL'), 8 | text: t('__COMMAND_SUM_COMPLAIN_TEXT'), 9 | prompt: t('__COMMAND_SUM_COMPLAIN_PROMPT'), 10 | aliases: [ 11 | '/complain', 12 | 'Complain', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /utils/get-command.js: -------------------------------------------------------------------------------- 1 | import { Command, ALL_COMMANDS } from '../app/commands/index.js'; 2 | 3 | /** 4 | * @param {string} text 5 | * @returns {Command} 6 | */ 7 | const getCommand = (text) => ( 8 | Object.values(ALL_COMMANDS) 9 | .sort((a, b) => b.text.length - a.text.length) 10 | .find((c) => ( 11 | c.aliases.includes(text) || text.toLowerCase().includes(c.text.toLowerCase()) 12 | )) 13 | ); 14 | 15 | export default getCommand; 16 | -------------------------------------------------------------------------------- /app/commands/sum-apologize.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SUM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SUM, 7 | label: t('__COMMAND_SUM_APOLOGIZE_LABEL'), 8 | text: t('__COMMAND_SUM_APOLOGIZE_TEXT'), 9 | prompt: t('__COMMAND_SUM_APOLOGIZE_PROMPT'), 10 | aliases: [ 11 | '/apologize', 12 | 'Apologize', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/sum-encourage.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SUM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SUM, 7 | label: t('__COMMAND_SUM_ENCOURAGE_LABEL'), 8 | text: t('__COMMAND_SUM_ENCOURAGE_TEXT'), 9 | prompt: t('__COMMAND_SUM_ENCOURAGE_PROMPT'), 10 | aliases: [ 11 | '/encourage', 12 | 'Encourage', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/analyze-analyze.js: -------------------------------------------------------------------------------- 1 | import { TYPE_ANALYZE } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_ANALYZE, 7 | label: t('__COMMAND_ANALYZE_ANALYZE_LABEL'), 8 | text: t('__COMMAND_ANALYZE_ANALYZE_TEXT'), 9 | prompt: t('__COMMAND_ANALYZE_ANALYZE_PROMPT'), 10 | aliases: [ 11 | '/analyze', 12 | 'Analyze', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/bot-draw.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_BOT_DRAW_LABEL'), 8 | text: t('__COMMAND_BOT_DRAW_TEXT'), 9 | prompt: t('__COMMAND_BOT_DRAW_PROMPT'), 10 | aliases: [ 11 | ...t('__COMMAND_BOT_DRAW_ALIASES'), 12 | '/draw', 13 | 'Draw', 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /app/commands/bot-forget.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_BOT_FORGET_LABEL'), 8 | text: t('__COMMAND_BOT_FORGET_TEXT'), 9 | reply: t('__COMMAND_BOT_FORGET_REPLY'), 10 | aliases: [ 11 | ...t('__COMMAND_BOT_FORGET_ALIASES'), 12 | '/forget', 13 | 'Forget', 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /middleware/validate-line-signature.js: -------------------------------------------------------------------------------- 1 | import config from '../config/index.js'; 2 | import { validateSignature } from '../utils/index.js'; 3 | 4 | const validateLineSignature = (req, res, next) => { 5 | const secret = config.LINE_CHANNEL_SECRET || ''; 6 | const signature = req.header('x-line-signature'); 7 | if (!validateSignature(req.rawBody, secret, signature)) { 8 | res.sendStatus(403); 9 | return; 10 | } 11 | next(); 12 | }; 13 | 14 | export default validateLineSignature; 15 | -------------------------------------------------------------------------------- /app/commands/analyze-literarily.js: -------------------------------------------------------------------------------- 1 | import { TYPE_ANALYZE } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_ANALYZE, 7 | label: t('__COMMAND_ANALYZE_LITERARILY_LABEL'), 8 | text: t('__COMMAND_ANALYZE_LITERARILY_TEXT'), 9 | prompt: t('__COMMAND_ANALYZE_LITERARILY_PROMPT'), 10 | aliases: [ 11 | '/analyze-literarily', 12 | 'Analyze literarily', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/bot-activate.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_BOT_ACTIVATE_LABEL'), 8 | text: t('__COMMAND_BOT_ACTIVATE_TEXT'), 9 | reply: t('__COMMAND_BOT_ACTIVATE_REPLY'), 10 | aliases: [ 11 | ...t('__COMMAND_BOT_ACTIVATE_ALIASES'), 12 | '/activate', 13 | 'Activate', 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /utils/add-mark.js: -------------------------------------------------------------------------------- 1 | import config from '../config/index.js'; 2 | 3 | const addMark = (text) => { 4 | if (!text) return text; 5 | const marks = ['?', '。', '!', '?', '.', '!']; 6 | if (marks.some((mark) => text.endsWith(mark))) { 7 | return text; 8 | } 9 | switch (config.APP_LANG) { 10 | case 'zh': 11 | case 'zh_TW': 12 | case 'zh_CN': 13 | case 'ja': 14 | return `${text}。`; 15 | default: 16 | return `${text}.`; 17 | } 18 | }; 19 | 20 | export default addMark; 21 | -------------------------------------------------------------------------------- /utils/convert-text.js: -------------------------------------------------------------------------------- 1 | import * as OpenCC from 'opencc-js'; 2 | import config from '../config/index.js'; 3 | 4 | const convertText = (text) => { 5 | if (config.APP_LANG === 'zh_TW') { 6 | const converter = OpenCC.Converter({ from: 'cn', to: 'tw' }); 7 | return converter(text); 8 | } 9 | if (config.APP_LANG === 'zh_CN') { 10 | const converter = OpenCC.Converter({ from: 'tw', to: 'cn' }); 11 | return converter(text); 12 | } 13 | return text; 14 | }; 15 | 16 | export default convertText; 17 | -------------------------------------------------------------------------------- /app/commands/bot-deactivate.js: -------------------------------------------------------------------------------- 1 | import { TYPE_SYSTEM } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_SYSTEM, 7 | label: t('__COMMAND_BOT_DEACTIVATE_LABEL'), 8 | text: t('__COMMAND_BOT_DEACTIVATE_TEXT'), 9 | reply: t('__COMMAND_BOT_DEACTIVATE_REPLY'), 10 | aliases: [ 11 | ...t('__COMMAND_BOT_DEACTIVATE_ALIASES'), 12 | '/deactivate', 13 | 'Deactivate', 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /app/commands/command.js: -------------------------------------------------------------------------------- 1 | class Command { 2 | type; 3 | 4 | label; 5 | 6 | text; 7 | 8 | reply; 9 | 10 | prompt; 11 | 12 | aliases; 13 | 14 | constructor({ 15 | type, 16 | label, 17 | text, 18 | reply = '', 19 | prompt = '', 20 | aliases = [], 21 | }) { 22 | this.type = type; 23 | this.label = label; 24 | this.text = text; 25 | this.reply = reply; 26 | this.prompt = prompt; 27 | this.aliases = aliases; 28 | } 29 | } 30 | 31 | export default Command; 32 | -------------------------------------------------------------------------------- /app/commands/translate-to-en.js: -------------------------------------------------------------------------------- 1 | import { TYPE_TRANSLATE } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_TRANSLATE, 7 | label: t('__COMMAND_TRANSLATE_TO_EN_LABEL'), 8 | text: t('__COMMAND_TRANSLATE_TO_EN_TEXT'), 9 | prompt: t('__COMMAND_TRANSLATE_TO_EN_PROMPT'), 10 | aliases: [ 11 | '/translate-to-en', 12 | 'Translate to English', 13 | 'Translate to EN', 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /app/commands/translate-to-ja.js: -------------------------------------------------------------------------------- 1 | import { TYPE_TRANSLATE } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_TRANSLATE, 7 | label: t('__COMMAND_TRANSLATE_TO_JA_LABEL'), 8 | text: t('__COMMAND_TRANSLATE_TO_JA_TEXT'), 9 | prompt: t('__COMMAND_TRANSLATE_TO_JA_PROMPT'), 10 | aliases: [ 11 | '/translate-to-ja', 12 | 'Translate to Japanese', 13 | 'Translate to JA', 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /app/commands/analyze-mathematically.js: -------------------------------------------------------------------------------- 1 | import { TYPE_ANALYZE } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_ANALYZE, 7 | label: t('__COMMAND_ANALYZE_MATHEMATICALLY_LABEL'), 8 | text: t('__COMMAND_ANALYZE_MATHEMATICALLY_TEXT'), 9 | prompt: t('__COMMAND_ANALYZE_MATHEMATICALLY_PROMPT'), 10 | aliases: [ 11 | '/analyze-mathematically', 12 | 'Analyze mathematically', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/messages/image.js: -------------------------------------------------------------------------------- 1 | import { MESSAGE_TYPE_IMAGE } from '../../services/line.js'; 2 | import Message from './message.js'; 3 | 4 | class ImageMessage extends Message { 5 | type = MESSAGE_TYPE_IMAGE; 6 | 7 | originalContentUrl; 8 | 9 | previewImageUrl; 10 | 11 | constructor({ 12 | originalContentUrl, 13 | previewImageUrl, 14 | }) { 15 | super(); 16 | this.originalContentUrl = originalContentUrl; 17 | this.previewImageUrl = previewImageUrl; 18 | } 19 | } 20 | 21 | export default ImageMessage; 22 | -------------------------------------------------------------------------------- /app/commands/analyze-numerologically.js: -------------------------------------------------------------------------------- 1 | import { TYPE_ANALYZE } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_ANALYZE, 7 | label: t('__COMMAND_ANALYZE_NUMEROLOGICALLY_LABEL'), 8 | text: t('__COMMAND_ANALYZE_NUMEROLOGICALLY_TEXT'), 9 | prompt: t('__COMMAND_ANALYZE_NUMEROLOGICALLY_PROMPT'), 10 | aliases: [ 11 | '/analyze-numerologically', 12 | 'Analyze numerologically', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/analyze-philosophically.js: -------------------------------------------------------------------------------- 1 | import { TYPE_ANALYZE } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_ANALYZE, 7 | label: t('__COMMAND_ANALYZE_PHILOSOPHICALLY_LABEL'), 8 | text: t('__COMMAND_ANALYZE_PHILOSOPHICALLY_TEXT'), 9 | prompt: t('__COMMAND_ANALYZE_PHILOSOPHICALLY_PROMPT'), 10 | aliases: [ 11 | '/analyze-philosophically', 12 | 'Analyze philosophically', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /app/commands/analyze-psychologically.js: -------------------------------------------------------------------------------- 1 | import { TYPE_ANALYZE } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import Command from './command.js'; 4 | 5 | export default new Command({ 6 | type: TYPE_ANALYZE, 7 | label: t('__COMMAND_ANALYZE_PSYCHOLOGICALLY_LABEL'), 8 | text: t('__COMMAND_ANALYZE_PSYCHOLOGICALLY_TEXT'), 9 | prompt: t('__COMMAND_ANALYZE_PSYCHOLOGICALLY_PROMPT'), 10 | aliases: [ 11 | '/analyze-psychologically', 12 | 'Analyze psychologically', 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /utils/validate-signature.js: -------------------------------------------------------------------------------- 1 | import { 2 | createHmac, 3 | timingSafeEqual, 4 | } from 'crypto'; 5 | 6 | const s2b = (str, encoding) => Buffer.from(str, encoding); 7 | 8 | const safeCompare = (a, b) => { 9 | if (a.length !== b.length) { 10 | return false; 11 | } 12 | return timingSafeEqual(a, b); 13 | }; 14 | 15 | const validateSignature = ( 16 | body, 17 | secret, 18 | signature, 19 | ) => safeCompare( 20 | createHmac('SHA256', secret).update(body).digest(), 21 | s2b(signature, 'base64'), 22 | ); 23 | 24 | export default validateSignature; 25 | -------------------------------------------------------------------------------- /app/models/source.js: -------------------------------------------------------------------------------- 1 | import { t } from '../../locales/index.js'; 2 | import { SOURCE_TYPE_GROUP } from '../../services/line.js'; 3 | 4 | class Source { 5 | type; 6 | 7 | name; 8 | 9 | bot; 10 | 11 | createdAt; 12 | 13 | constructor({ 14 | type, 15 | name, 16 | bot, 17 | }) { 18 | this.type = type; 19 | this.name = name || (type === SOURCE_TYPE_GROUP ? t('__SOURCE_NAME_SOME_GROUP') : t('__SOURCE_NAME_SOMEONE')); 20 | this.bot = bot; 21 | this.createdAt = Math.floor(Date.now() / 1000); 22 | } 23 | } 24 | 25 | export default Source; 26 | -------------------------------------------------------------------------------- /constants/mock.js: -------------------------------------------------------------------------------- 1 | export const MOCK_TEXT_OK = 'OK!'; 2 | 3 | export const MOCK_GROUP_01 = '000001'; 4 | export const MOCK_GROUP_02 = '000002'; 5 | 6 | export const MOCK_USER_01 = '000001'; 7 | export const MOCK_USER_02 = '000002'; 8 | 9 | const mockGroups = {}; 10 | mockGroups[MOCK_GROUP_01] = { groupName: 'group' }; 11 | mockGroups[MOCK_USER_02] = { groupName: 'group 2' }; 12 | 13 | const mockUsers = {}; 14 | mockUsers[MOCK_USER_01] = { displayName: 'user' }; 15 | mockUsers[MOCK_USER_02] = { displayName: 'user 2' }; 16 | 17 | export { 18 | mockGroups, 19 | mockUsers, 20 | }; 21 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: 'airbnb', 8 | overrides: [ 9 | { 10 | files: [ 11 | 'config/index.js', 12 | ], 13 | rules: { 14 | 'max-len': 'off', 15 | }, 16 | }, 17 | ], 18 | parserOptions: { 19 | ecmaVersion: 'latest', 20 | }, 21 | rules: { 22 | 'import/extensions': ['error', 'always', { ignorePackages: true }], 23 | 'no-console': 'off', 24 | 'no-param-reassign': 'off', 25 | 'no-unused-vars': 'off', 26 | 'max-len': 'off', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /utils/generate-transcription.js: -------------------------------------------------------------------------------- 1 | import { createAudioTranscriptions } from '../services/openai.js'; 2 | 3 | class Transcription { 4 | text; 5 | 6 | constructor({ 7 | text, 8 | }) { 9 | this.text = text; 10 | } 11 | } 12 | 13 | /** 14 | * @param {Object} param 15 | * @param {Buffer} param.buffer 16 | * @param {string} param.file 17 | * @returns {Promise} 18 | */ 19 | const generateTranscription = async ({ 20 | buffer, 21 | file, 22 | }) => { 23 | const { data } = await createAudioTranscriptions({ buffer, file }); 24 | return new Transcription(data); 25 | }; 26 | 27 | export default generateTranscription; 28 | -------------------------------------------------------------------------------- /app/messages/message.js: -------------------------------------------------------------------------------- 1 | import { QUICK_REPLY_TYPE_ACTION } from '../../services/line.js'; 2 | import { MessageAction } from '../actions/index.js'; 3 | import { Command } from '../commands/index.js'; 4 | 5 | class Message { 6 | type; 7 | 8 | quickReply; 9 | 10 | /** 11 | * @param {Array} actions 12 | */ 13 | setQuickReply(actions = []) { 14 | if (actions.length < 1) return; 15 | this.quickReply = { 16 | items: actions.map((action) => ({ 17 | type: QUICK_REPLY_TYPE_ACTION, 18 | action: new MessageAction(action), 19 | })), 20 | }; 21 | } 22 | } 23 | 24 | export default Message; 25 | -------------------------------------------------------------------------------- /app/messages/template.js: -------------------------------------------------------------------------------- 1 | import { MESSAGE_TYPE_TEMPLATE, TEMPLATE_TYPE_BUTTONS } from '../../services/line.js'; 2 | import { MessageAction } from '../actions/index.js'; 3 | import Message from './message.js'; 4 | 5 | class TemplateMessage extends Message { 6 | type = MESSAGE_TYPE_TEMPLATE; 7 | 8 | altText; 9 | 10 | template; 11 | 12 | constructor({ 13 | text, 14 | actions, 15 | }) { 16 | super(); 17 | this.altText = text; 18 | this.template = { 19 | type: TEMPLATE_TYPE_BUTTONS, 20 | text, 21 | actions: actions.map((action) => new MessageAction(action)), 22 | }; 23 | } 24 | } 25 | 26 | export default TemplateMessage; 27 | -------------------------------------------------------------------------------- /app/prompt/message.js: -------------------------------------------------------------------------------- 1 | import { TYPE_ANALYZE, TYPE_SUM, TYPE_TRANSLATE } from '../../constants/command.js'; 2 | 3 | class Message { 4 | role; 5 | 6 | content; 7 | 8 | constructor({ 9 | role, 10 | content, 11 | }) { 12 | this.role = role; 13 | this.content = content; 14 | } 15 | 16 | get isEnquiring() { 17 | return this.content === TYPE_SUM 18 | || this.content === TYPE_ANALYZE 19 | || this.content === TYPE_TRANSLATE; 20 | } 21 | 22 | toString() { 23 | if (Array.isArray(this.content)) { 24 | return `\n${this.role}: ${this.content[0].text}`; 25 | } 26 | return this.role ? `\n${this.role}: ${this.content}` : this.content; 27 | } 28 | } 29 | 30 | export default Message; 31 | -------------------------------------------------------------------------------- /app/handlers/doc.js: -------------------------------------------------------------------------------- 1 | import { COMMAND_SYS_DOC, GENERAL_COMMANDS } from '../commands/index.js'; 2 | import Context from '../context.js'; 3 | import { updateHistory } from '../history/index.js'; 4 | 5 | /** 6 | * @param {Context} context 7 | * @returns {boolean} 8 | */ 9 | const check = (context) => context.hasCommand(COMMAND_SYS_DOC); 10 | 11 | /** 12 | * @param {Context} context 13 | * @returns {Promise} 14 | */ 15 | const exec = (context) => check(context) && ( 16 | async () => { 17 | updateHistory(context.id, (history) => history.erase()); 18 | context.pushText('https://memochou1993.github.io/gpt-ai-assistant-docs/', GENERAL_COMMANDS); 19 | return context; 20 | } 21 | )(); 22 | 23 | export default exec; 24 | -------------------------------------------------------------------------------- /utils/generate-image.js: -------------------------------------------------------------------------------- 1 | import config from '../config/index.js'; 2 | import { MOCK_TEXT_OK } from '../constants/mock.js'; 3 | import { createImage } from '../services/openai.js'; 4 | 5 | class Image { 6 | url; 7 | 8 | constructor({ 9 | url, 10 | }) { 11 | this.url = url; 12 | } 13 | } 14 | 15 | /** 16 | * @param {Object} param 17 | * @param {string} param.prompt 18 | * @returns {Promise} 19 | */ 20 | const generateImage = async ({ 21 | prompt, 22 | }) => { 23 | if (config.APP_ENV !== 'production') return new Image({ url: MOCK_TEXT_OK }); 24 | const { data } = await createImage({ prompt }); 25 | const [image] = data.data; 26 | return new Image(image); 27 | }; 28 | 29 | export default generateImage; 30 | -------------------------------------------------------------------------------- /app/handlers/forget.js: -------------------------------------------------------------------------------- 1 | import { COMMAND_BOT_FORGET } from '../commands/index.js'; 2 | import Context from '../context.js'; 3 | import { removeHistory } from '../history/index.js'; 4 | import { removePrompt } from '../prompt/index.js'; 5 | 6 | /** 7 | * @param {Context} context 8 | * @returns {boolean} 9 | */ 10 | const check = (context) => context.hasCommand(COMMAND_BOT_FORGET); 11 | 12 | /** 13 | * @param {Context} context 14 | * @returns {Promise} 15 | */ 16 | const exec = (context) => check(context) && ( 17 | async () => { 18 | removePrompt(context.userId); 19 | removeHistory(context.userId); 20 | context.pushText(COMMAND_BOT_FORGET.reply); 21 | return context; 22 | } 23 | )(); 24 | 25 | export default exec; 26 | -------------------------------------------------------------------------------- /app/handlers/report.js: -------------------------------------------------------------------------------- 1 | import { COMMAND_SYS_REPORT, GENERAL_COMMANDS } from '../commands/index.js'; 2 | import Context from '../context.js'; 3 | import { updateHistory } from '../history/index.js'; 4 | 5 | /** 6 | * @param {Context} context 7 | * @returns {boolean} 8 | */ 9 | const check = (context) => context.hasCommand(COMMAND_SYS_REPORT); 10 | 11 | /** 12 | * @param {Context} context 13 | * @returns {Promise} 14 | */ 15 | const exec = (context) => check(context) && ( 16 | async () => { 17 | updateHistory(context.id, (history) => history.erase()); 18 | context.pushText('https://github.com/memochou1993/gpt-ai-assistant/issues', GENERAL_COMMANDS); 19 | return context; 20 | } 21 | )(); 22 | 23 | export default exec; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gpt-ai-assistant", 3 | "version": "4.9.1", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "nodemon api/index.js", 7 | "start": "node api/index.js", 8 | "test": "jest" 9 | }, 10 | "dependencies": { 11 | "axios": "^1.2.1", 12 | "dotenv": "^16.0.3", 13 | "express": "^4.18.2", 14 | "form-data": "^4.0.0", 15 | "gpt-3-encoder": "^1.1.3", 16 | "opencc-js": "^1.0.5" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.20.5", 20 | "@babel/preset-env": "^7.20.2", 21 | "@jest/globals": "^29.3.1", 22 | "babel-jest": "^29.3.1", 23 | "eslint": "^8.29.0", 24 | "eslint-config-airbnb": "^19.0.4", 25 | "jest": "^29.3.1", 26 | "nodemon": "^2.0.20" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/utils.js: -------------------------------------------------------------------------------- 1 | import Event from '../app/models/event.js'; 2 | import { MOCK_TEXT_OK, MOCK_USER_01, MOCK_USER_02 } from '../constants/mock.js'; 3 | import { 4 | EVENT_TYPE_MESSAGE, MESSAGE_TYPE_TEXT, SOURCE_TYPE_GROUP, SOURCE_TYPE_USER, 5 | } from '../services/line.js'; 6 | 7 | export const TIMEOUT = 9 * 1000; 8 | 9 | const createEvents = ( 10 | messages, 11 | groupId, 12 | userId = MOCK_USER_01, 13 | ) => messages.map((text) => new Event({ 14 | replyToken: '', 15 | type: EVENT_TYPE_MESSAGE, 16 | source: { type: groupId ? SOURCE_TYPE_GROUP : SOURCE_TYPE_USER, userId, groupId }, 17 | message: { type: MESSAGE_TYPE_TEXT, text }, 18 | })); 19 | 20 | export { 21 | MOCK_TEXT_OK, 22 | MOCK_USER_01, 23 | MOCK_USER_02, 24 | createEvents, 25 | }; 26 | -------------------------------------------------------------------------------- /app/repository/source.js: -------------------------------------------------------------------------------- 1 | import storage from '../../storage/index.js'; 2 | import { Source } from '../models/index.js'; 3 | 4 | const FIELD_KEY = 'sources'; 5 | 6 | /** 7 | * @returns {Object.} 8 | */ 9 | const getSources = () => storage.getItem(FIELD_KEY) || {}; 10 | 11 | /** 12 | * @param {Object.} sources 13 | */ 14 | const setSources = (sources) => storage.setItem(FIELD_KEY, sources); 15 | 16 | /** 17 | * @param {string} contextId 18 | * @param {function(Source)} callback 19 | */ 20 | const updateSources = async (contextId, callback) => { 21 | const sources = getSources(); 22 | callback(sources[contextId]); 23 | await setSources(sources); 24 | }; 25 | 26 | export { 27 | getSources, 28 | setSources, 29 | updateSources, 30 | }; 31 | -------------------------------------------------------------------------------- /utils/fetch-user.js: -------------------------------------------------------------------------------- 1 | import config from '../config/index.js'; 2 | import { mockUsers } from '../constants/mock.js'; 3 | import { t } from '../locales/index.js'; 4 | import { fetchProfile } from '../services/line.js'; 5 | 6 | class User { 7 | displayName; 8 | 9 | constructor({ 10 | displayName, 11 | }) { 12 | this.displayName = displayName; 13 | } 14 | } 15 | 16 | /** 17 | * @param {string} userId 18 | * @returns {Promise} 19 | */ 20 | const fetchUser = async (userId) => { 21 | if (config.APP_ENV !== 'production') return new User(mockUsers[userId]); 22 | try { 23 | const { data } = await fetchProfile({ userId }); 24 | return new User(data); 25 | } catch { 26 | return new User({ displayName: t('__SOURCE_NAME_SOMEONE') }); 27 | } 28 | }; 29 | 30 | export default fetchUser; 31 | -------------------------------------------------------------------------------- /utils/fetch-group.js: -------------------------------------------------------------------------------- 1 | import config from '../config/index.js'; 2 | import { mockGroups } from '../constants/mock.js'; 3 | import { t } from '../locales/index.js'; 4 | import { fetchGroupSummary } from '../services/line.js'; 5 | 6 | class Group { 7 | groupName; 8 | 9 | constructor({ 10 | groupName, 11 | }) { 12 | this.groupName = groupName; 13 | } 14 | } 15 | 16 | /** 17 | * @param {string} groupId 18 | * @returns {Promise} 19 | */ 20 | const fetchGroup = async (groupId) => { 21 | if (config.APP_ENV !== 'production') return new Group(mockGroups[groupId]); 22 | try { 23 | const { data } = await fetchGroupSummary({ groupId }); 24 | return new Group(data); 25 | } catch { 26 | return new Group({ groupName: t('__SOURCE_NAME_SOME_GROUP') }); 27 | } 28 | }; 29 | 30 | export default fetchGroup; 31 | -------------------------------------------------------------------------------- /services/utils/index.js: -------------------------------------------------------------------------------- 1 | import config from '../../config/index.js'; 2 | 3 | const handleRequest = (c) => { 4 | c.metadata = { startTime: new Date() }; 5 | return c; 6 | }; 7 | 8 | const handleFulfilled = (response) => { 9 | 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)`); 10 | return response; 11 | }; 12 | 13 | const handleRejected = (err) => { 14 | 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)`); 15 | return Promise.reject(err); 16 | }; 17 | 18 | export { 19 | handleRequest, 20 | handleFulfilled, 21 | handleRejected, 22 | }; 23 | -------------------------------------------------------------------------------- /app/prompt/index.js: -------------------------------------------------------------------------------- 1 | import Prompt from './prompt.js'; 2 | 3 | const prompts = new Map(); 4 | 5 | /** 6 | * @param {string} userId 7 | * @returns {Prompt} 8 | */ 9 | const getPrompt = (userId) => prompts.get(userId) || new Prompt(); 10 | 11 | /** 12 | * @param {string} userId 13 | * @param {Prompt} prompt 14 | */ 15 | const setPrompt = (userId, prompt) => { 16 | prompts.set(userId, prompt); 17 | }; 18 | 19 | /** 20 | * @param {string} userId 21 | */ 22 | const removePrompt = (userId) => { 23 | prompts.delete(userId); 24 | }; 25 | 26 | const printPrompts = () => { 27 | if (Array.from(prompts.keys()).length < 1) return; 28 | const content = Array.from(prompts.keys()).map((userId) => `\n=== ${userId.slice(0, 6)} ===\n${getPrompt(userId)}\n`).join(''); 29 | console.info(content); 30 | }; 31 | 32 | export { 33 | Prompt, 34 | getPrompt, 35 | setPrompt, 36 | removePrompt, 37 | printPrompts, 38 | }; 39 | 40 | export default prompts; 41 | -------------------------------------------------------------------------------- /tests/summon.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | afterEach, beforeEach, expect, test, 3 | } from '@jest/globals'; 4 | import { getPrompt, handleEvents, removePrompt } from '../app/index.js'; 5 | import config from '../config/index.js'; 6 | import { 7 | createEvents, MOCK_TEXT_OK, MOCK_USER_01, TIMEOUT, 8 | } from './utils.js'; 9 | 10 | beforeEach(() => { 11 | // 12 | }); 13 | 14 | afterEach(() => { 15 | removePrompt(MOCK_USER_01); 16 | }); 17 | 18 | test('COMMAND_BOT_SUMMON', async () => { 19 | const events = [ 20 | ...createEvents([`${config.BOT_NAME} 你好`]), 21 | ]; 22 | let results; 23 | try { 24 | results = await handleEvents(events); 25 | } catch (err) { 26 | console.error(err); 27 | } 28 | expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5); 29 | const replies = results.map(({ messages }) => messages.map(({ text }) => text)); 30 | expect(replies).toEqual( 31 | [ 32 | [MOCK_TEXT_OK], 33 | ], 34 | ); 35 | }, TIMEOUT); 36 | -------------------------------------------------------------------------------- /utils/fetch-answer.js: -------------------------------------------------------------------------------- 1 | import config from '../config/index.js'; 2 | import { search } from '../services/serpapi.js'; 3 | 4 | class OrganicResult { 5 | answer; 6 | 7 | constructor({ 8 | answer, 9 | } = {}) { 10 | this.answer = answer; 11 | } 12 | } 13 | 14 | const fetchAnswer = async (q) => { 15 | if (config.APP_ENV !== 'production' || !config.SERPAPI_API_KEY) return new OrganicResult(); 16 | const res = await search({ q }); 17 | const { answer_box: answerBox, knowledge_graph: knowledgeGraph, organic_results: organicResults } = res.data; 18 | let answer = organicResults[0].snippet; 19 | if (answerBox?.answer) answer += answerBox.answer; 20 | if (answerBox?.result) answer += answerBox.result; 21 | if (answerBox?.snippet) answer += answerBox.snippet; 22 | if (knowledgeGraph?.description) answer += `${knowledgeGraph.title} - ${knowledgeGraph.description}`; 23 | return new OrganicResult({ answer }); 24 | }; 25 | 26 | export default fetchAnswer; 27 | -------------------------------------------------------------------------------- /app/handlers/index.js: -------------------------------------------------------------------------------- 1 | import activateHandler from './activate.js'; 2 | import commandHandler from './command.js'; 3 | import continueHandler from './continue.js'; 4 | import deactivateHandler from './deactivate.js'; 5 | import deployHandler from './deploy.js'; 6 | import docHandler from './doc.js'; 7 | import drawHandler from './draw.js'; 8 | import forgetHandler from './forget.js'; 9 | import enquireHandler from './enquire.js'; 10 | import reportHandler from './report.js'; 11 | import retryHandler from './retry.js'; 12 | import searchHandler from './search.js'; 13 | import talkHandler from './talk.js'; 14 | import versionHandler from './version.js'; 15 | 16 | export { 17 | activateHandler, 18 | commandHandler, 19 | continueHandler, 20 | deactivateHandler, 21 | deployHandler, 22 | docHandler, 23 | drawHandler, 24 | forgetHandler, 25 | enquireHandler, 26 | reportHandler, 27 | retryHandler, 28 | searchHandler, 29 | talkHandler, 30 | versionHandler, 31 | }; 32 | -------------------------------------------------------------------------------- /tests/talk.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | afterEach, beforeEach, expect, test, 3 | } from '@jest/globals'; 4 | import { getPrompt, handleEvents, removePrompt } from '../app/index.js'; 5 | import { COMMAND_BOT_TALK } from '../app/commands/index.js'; 6 | import { 7 | createEvents, TIMEOUT, MOCK_USER_01, MOCK_TEXT_OK, 8 | } from './utils.js'; 9 | 10 | beforeEach(() => { 11 | // 12 | }); 13 | 14 | afterEach(() => { 15 | removePrompt(MOCK_USER_01); 16 | }); 17 | 18 | test('COMMAND_BOT_TALK', async () => { 19 | const events = [ 20 | ...createEvents([`${COMMAND_BOT_TALK.text}人工智慧`]), 21 | ]; 22 | let results; 23 | try { 24 | results = await handleEvents(events); 25 | } catch (err) { 26 | console.error(err); 27 | } 28 | expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5); 29 | const replies = results.map(({ messages }) => messages.map(({ text }) => text)); 30 | expect(replies).toEqual( 31 | [ 32 | [MOCK_TEXT_OK], 33 | ], 34 | ); 35 | }, TIMEOUT); 36 | -------------------------------------------------------------------------------- /tests/continue.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | afterEach, beforeEach, expect, test, 3 | } from '@jest/globals'; 4 | import { getPrompt, handleEvents, removePrompt } from '../app/index.js'; 5 | import { COMMAND_BOT_CONTINUE } from '../app/commands/index.js'; 6 | import { 7 | createEvents, TIMEOUT, MOCK_USER_01, MOCK_TEXT_OK, 8 | } from './utils.js'; 9 | 10 | beforeEach(() => { 11 | // 12 | }); 13 | 14 | afterEach(() => { 15 | removePrompt(MOCK_USER_01); 16 | }); 17 | 18 | test('COMMAND_BOT_CONTINUE', async () => { 19 | const events = [ 20 | ...createEvents([COMMAND_BOT_CONTINUE.text]), 21 | ]; 22 | let results; 23 | try { 24 | results = await handleEvents(events); 25 | } catch (err) { 26 | console.error(err); 27 | } 28 | expect(getPrompt(MOCK_USER_01).messages.length).toEqual(3); 29 | const replies = results.map(({ messages }) => messages.map(({ text }) => text)); 30 | expect(replies).toEqual( 31 | [ 32 | [MOCK_TEXT_OK], 33 | ], 34 | ); 35 | }, TIMEOUT); 36 | -------------------------------------------------------------------------------- /services/serpapi.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import config from '../config/index.js'; 3 | import { handleFulfilled, handleRejected, handleRequest } from './utils/index.js'; 4 | 5 | const client = axios.create({ 6 | baseURL: 'https://serpapi.com', 7 | timeout: config.SERPAPI_TIMEOUT, 8 | headers: { 9 | 'Accept-Encoding': 'gzip, deflate, compress', 10 | }, 11 | }); 12 | 13 | client.interceptors.request.use((c) => { 14 | c.params = { 15 | key: config.SERPAPI_API_KEY, 16 | ...c.params, 17 | }; 18 | return handleRequest(c); 19 | }); 20 | 21 | client.interceptors.response.use(handleFulfilled, (err) => { 22 | if (err.response?.data?.error) { 23 | err.message = err.response.data.error; 24 | } 25 | return handleRejected(err); 26 | }); 27 | 28 | const search = ({ 29 | gl = config.SERPAPI_LOCATION, 30 | q, 31 | }) => client.get('/search', { 32 | params: { 33 | gl, 34 | q, 35 | }, 36 | }); 37 | 38 | export { 39 | search, 40 | }; 41 | 42 | export default null; 43 | -------------------------------------------------------------------------------- /tests/command.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | afterEach, 3 | beforeEach, 4 | expect, 5 | test, 6 | } from '@jest/globals'; 7 | import { getPrompt, handleEvents, removePrompt } from '../app/index.js'; 8 | import { COMMAND_SYS_COMMAND } from '../app/commands/index.js'; 9 | import { createEvents, TIMEOUT, MOCK_USER_01 } from './utils.js'; 10 | 11 | beforeEach(() => { 12 | // 13 | }); 14 | 15 | afterEach(() => { 16 | removePrompt(MOCK_USER_01); 17 | }); 18 | 19 | test('COMMAND_SYS_COMMAND', async () => { 20 | const events = [ 21 | ...createEvents([`${COMMAND_SYS_COMMAND.text}`]), 22 | ]; 23 | let results; 24 | try { 25 | results = await handleEvents(events); 26 | } catch (err) { 27 | console.error(err); 28 | } 29 | expect(getPrompt(MOCK_USER_01).messages.length).toEqual(3); 30 | const replies = results.map(({ messages }) => messages.map(({ altText }) => altText)); 31 | expect(replies).toEqual( 32 | [ 33 | [COMMAND_SYS_COMMAND.label], 34 | ], 35 | ); 36 | }, TIMEOUT); 37 | -------------------------------------------------------------------------------- /tests/default.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | afterEach, beforeEach, expect, test, 3 | } from '@jest/globals'; 4 | import { 5 | getPrompt, handleEvents, removePrompt, printHistories, 6 | } from '../app/index.js'; 7 | import config from '../config/index.js'; 8 | import { 9 | createEvents, TIMEOUT, MOCK_USER_01, MOCK_TEXT_OK, 10 | } from './utils.js'; 11 | 12 | beforeEach(() => { 13 | // 14 | }); 15 | 16 | afterEach(() => { 17 | removePrompt(MOCK_USER_01); 18 | }); 19 | 20 | test('DEFAULT', async () => { 21 | const events = [ 22 | ...createEvents(['嗨!']), 23 | ]; 24 | let results; 25 | try { 26 | results = await handleEvents(events); 27 | } catch (err) { 28 | console.error(err); 29 | } 30 | if (config.APP_DEBUG) printHistories(); 31 | expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5); 32 | const replies = results.map(({ messages }) => messages.map(({ text }) => text)); 33 | expect(replies).toEqual( 34 | [ 35 | [MOCK_TEXT_OK], 36 | ], 37 | ); 38 | }, TIMEOUT); 39 | -------------------------------------------------------------------------------- /tests/draw.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | afterEach, beforeEach, expect, test, 3 | } from '@jest/globals'; 4 | import { handleEvents, getPrompt, removePrompt } from '../app/index.js'; 5 | import { COMMAND_BOT_DRAW } from '../app/commands/index.js'; 6 | import { 7 | createEvents, TIMEOUT, MOCK_USER_01, MOCK_TEXT_OK, 8 | } from './utils.js'; 9 | 10 | beforeEach(() => { 11 | // 12 | }); 13 | 14 | afterEach(() => { 15 | removePrompt(MOCK_USER_01); 16 | }); 17 | 18 | test('COMMAND_BOT_DRAW', async () => { 19 | const events = [ 20 | ...createEvents([`${COMMAND_BOT_DRAW.text}人工智慧`]), 21 | ]; 22 | let results; 23 | try { 24 | results = await handleEvents(events); 25 | } catch (err) { 26 | console.error(err); 27 | } 28 | expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5); 29 | const replies = results.map(({ messages }) => messages.map(({ originalContentUrl }) => originalContentUrl)); 30 | expect(replies).toEqual( 31 | [ 32 | [MOCK_TEXT_OK], 33 | ], 34 | ); 35 | }, TIMEOUT); 36 | -------------------------------------------------------------------------------- /app/handlers/deploy.js: -------------------------------------------------------------------------------- 1 | import config from '../../config/index.js'; 2 | import { t } from '../../locales/index.js'; 3 | import { deploy } from '../../services/vercel.js'; 4 | import { COMMAND_SYS_DEPLOY } from '../commands/index.js'; 5 | import Context from '../context.js'; 6 | import { updateHistory } from '../history/index.js'; 7 | 8 | /** 9 | * @param {Context} context 10 | * @returns {boolean} 11 | */ 12 | const check = (context) => context.hasCommand(COMMAND_SYS_DEPLOY); 13 | 14 | /** 15 | * @param {Context} context 16 | * @returns {Promise} 17 | */ 18 | const exec = (context) => check(context) && ( 19 | async () => { 20 | updateHistory(context.id, (history) => history.erase()); 21 | if (!config.VERCEL_DEPLOY_HOOK_URL) context.pushText(t('__ERROR_MISSING_ENV')('VERCEL_DEPLOY_HOOK_URL')); 22 | try { 23 | await deploy(); 24 | context.pushText(COMMAND_SYS_DEPLOY.reply); 25 | } catch (err) { 26 | context.pushError(err); 27 | } 28 | return context; 29 | } 30 | )(); 31 | 32 | export default exec; 33 | -------------------------------------------------------------------------------- /app/handlers/command.js: -------------------------------------------------------------------------------- 1 | import { 2 | COMMAND_BOT_ACTIVATE, 3 | COMMAND_SYS_COMMAND, 4 | COMMAND_BOT_DEACTIVATE, 5 | GENERAL_COMMANDS, 6 | INFO_COMMANDS, 7 | } from '../commands/index.js'; 8 | import Context from '../context.js'; 9 | import { updateHistory } from '../history/index.js'; 10 | 11 | /** 12 | * @param {Context} context 13 | * @returns {boolean} 14 | */ 15 | const check = (context) => context.hasCommand(COMMAND_SYS_COMMAND); 16 | 17 | /** 18 | * @param {Context} context 19 | * @returns {Context} 20 | */ 21 | const exec = (context) => check(context) && ( 22 | async () => { 23 | updateHistory(context.id, (history) => history.erase()); 24 | try { 25 | const buttons = [...INFO_COMMANDS]; 26 | buttons.splice(2, 0, context.source.bot.isActivated ? COMMAND_BOT_DEACTIVATE : COMMAND_BOT_ACTIVATE); 27 | context.pushTemplate(COMMAND_SYS_COMMAND.label, buttons, GENERAL_COMMANDS); 28 | } catch (err) { 29 | context.pushError(err); 30 | } 31 | return context; 32 | } 33 | )(); 34 | 35 | export default exec; 36 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_DEBUG=true 2 | APP_URL= 3 | APP_PORT=3000 4 | APP_LANG= 5 | APP_WEBHOOK_PATH= 6 | APP_API_TIMEOUT= 7 | APP_MAX_GROUPS= 8 | APP_MAX_USERS= 9 | APP_MAX_PROMPT_MESSAGES= 10 | APP_MAX_PROMPT_TOKENS= 11 | APP_INIT_PROMPT= 12 | 13 | HUMAN_NAME= 14 | HUMAN_INIT_PROMPT= 15 | 16 | BOT_NAME= 17 | BOT_INIT_PROMPT= 18 | BOT_TONE= 19 | BOT_DEACTIVATED=false 20 | 21 | ERROR_MESSAGE_DISABLED=false 22 | 23 | VERCEL_TIMEOUT= 24 | VERCEL_PROJECT_NAME=gpt-ai-assistant 25 | VERCEL_TEAM_ID= 26 | VERCEL_ACCESS_TOKEN= 27 | VERCEL_DEPLOY_HOOK_URL= 28 | 29 | OPENAI_TIMEOUT= 30 | OPENAI_API_KEY= 31 | OPENAI_BASE_URL= 32 | OPENAI_COMPLETION_MODEL= 33 | OPENAI_COMPLETION_TEMPERATURE= 34 | OPENAI_COMPLETION_MAX_TOKENS= 35 | OPENAI_COMPLETION_FREQUENCY_PENALTY= 36 | OPENAI_COMPLETION_PRESENCE_PENALTY= 37 | OPENAI_IMAGE_GENERATION_MODEL= 38 | OPENAI_IMAGE_GENERATION_SIZE= 39 | OPENAI_IMAGE_GENERATION_QUALITY= 40 | OPENAI_VISION_MODEL= 41 | 42 | LINE_TIMEOUT= 43 | LINE_CHANNEL_ACCESS_TOKEN= 44 | LINE_CHANNEL_SECRET= 45 | 46 | SERPAPI_TIMEOUT= 47 | SERPAPI_API_KEY= 48 | SERPAPI_LOCATION= 49 | -------------------------------------------------------------------------------- /app/handlers/version.js: -------------------------------------------------------------------------------- 1 | import { t } from '../../locales/index.js'; 2 | import { fetchVersion, getVersion } from '../../utils/index.js'; 3 | import { COMMAND_SYS_VERSION, GENERAL_COMMANDS } from '../commands/index.js'; 4 | import Context from '../context.js'; 5 | import { updateHistory } from '../history/index.js'; 6 | 7 | /** 8 | * @param {Context} context 9 | * @returns {boolean} 10 | */ 11 | const check = (context) => context.hasCommand(COMMAND_SYS_VERSION); 12 | 13 | /** 14 | * @param {Context} context 15 | * @returns {Promise} 16 | */ 17 | const exec = (context) => check(context) && ( 18 | async () => { 19 | updateHistory(context.id, (history) => history.erase()); 20 | const current = getVersion(); 21 | const latest = await fetchVersion(); 22 | const isLatest = current === latest; 23 | const text = t('__COMMAND_SYS_VERSION_REPLY')(current, isLatest); 24 | context.pushText(text, GENERAL_COMMANDS); 25 | if (!isLatest) context.pushText(t('__MESSAGE_NEW_VERSION_AVAILABLE')(latest)); 26 | return context; 27 | } 28 | )(); 29 | 30 | export default exec; 31 | -------------------------------------------------------------------------------- /utils/generate-completion.js: -------------------------------------------------------------------------------- 1 | import config from '../config/index.js'; 2 | import { MOCK_TEXT_OK } from '../constants/mock.js'; 3 | import { createChatCompletion, FINISH_REASON_STOP } from '../services/openai.js'; 4 | 5 | class Completion { 6 | text; 7 | 8 | finishReason; 9 | 10 | constructor({ 11 | text, 12 | finishReason, 13 | }) { 14 | this.text = text; 15 | this.finishReason = finishReason; 16 | } 17 | 18 | get isFinishReasonStop() { 19 | return this.finishReason === FINISH_REASON_STOP; 20 | } 21 | } 22 | 23 | /** 24 | * @param {Object} param 25 | * @param {Prompt} param.prompt 26 | * @returns {Promise} 27 | */ 28 | const generateCompletion = async ({ 29 | prompt, 30 | }) => { 31 | if (config.APP_ENV !== 'production') return new Completion({ text: MOCK_TEXT_OK }); 32 | const { data } = await createChatCompletion({ messages: prompt.messages }); 33 | const [choice] = data.choices; 34 | return new Completion({ 35 | text: choice.message.content.trim(), 36 | finishReason: choice.finish_reason, 37 | }); 38 | }; 39 | 40 | export default generateCompletion; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Memo Chou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/handlers/activate.js: -------------------------------------------------------------------------------- 1 | import config from '../../config/index.js'; 2 | import { t } from '../../locales/index.js'; 3 | import { COMMAND_BOT_ACTIVATE, GENERAL_COMMANDS } from '../commands/index.js'; 4 | import Context from '../context.js'; 5 | import { updateHistory } from '../history/index.js'; 6 | import { updateSources } from '../repository/index.js'; 7 | 8 | /** 9 | * @param {Context} context 10 | * @returns {boolean} 11 | */ 12 | const check = (context) => context.hasCommand(COMMAND_BOT_ACTIVATE); 13 | 14 | /** 15 | * @param {Context} context 16 | * @returns {Context} 17 | */ 18 | const exec = (context) => check(context) && ( 19 | async () => { 20 | updateHistory(context.id, (history) => history.erase()); 21 | if (!config.VERCEL_ACCESS_TOKEN) context.pushText(t('__ERROR_MISSING_ENV')('VERCEL_ACCESS_TOKEN')); 22 | try { 23 | await updateSources(context.id, (source) => { 24 | source.bot.isActivated = true; 25 | }); 26 | context.pushText(COMMAND_BOT_ACTIVATE.reply, GENERAL_COMMANDS); 27 | } catch (err) { 28 | context.pushError(err); 29 | } 30 | return context; 31 | } 32 | )(); 33 | 34 | export default exec; 35 | -------------------------------------------------------------------------------- /app/handlers/deactivate.js: -------------------------------------------------------------------------------- 1 | import config from '../../config/index.js'; 2 | import { t } from '../../locales/index.js'; 3 | import { COMMAND_BOT_DEACTIVATE, GENERAL_COMMANDS } from '../commands/index.js'; 4 | import Context from '../context.js'; 5 | import { updateHistory } from '../history/index.js'; 6 | import { updateSources } from '../repository/index.js'; 7 | 8 | /** 9 | * @param {Context} context 10 | * @returns {boolean} 11 | */ 12 | const check = (context) => context.hasCommand(COMMAND_BOT_DEACTIVATE); 13 | 14 | /** 15 | * @param {Context} context 16 | * @returns {Promise} 17 | */ 18 | const exec = (context) => check(context) && ( 19 | async () => { 20 | updateHistory(context.id, (history) => history.erase()); 21 | if (!config.VERCEL_ACCESS_TOKEN) context.pushText(t('__ERROR_MISSING_ENV')('VERCEL_ACCESS_TOKEN')); 22 | try { 23 | await updateSources(context.id, (source) => { 24 | source.bot.isActivated = false; 25 | }); 26 | context.pushText(COMMAND_BOT_DEACTIVATE.reply, GENERAL_COMMANDS); 27 | } catch (err) { 28 | context.pushError(err); 29 | } 30 | return context; 31 | } 32 | )(); 33 | 34 | export default exec; 35 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | import addMark from './add-mark.js'; 2 | import convertText from './convert-text.js'; 3 | import fetchAnswer from './fetch-answer.js'; 4 | import fetchAudio from './fetch-audio.js'; 5 | import fetchImage from './fetch-image.js'; 6 | import fetchEnvironment from './fetch-environment.js'; 7 | import fetchGroup from './fetch-group.js'; 8 | import fetchUser from './fetch-user.js'; 9 | import fetchVersion from './fetch-version.js'; 10 | import generateCompletion from './generate-completion.js'; 11 | import generateImage from './generate-image.js'; 12 | import generateTranscription from './generate-transcription.js'; 13 | import getCommand from './get-command.js'; 14 | import getVersion from './get-version.js'; 15 | import replyMessage from './reply-message.js'; 16 | import validateSignature from './validate-signature.js'; 17 | 18 | export { 19 | addMark, 20 | convertText, 21 | fetchAnswer, 22 | fetchAudio, 23 | fetchImage, 24 | fetchEnvironment, 25 | fetchGroup, 26 | fetchUser, 27 | fetchVersion, 28 | generateCompletion, 29 | generateImage, 30 | generateTranscription, 31 | getCommand, 32 | getVersion, 33 | replyMessage, 34 | validateSignature, 35 | }; 36 | -------------------------------------------------------------------------------- /tests/version.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | afterEach, beforeEach, expect, test, 3 | } from '@jest/globals'; 4 | import { getPrompt, handleEvents, removePrompt } from '../app/index.js'; 5 | import { COMMAND_SYS_VERSION } from '../app/commands/index.js'; 6 | import { t } from '../locales/index.js'; 7 | import { fetchVersion, getVersion } from '../utils/index.js'; 8 | import { createEvents, MOCK_USER_01, TIMEOUT } from './utils.js'; 9 | 10 | beforeEach(() => { 11 | // 12 | }); 13 | 14 | afterEach(() => { 15 | removePrompt(MOCK_USER_01); 16 | }); 17 | 18 | test('COMMAND_SYS_VERSION', async () => { 19 | const events = [ 20 | ...createEvents([COMMAND_SYS_VERSION.text]), 21 | ]; 22 | let results; 23 | try { 24 | results = await handleEvents(events); 25 | } catch (err) { 26 | console.error(err); 27 | } 28 | const current = getVersion(); 29 | const latest = await fetchVersion(); 30 | const isLatest = current === latest; 31 | expect(getPrompt(MOCK_USER_01).messages.length).toEqual(3); 32 | const replies = results.map(({ messages }) => messages.map(({ text }) => text)); 33 | expect(replies).toEqual( 34 | [ 35 | [t('__COMMAND_SYS_VERSION_REPLY')(current, isLatest)], 36 | ], 37 | ); 38 | }, TIMEOUT); 39 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { handleEvents, printPrompts } from '../app/index.js'; 3 | import config from '../config/index.js'; 4 | import { validateLineSignature } from '../middleware/index.js'; 5 | import storage from '../storage/index.js'; 6 | import { fetchVersion, getVersion } from '../utils/index.js'; 7 | 8 | const app = express(); 9 | 10 | app.use(express.json({ 11 | verify: (req, res, buf) => { 12 | req.rawBody = buf.toString(); 13 | }, 14 | })); 15 | 16 | app.get('/', async (req, res) => { 17 | if (config.APP_URL) { 18 | res.redirect(config.APP_URL); 19 | return; 20 | } 21 | const currentVersion = getVersion(); 22 | const latestVersion = await fetchVersion(); 23 | res.status(200).send({ status: 'OK', currentVersion, latestVersion }); 24 | }); 25 | 26 | app.post(config.APP_WEBHOOK_PATH, validateLineSignature, async (req, res) => { 27 | try { 28 | await storage.initialize(); 29 | await handleEvents(req.body.events); 30 | res.sendStatus(200); 31 | } catch (err) { 32 | console.error(err.message); 33 | res.sendStatus(500); 34 | } 35 | if (config.APP_DEBUG) printPrompts(); 36 | }); 37 | 38 | if (config.APP_PORT) { 39 | app.listen(config.APP_PORT); 40 | } 41 | 42 | export default app; 43 | -------------------------------------------------------------------------------- /storage/index.js: -------------------------------------------------------------------------------- 1 | import config from '../config/index.js'; 2 | import { createEnvironment, ENV_TYPE_PLAIN, updateEnvironment } from '../services/vercel.js'; 3 | import { fetchEnvironment } from '../utils/index.js'; 4 | 5 | const ENV_KEY = 'APP_STORAGE'; 6 | 7 | class Storage { 8 | env; 9 | 10 | data = {}; 11 | 12 | async initialize() { 13 | if (!config.VERCEL_ACCESS_TOKEN) return; 14 | this.env = await fetchEnvironment(ENV_KEY); 15 | if (!this.env) { 16 | const { data } = await createEnvironment({ 17 | key: ENV_KEY, 18 | value: JSON.stringify(this.data), 19 | type: ENV_TYPE_PLAIN, 20 | }); 21 | this.env = data.created; 22 | } 23 | this.data = JSON.parse(this.env.value); 24 | } 25 | 26 | /** 27 | * @param {string} key 28 | * @returns {string} 29 | */ 30 | getItem(key) { 31 | return this.data[key]; 32 | } 33 | 34 | /** 35 | * @param {string} key 36 | * @param {string} value 37 | */ 38 | async setItem(key, value) { 39 | this.data[key] = value; 40 | if (!config.VERCEL_ACCESS_TOKEN) return; 41 | await updateEnvironment({ 42 | id: this.env.id, 43 | value: JSON.stringify(this.data, null, config.VERCEL_ENV ? 0 : 2), 44 | type: ENV_TYPE_PLAIN, 45 | }); 46 | } 47 | } 48 | 49 | const storage = new Storage(); 50 | 51 | export default storage; 52 | -------------------------------------------------------------------------------- /app/history/index.js: -------------------------------------------------------------------------------- 1 | import History from './history.js'; 2 | 3 | const histories = new Map(); 4 | 5 | /** 6 | * @param {string} contextId 7 | * @returns {History} 8 | */ 9 | const getHistory = (contextId) => histories.get(contextId) || new History(); 10 | 11 | /** 12 | * @param {string} contextId 13 | * @param {History} history 14 | * @returns {History} 15 | */ 16 | const setHistory = (contextId, history) => histories.set(contextId, history); 17 | 18 | /** 19 | * @param {string} contextId 20 | * @param {function(History)} callback 21 | */ 22 | const updateHistory = (contextId, callback) => { 23 | const history = getHistory(contextId); 24 | callback(history); 25 | setHistory(contextId, history); 26 | }; 27 | 28 | /** 29 | * @param {string} userId 30 | */ 31 | const removeHistory = (userId) => { 32 | histories.delete(userId); 33 | }; 34 | 35 | const printHistories = () => { 36 | const messages = Array.from(histories.keys()) 37 | .filter((contextId) => getHistory(contextId).messages.length > 0) 38 | .map((contextId) => `\n=== ${contextId.slice(0, 6)} ===\n\n${getHistory(contextId).toString()}\n`); 39 | if (messages.length < 1) return; 40 | console.info(messages.join('')); 41 | }; 42 | 43 | export { 44 | getHistory, 45 | setHistory, 46 | updateHistory, 47 | removeHistory, 48 | printHistories, 49 | }; 50 | 51 | export default histories; 52 | -------------------------------------------------------------------------------- /tests/enquire.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | afterEach, beforeEach, expect, test, 3 | } from '@jest/globals'; 4 | import { COMMAND_BOT_TALK, COMMAND_SUM_SUM } from '../app/commands/index.js'; 5 | import { getPrompt, handleEvents, removePrompt } from '../app/index.js'; 6 | import { MOCK_GROUP_01 } from '../constants/mock.js'; 7 | import { 8 | createEvents, MOCK_TEXT_OK, MOCK_USER_01, MOCK_USER_02, TIMEOUT, 9 | } from './utils.js'; 10 | 11 | beforeEach(async () => { 12 | // 13 | }); 14 | 15 | afterEach(() => { 16 | removePrompt(MOCK_USER_01); 17 | removePrompt(MOCK_USER_02); 18 | }); 19 | 20 | test('COMMAND_ENQUIRE', async () => { 21 | try { 22 | await handleEvents(createEvents([`${COMMAND_BOT_TALK.text}人工智慧`], MOCK_GROUP_01, MOCK_USER_01)); 23 | } catch (err) { 24 | console.error(err); 25 | } 26 | const events = [ 27 | ...createEvents([`${COMMAND_SUM_SUM.text}`], MOCK_GROUP_01, MOCK_USER_02), 28 | ]; 29 | let results; 30 | try { 31 | results = await handleEvents(events); 32 | } catch (err) { 33 | console.error(err); 34 | } 35 | expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5); 36 | expect(getPrompt(MOCK_USER_02).messages.length).toEqual(6); 37 | const replies = results.map(({ messages }) => messages.map(({ text }) => text)); 38 | expect(replies).toEqual( 39 | [ 40 | [MOCK_TEXT_OK], 41 | ], 42 | ); 43 | }, TIMEOUT); 44 | -------------------------------------------------------------------------------- /tests/activate.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | afterEach, beforeEach, expect, test, 3 | } from '@jest/globals'; 4 | import { getPrompt, handleEvents, removePrompt } from '../app/index.js'; 5 | import { COMMAND_BOT_ACTIVATE, COMMAND_BOT_DEACTIVATE } from '../app/commands/index.js'; 6 | import { t } from '../locales/index.js'; 7 | import { 8 | createEvents, MOCK_TEXT_OK, MOCK_USER_01, TIMEOUT, 9 | } from './utils.js'; 10 | 11 | beforeEach(async () => { 12 | const events = [ 13 | ...createEvents([COMMAND_BOT_DEACTIVATE.text]), 14 | ]; 15 | await handleEvents(events); 16 | }); 17 | 18 | afterEach(() => { 19 | removePrompt(MOCK_USER_01); 20 | }); 21 | 22 | test('COMMAND_BOT_ACTIVATE', async () => { 23 | const events = [ 24 | ...createEvents(['嗨!']), // should be ignored 25 | ...createEvents([COMMAND_BOT_ACTIVATE.text]), 26 | ...createEvents(['嗨!']), 27 | ]; 28 | let results; 29 | try { 30 | results = await handleEvents(events); 31 | } catch (err) { 32 | console.error(err); 33 | } 34 | expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5); 35 | const replies = results.map(({ messages }) => messages.map(({ text }) => text)); 36 | expect(replies).toEqual( 37 | [ 38 | [ 39 | t('__ERROR_MISSING_ENV')('VERCEL_ACCESS_TOKEN'), 40 | COMMAND_BOT_ACTIVATE.reply, 41 | ], 42 | [MOCK_TEXT_OK], 43 | ], 44 | ); 45 | }, TIMEOUT); 46 | -------------------------------------------------------------------------------- /app/handlers/draw.js: -------------------------------------------------------------------------------- 1 | import config from '../../config/index.js'; 2 | import { MOCK_TEXT_OK } from '../../constants/mock.js'; 3 | import { ROLE_AI, ROLE_HUMAN } from '../../services/openai.js'; 4 | import { generateImage } from '../../utils/index.js'; 5 | import { COMMAND_BOT_DRAW } from '../commands/index.js'; 6 | import Context from '../context.js'; 7 | import { updateHistory } from '../history/index.js'; 8 | import { getPrompt, setPrompt } from '../prompt/index.js'; 9 | 10 | /** 11 | * @param {Context} context 12 | * @returns {boolean} 13 | */ 14 | const check = (context) => context.hasCommand(COMMAND_BOT_DRAW); 15 | 16 | /** 17 | * @param {Context} context 18 | * @returns {Promise} 19 | */ 20 | const exec = (context) => check(context) && ( 21 | async () => { 22 | const prompt = getPrompt(context.userId); 23 | prompt.write(ROLE_HUMAN, `${context.trimmedText}`).write(ROLE_AI); 24 | try { 25 | const trimmedText = context.trimmedText.replace(COMMAND_BOT_DRAW.text, ''); 26 | const { url } = await generateImage({ prompt: trimmedText }); 27 | prompt.patch(MOCK_TEXT_OK); 28 | setPrompt(context.userId, prompt); 29 | updateHistory(context.id, (history) => history.write(config.BOT_NAME, MOCK_TEXT_OK)); 30 | context.pushImage(url); 31 | } catch (err) { 32 | context.pushError(err); 33 | } 34 | return context; 35 | } 36 | )(); 37 | 38 | export default exec; 39 | -------------------------------------------------------------------------------- /app/handlers/retry.js: -------------------------------------------------------------------------------- 1 | import config from '../../config/index.js'; 2 | import { ROLE_AI } from '../../services/openai.js'; 3 | import { generateCompletion } from '../../utils/index.js'; 4 | import { COMMAND_BOT_CONTINUE, COMMAND_BOT_RETRY, GENERAL_COMMANDS } from '../commands/index.js'; 5 | import Context from '../context.js'; 6 | import { updateHistory } from '../history/index.js'; 7 | import { getPrompt, setPrompt } from '../prompt/index.js'; 8 | 9 | /** 10 | * @param {Context} context 11 | * @returns {boolean} 12 | */ 13 | const check = (context) => context.hasCommand(COMMAND_BOT_RETRY); 14 | 15 | /** 16 | * @param {Context} context 17 | * @returns {Promise} 18 | */ 19 | const exec = (context) => check(context) && ( 20 | async () => { 21 | updateHistory(context.id, (history) => history.erase()); 22 | const prompt = getPrompt(context.userId); 23 | prompt.erase().write(ROLE_AI); 24 | try { 25 | const { text, isFinishReasonStop } = await generateCompletion({ prompt }); 26 | prompt.patch(text); 27 | setPrompt(context.userId, prompt); 28 | updateHistory(context.id, (history) => history.write(config.BOT_NAME, text)); 29 | const actions = isFinishReasonStop ? [] : [COMMAND_BOT_CONTINUE]; 30 | context.pushText(text, actions); 31 | } catch (err) { 32 | context.pushError(err); 33 | } 34 | return context; 35 | } 36 | )(); 37 | 38 | export default exec; 39 | -------------------------------------------------------------------------------- /tests/deactivate.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | afterEach, beforeEach, expect, test, 3 | } from '@jest/globals'; 4 | import { getPrompt, handleEvents, removePrompt } from '../app/index.js'; 5 | import { COMMAND_BOT_ACTIVATE, COMMAND_BOT_DEACTIVATE } from '../app/commands/index.js'; 6 | import { t } from '../locales/index.js'; 7 | import storage from '../storage/index.js'; 8 | import { 9 | createEvents, MOCK_TEXT_OK, MOCK_USER_01, TIMEOUT, 10 | } from './utils.js'; 11 | 12 | beforeEach(async () => { 13 | const events = [ 14 | ...createEvents([COMMAND_BOT_ACTIVATE.text]), 15 | ]; 16 | await handleEvents(events); 17 | }); 18 | 19 | afterEach(() => { 20 | removePrompt(MOCK_USER_01); 21 | }); 22 | 23 | test('COMMAND_BOT_DEACTIVATE', async () => { 24 | const events = [ 25 | ...createEvents(['嗨!']), 26 | ...createEvents([COMMAND_BOT_DEACTIVATE.text]), 27 | ...createEvents(['嗨!']), // should be ignored 28 | ]; 29 | let results; 30 | try { 31 | results = await handleEvents(events); 32 | } catch (err) { 33 | console.error(err); 34 | } 35 | expect(getPrompt(MOCK_USER_01).messages.length).toEqual(5); 36 | const replies = results.map(({ messages }) => messages.map(({ text }) => text)); 37 | expect(replies).toEqual( 38 | [ 39 | [MOCK_TEXT_OK], 40 | [ 41 | t('__ERROR_MISSING_ENV')('VERCEL_ACCESS_TOKEN'), 42 | COMMAND_BOT_DEACTIVATE.reply, 43 | ], 44 | ], 45 | ); 46 | }, TIMEOUT); 47 | -------------------------------------------------------------------------------- /app/handlers/continue.js: -------------------------------------------------------------------------------- 1 | import { generateCompletion } from '../../utils/index.js'; 2 | import { ALL_COMMANDS, COMMAND_BOT_CONTINUE } from '../commands/index.js'; 3 | import Context from '../context.js'; 4 | import { updateHistory } from '../history/index.js'; 5 | import { getPrompt, setPrompt } from '../prompt/index.js'; 6 | 7 | /** 8 | * @param {Context} context 9 | * @returns {boolean} 10 | */ 11 | const check = (context) => context.hasCommand(COMMAND_BOT_CONTINUE); 12 | 13 | /** 14 | * @param {Context} context 15 | * @returns {Promise} 16 | */ 17 | const exec = (context) => check(context) && ( 18 | async () => { 19 | updateHistory(context.id, (history) => history.erase()); 20 | const prompt = getPrompt(context.userId); 21 | const { lastMessage } = prompt; 22 | if (lastMessage.isEnquiring) prompt.erase(); 23 | try { 24 | const { text, isFinishReasonStop } = await generateCompletion({ prompt }); 25 | prompt.patch(text); 26 | if (lastMessage.isEnquiring && !isFinishReasonStop) prompt.write('', lastMessage.content); 27 | setPrompt(context.userId, prompt); 28 | if (!lastMessage.isEnquiring) updateHistory(context.id, (history) => history.patch(text)); 29 | const defaultActions = ALL_COMMANDS.filter(({ type }) => type === lastMessage.content); 30 | const actions = isFinishReasonStop ? defaultActions : [COMMAND_BOT_CONTINUE]; 31 | context.pushText(text, actions); 32 | } catch (err) { 33 | context.pushError(err); 34 | } 35 | return context; 36 | } 37 | )(); 38 | 39 | export default exec; 40 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import { replyMessage } from '../utils/index.js'; 2 | import { 3 | activateHandler, 4 | commandHandler, 5 | continueHandler, 6 | deactivateHandler, 7 | deployHandler, 8 | docHandler, 9 | drawHandler, 10 | forgetHandler, 11 | enquireHandler, 12 | reportHandler, 13 | retryHandler, 14 | searchHandler, 15 | talkHandler, 16 | versionHandler, 17 | } from './handlers/index.js'; 18 | import Context from './context.js'; 19 | import Event from './models/event.js'; 20 | 21 | /** 22 | * @param {Context} context 23 | * @returns {Promise} 24 | */ 25 | const handleContext = async (context) => ( 26 | activateHandler(context) 27 | || commandHandler(context) 28 | || continueHandler(context) 29 | || deactivateHandler(context) 30 | || deployHandler(context) 31 | || docHandler(context) 32 | || drawHandler(context) 33 | || forgetHandler(context) 34 | || enquireHandler(context) 35 | || reportHandler(context) 36 | || retryHandler(context) 37 | || searchHandler(context) 38 | || versionHandler(context) 39 | || talkHandler(context) 40 | || context 41 | ); 42 | 43 | const handleEvents = async (events = []) => ( 44 | (Promise.all( 45 | (await Promise.all( 46 | (await Promise.all( 47 | events 48 | .map((event) => new Event(event)) 49 | .filter((event) => event.isMessage) 50 | .filter((event) => event.isText || event.isAudio || event.isImage) 51 | .map((event) => new Context(event)) 52 | .map((context) => context.initialize()), 53 | )) 54 | .map((context) => (context.error ? context : handleContext(context))), 55 | )) 56 | .filter((context) => context.messages.length > 0) 57 | .map((context) => replyMessage(context)), 58 | )) 59 | ); 60 | 61 | export default handleEvents; 62 | -------------------------------------------------------------------------------- /app/handlers/talk.js: -------------------------------------------------------------------------------- 1 | import config from '../../config/index.js'; 2 | import { t } from '../../locales/index.js'; 3 | import { ROLE_AI, ROLE_HUMAN } from '../../services/openai.js'; 4 | import { generateCompletion } from '../../utils/index.js'; 5 | import { COMMAND_BOT_CONTINUE, COMMAND_BOT_FORGET, COMMAND_BOT_TALK } from '../commands/index.js'; 6 | import Context from '../context.js'; 7 | import { updateHistory } from '../history/index.js'; 8 | import { getPrompt, setPrompt } from '../prompt/index.js'; 9 | 10 | /** 11 | * @param {Context} context 12 | * @returns {boolean} 13 | */ 14 | const check = (context) => ( 15 | context.hasCommand(COMMAND_BOT_TALK) 16 | || context.hasBotName 17 | || context.source.bot.isActivated 18 | ); 19 | 20 | /** 21 | * @param {Context} context 22 | * @returns {Promise} 23 | */ 24 | const exec = (context) => check(context) && ( 25 | async () => { 26 | const prompt = getPrompt(context.userId); 27 | try { 28 | if (context.event.isText) { 29 | prompt.write(ROLE_HUMAN, `${t('__COMPLETION_DEFAULT_AI_TONE')(config.BOT_TONE)}${context.trimmedText}`).write(ROLE_AI); 30 | } 31 | if (context.event.isImage) { 32 | const { trimmedText } = context; 33 | prompt.writeImage(ROLE_HUMAN, trimmedText).write(ROLE_AI); 34 | } 35 | const { text, isFinishReasonStop } = await generateCompletion({ prompt }); 36 | prompt.patch(text); 37 | setPrompt(context.userId, prompt); 38 | updateHistory(context.id, (history) => history.write(config.BOT_NAME, text)); 39 | const actions = isFinishReasonStop ? [COMMAND_BOT_FORGET] : [COMMAND_BOT_CONTINUE]; 40 | context.pushText(text, actions); 41 | } catch (err) { 42 | context.pushError(err); 43 | } 44 | return context; 45 | } 46 | )(); 47 | 48 | export default exec; 49 | -------------------------------------------------------------------------------- /app/handlers/search.js: -------------------------------------------------------------------------------- 1 | import config from '../../config/index.js'; 2 | import { t } from '../../locales/index.js'; 3 | import { ROLE_AI, ROLE_HUMAN } from '../../services/openai.js'; 4 | import { fetchAnswer, generateCompletion } from '../../utils/index.js'; 5 | import { COMMAND_BOT_CONTINUE, COMMAND_BOT_SEARCH } from '../commands/index.js'; 6 | import Context from '../context.js'; 7 | import { updateHistory } from '../history/index.js'; 8 | import { getPrompt, setPrompt } from '../prompt/index.js'; 9 | 10 | /** 11 | * @param {Context} context 12 | * @returns {boolean} 13 | */ 14 | const check = (context) => context.hasCommand(COMMAND_BOT_SEARCH); 15 | 16 | /** 17 | * @param {Context} context 18 | * @returns {Promise} 19 | */ 20 | const exec = (context) => check(context) && ( 21 | async () => { 22 | let trimmedText = context.trimmedText.replace(COMMAND_BOT_SEARCH.text, ''); 23 | const prompt = getPrompt(context.userId); 24 | if (!config.SERPAPI_API_KEY) context.pushText(t('__ERROR_MISSING_ENV')('SERPAPI_API_KEY')); 25 | try { 26 | const { answer } = await fetchAnswer(trimmedText); 27 | trimmedText = `${t('__COMPLETION_SEARCH')(answer || t('__COMPLETION_SEARCH_NOT_FOUND'), trimmedText)}`; 28 | } catch (err) { 29 | return context.pushError(err); 30 | } 31 | prompt.write(ROLE_HUMAN, `${trimmedText}`).write(ROLE_AI); 32 | try { 33 | const { text, isFinishReasonStop } = await generateCompletion({ prompt }); 34 | prompt.patch(text); 35 | setPrompt(context.userId, prompt); 36 | updateHistory(context.id, (history) => history.write(config.BOT_NAME, text)); 37 | const actions = isFinishReasonStop ? [] : [COMMAND_BOT_CONTINUE]; 38 | context.pushText(text, actions); 39 | } catch (err) { 40 | context.pushError(err); 41 | } 42 | return context; 43 | } 44 | )(); 45 | 46 | export default exec; 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPT AI Assistant 2 | 3 |
4 | 5 | [![license](https://img.shields.io/pypi/l/ansicolortags.svg)](LICENSE) [![Release](https://img.shields.io/github/release/memochou1993/gpt-ai-assistant)](https://GitHub.com/memochou1993/gpt-ai-assistant/releases/) 6 | 7 |
8 | 9 | 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. 10 | 11 | ## News 12 | 13 | - 2024-07-10: The `4.9` version now support `gpt-4o` OpenAI model. :fire: 14 | - 2023-05-03: The `4.6` version now support `gpt-4` OpenAI model. 15 | - 2023-03-05: The `4.1` version now support the audio message of LINE and `whisper-1` OpenAI model. 16 | - 2023-03-02: The `4.0` version now support `gpt-3.5-turbo` OpenAI model. 17 | 18 | ## Documentations 19 | 20 | - 中文 21 | - English 22 | 23 | ## Credits 24 | 25 | - [jayer95](https://github.com/jayer95) - Debugging and testing 26 | - [kkdai](https://github.com/kkdai) - Idea of `sum` command 27 | - [Dayu0815](https://github.com/Dayu0815) - Idea of `search` command 28 | - [mics8128](https://github.com/mics8128) - Implementing new features 29 | - [myh-st](https://github.com/myh-st) - Implementing new features 30 | - [Jakevin](https://github.com/Jakevin) - Implementing new features 31 | - [cdcd72](https://github.com/cdcd72) - Implementing new features 32 | - [All other contributors](https://github.com/memochou1993/gpt-ai-assistant/graphs/contributors) 33 | 34 | ## Contact 35 | 36 | If there is any question, please contact me at memochou1993@gmail.com. Thank you. 37 | 38 | ## Changelog 39 | 40 | Detailed changes for each release are documented in the [release notes](https://github.com/memochou1993/gpt-ai-assistant/releases). 41 | 42 | ## License 43 | 44 | [MIT](LICENSE) 45 | -------------------------------------------------------------------------------- /app/models/event.js: -------------------------------------------------------------------------------- 1 | import { 2 | EVENT_TYPE_MESSAGE, 3 | MESSAGE_TYPE_AUDIO, 4 | MESSAGE_TYPE_STICKER, 5 | MESSAGE_TYPE_TEXT, 6 | MESSAGE_TYPE_IMAGE, 7 | SOURCE_TYPE_GROUP, 8 | } from '../../services/line.js'; 9 | 10 | class Event { 11 | type; 12 | 13 | replyToken; 14 | 15 | source; 16 | 17 | message; 18 | 19 | constructor({ 20 | type, 21 | replyToken, 22 | source, 23 | message, 24 | }) { 25 | this.type = type; 26 | this.replyToken = replyToken; 27 | this.source = source; 28 | this.message = message; 29 | } 30 | 31 | /** 32 | * @returns {boolean} 33 | */ 34 | get isMessage() { 35 | return this.type === EVENT_TYPE_MESSAGE; 36 | } 37 | 38 | /** 39 | * @returns {boolean} 40 | */ 41 | get isGroup() { 42 | return this.source.type === SOURCE_TYPE_GROUP; 43 | } 44 | 45 | /** 46 | * @returns {boolean} 47 | */ 48 | get isText() { 49 | return this.message.type === MESSAGE_TYPE_TEXT; 50 | } 51 | 52 | /** 53 | * @returns {boolean} 54 | */ 55 | get isSticker() { 56 | return this.message.type === MESSAGE_TYPE_STICKER; 57 | } 58 | 59 | /** 60 | * @returns {boolean} 61 | */ 62 | get isAudio() { 63 | return this.message.type === MESSAGE_TYPE_AUDIO; 64 | } 65 | 66 | /** 67 | * @returns {boolean} 68 | */ 69 | get isImage() { 70 | return this.message.type === MESSAGE_TYPE_IMAGE; 71 | } 72 | 73 | /** 74 | * @returns {string} 75 | */ 76 | get groupId() { 77 | return this.source.groupId; 78 | } 79 | 80 | /** 81 | * @returns {string} 82 | */ 83 | get userId() { 84 | return this.source.userId; 85 | } 86 | 87 | /** 88 | * @returns {string} 89 | */ 90 | get messageId() { 91 | return this.message.id; 92 | } 93 | 94 | /** 95 | * @returns {string} 96 | */ 97 | get text() { 98 | return this.message.text; 99 | } 100 | } 101 | 102 | export default Event; 103 | -------------------------------------------------------------------------------- /app/history/history.js: -------------------------------------------------------------------------------- 1 | import { encode } from 'gpt-3-encoder'; 2 | import config from '../../config/index.js'; 3 | import { t } from '../../locales/index.js'; 4 | import { addMark } from '../../utils/index.js'; 5 | import Message from './message.js'; 6 | 7 | const MAX_MESSAGES = config.APP_MAX_PROMPT_MESSAGES / 2; 8 | const MAX_TOKENS = config.APP_MAX_PROMPT_TOKENS / 2; 9 | 10 | class History { 11 | messages = []; 12 | 13 | /** 14 | * @returns {Message} 15 | */ 16 | get lastMessage() { 17 | return this.messages.length > 0 ? this.messages[this.messages.length - 1] : null; 18 | } 19 | 20 | get tokenCount() { 21 | const encoded = encode(this.toString()); 22 | return encoded.length; 23 | } 24 | 25 | erase() { 26 | if (this.messages.length > 0) { 27 | this.messages.pop(); 28 | } 29 | return this; 30 | } 31 | 32 | /** 33 | * @param {string} role 34 | * @param {string} content 35 | */ 36 | write(role, content) { 37 | if (this.messages.length >= MAX_MESSAGES || this.tokenCount >= MAX_TOKENS) { 38 | this.messages.shift(); 39 | } 40 | this.messages.push(new Message({ role, content: addMark(content) })); 41 | return this; 42 | } 43 | 44 | /** 45 | * @param {string} role 46 | * @param {string} content 47 | */ 48 | writeImage(role, content = '') { 49 | const imageContent = [ 50 | { 51 | type: 'text', 52 | text: t('__COMPLETION_VISION'), 53 | }, 54 | { 55 | type: 'image', 56 | image_url: { 57 | url: content, 58 | }, 59 | }, 60 | ]; 61 | this.messages.push(new Message({ role, content: imageContent })); 62 | return this; 63 | } 64 | 65 | /** 66 | * @param {string} content 67 | */ 68 | patch(content) { 69 | if (this.messages.length < 1) return; 70 | this.messages[this.messages.length - 1].content += content; 71 | } 72 | 73 | toString() { 74 | return this.messages.map((record) => record.toString()).join('\n'); 75 | } 76 | } 77 | 78 | export default History; 79 | -------------------------------------------------------------------------------- /app/handlers/enquire.js: -------------------------------------------------------------------------------- 1 | import { TYPE_TRANSLATE } from '../../constants/command.js'; 2 | import { t } from '../../locales/index.js'; 3 | import { ROLE_AI, ROLE_HUMAN } from '../../services/openai.js'; 4 | import { generateCompletion, getCommand } from '../../utils/index.js'; 5 | import { ALL_COMMANDS, COMMAND_BOT_CONTINUE, ENQUIRE_COMMANDS } from '../commands/index.js'; 6 | import Context from '../context.js'; 7 | import { getHistory, updateHistory } from '../history/index.js'; 8 | import { getPrompt, setPrompt, Prompt } from '../prompt/index.js'; 9 | 10 | /** 11 | * @param {Context} context 12 | * @returns {boolean} 13 | */ 14 | const check = (context) => ( 15 | [...ENQUIRE_COMMANDS] 16 | .sort((a, b) => b.text.length - a.text.length) 17 | .some((command) => context.hasCommand(command)) 18 | ); 19 | 20 | /** 21 | * @param {Context} context 22 | * @returns {Promise} 23 | */ 24 | const exec = (context) => check(context) && ( 25 | async () => { 26 | updateHistory(context.id, (history) => history.erase()); 27 | const command = getCommand(context.trimmedText); 28 | const history = getHistory(context.id); 29 | if (!history.lastMessage) return context; 30 | const reference = command.type === TYPE_TRANSLATE ? history.lastMessage.content : history.toString(); 31 | const content = `${command.prompt}\n${t('__COMPLETION_QUOTATION_MARK_OPENING')}\n${reference}\n${t('__COMPLETION_QUOTATION_MARK_CLOSING')}`; 32 | const partial = (new Prompt()).write(ROLE_HUMAN, content); 33 | const prompt = getPrompt(context.userId); 34 | prompt.write(ROLE_HUMAN, content).write(ROLE_AI); 35 | try { 36 | const { text, isFinishReasonStop } = await generateCompletion({ prompt: partial }); 37 | prompt.patch(text); 38 | if (!isFinishReasonStop) prompt.write('', command.type); 39 | setPrompt(context.userId, prompt); 40 | const defaultActions = ALL_COMMANDS.filter(({ type }) => type === command.type); 41 | const actions = isFinishReasonStop ? defaultActions : [COMMAND_BOT_CONTINUE]; 42 | context.pushText(text, actions); 43 | } catch (err) { 44 | context.pushError(err); 45 | } 46 | return context; 47 | } 48 | )(); 49 | 50 | export default exec; 51 | -------------------------------------------------------------------------------- /services/vercel.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import config from '../config/index.js'; 3 | import { handleFulfilled, handleRejected, handleRequest } from './utils/index.js'; 4 | 5 | export const ENV_TYPE_ENCRYPTED = 'encrypted'; 6 | export const ENV_TYPE_PLAIN = 'plain'; 7 | 8 | export const ENV_TARGET_PRODUCTION = 'production'; 9 | export const ENV_TARGET_PREVIEW = 'preview'; 10 | export const ENV_TARGET_DEVELOPMENT = 'development'; 11 | 12 | const client = axios.create({ 13 | baseURL: 'https://api.vercel.com', 14 | timeout: config.VERCEL_TIMEOUT, 15 | headers: { 16 | 'Accept-Encoding': 'gzip, deflate, compress', 17 | }, 18 | }); 19 | 20 | client.interceptors.request.use((c) => { 21 | c.headers.Authorization = `Bearer ${config.VERCEL_ACCESS_TOKEN}`; 22 | return handleRequest(c); 23 | }); 24 | 25 | client.interceptors.response.use(handleFulfilled, (err) => { 26 | if (err.response?.data?.error?.message) { 27 | err.message = err.response.data.error.message; 28 | } 29 | return handleRejected(err); 30 | }); 31 | 32 | const fetchEnvironments = () => client.get(`/v9/projects/${config.VERCEL_PROJECT_NAME}/env`, { 33 | params: { 34 | ...(config.VERCEL_TEAM_ID ? { teamId: config.VERCEL_TEAM_ID } : {}), 35 | }, 36 | }); 37 | 38 | const createEnvironment = ({ 39 | key, 40 | value, 41 | type = ENV_TYPE_ENCRYPTED, 42 | target = [ENV_TARGET_PRODUCTION, ENV_TARGET_PREVIEW, ENV_TARGET_DEVELOPMENT], 43 | }) => client.post(`/v10/projects/${config.VERCEL_PROJECT_NAME}/env`, { 44 | key: String(key), 45 | value: String(value), 46 | type, 47 | target, 48 | }, { 49 | params: { 50 | ...(config.VERCEL_TEAM_ID ? { teamId: config.VERCEL_TEAM_ID } : {}), 51 | }, 52 | }); 53 | 54 | const updateEnvironment = ({ 55 | id, 56 | value, 57 | type = ENV_TYPE_ENCRYPTED, 58 | target = [ENV_TARGET_PRODUCTION, ENV_TARGET_PREVIEW, ENV_TARGET_DEVELOPMENT], 59 | }) => client.patch(`/v9/projects/${config.VERCEL_PROJECT_NAME}/env/${id}`, { 60 | value: String(value), 61 | type, 62 | target, 63 | }, { 64 | params: { 65 | ...(config.VERCEL_TEAM_ID ? { teamId: config.VERCEL_TEAM_ID } : {}), 66 | }, 67 | }); 68 | 69 | const deploy = () => axios.post(config.VERCEL_DEPLOY_HOOK_URL); 70 | 71 | export { 72 | fetchEnvironments, 73 | createEnvironment, 74 | updateEnvironment, 75 | deploy, 76 | }; 77 | -------------------------------------------------------------------------------- /app/prompt/prompt.js: -------------------------------------------------------------------------------- 1 | import { encode } from 'gpt-3-encoder'; 2 | import config from '../../config/index.js'; 3 | import { t } from '../../locales/index.js'; 4 | import { ROLE_AI, ROLE_HUMAN, ROLE_SYSTEM } from '../../services/openai.js'; 5 | import { addMark } from '../../utils/index.js'; 6 | import Message from './message.js'; 7 | 8 | const MAX_MESSAGES = config.APP_MAX_PROMPT_MESSAGES + 3; 9 | const MAX_TOKENS = config.APP_MAX_PROMPT_TOKENS; 10 | 11 | class Prompt { 12 | messages = []; 13 | 14 | constructor() { 15 | this 16 | .write(ROLE_SYSTEM, config.APP_INIT_PROMPT || t('__COMPLETION_DEFAULT_SYSTEM_PROMPT')) 17 | .write(ROLE_HUMAN, `${t('__COMPLETION_DEFAULT_HUMAN_PROMPT')(config.HUMAN_NAME)}${config.HUMAN_INIT_PROMPT}`) 18 | .write(ROLE_AI, `${t('__COMPLETION_DEFAULT_AI_PROMPT')(config.BOT_NAME)}${config.BOT_INIT_PROMPT}`); 19 | } 20 | 21 | /** 22 | * @returns {Message} 23 | */ 24 | get lastMessage() { 25 | return this.messages.length > 0 ? this.messages[this.messages.length - 1] : null; 26 | } 27 | 28 | get tokenCount() { 29 | const encoded = encode(this.toString()); 30 | return encoded.length; 31 | } 32 | 33 | erase() { 34 | if (this.messages.length > 0) { 35 | this.messages.pop(); 36 | } 37 | return this; 38 | } 39 | 40 | /** 41 | * @param {string} role 42 | * @param {string} content 43 | */ 44 | write(role, content = '') { 45 | if (this.messages.length >= MAX_MESSAGES || this.tokenCount >= MAX_TOKENS) { 46 | this.messages.splice(3, 1); 47 | } 48 | this.messages.push(new Message({ role, content: addMark(content) })); 49 | return this; 50 | } 51 | 52 | /** 53 | * @param {string} role 54 | * @param {string} content 55 | */ 56 | writeImage(role, content = '') { 57 | const imageContent = [ 58 | { 59 | type: 'text', 60 | text: t('__COMPLETION_VISION'), 61 | }, 62 | { 63 | type: 'image_url', 64 | image_url: { 65 | url: content, 66 | }, 67 | }, 68 | ]; 69 | this.messages.push(new Message({ role, content: imageContent })); 70 | return this; 71 | } 72 | 73 | /** 74 | * @param {string} content 75 | */ 76 | patch(content) { 77 | this.messages[this.messages.length - 1].content += content; 78 | } 79 | 80 | toString() { 81 | return this.messages.map((sentence) => sentence.toString()).join(''); 82 | } 83 | } 84 | 85 | export default Prompt; 86 | -------------------------------------------------------------------------------- /services/line.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import config from '../config/index.js'; 3 | import { handleFulfilled, handleRejected, handleRequest } from './utils/index.js'; 4 | 5 | export const EVENT_TYPE_MESSAGE = 'message'; 6 | export const EVENT_TYPE_POSTBACK = 'postback'; 7 | 8 | export const SOURCE_TYPE_USER = 'user'; 9 | export const SOURCE_TYPE_GROUP = 'group'; 10 | 11 | export const MESSAGE_TYPE_TEXT = 'text'; 12 | export const MESSAGE_TYPE_STICKER = 'sticker'; 13 | export const MESSAGE_TYPE_AUDIO = 'audio'; 14 | export const MESSAGE_TYPE_IMAGE = 'image'; 15 | export const MESSAGE_TYPE_TEMPLATE = 'template'; 16 | 17 | export const TEMPLATE_TYPE_BUTTONS = 'buttons'; 18 | 19 | export const ACTION_TYPE_MESSAGE = 'message'; 20 | export const ACTION_TYPE_POSTBACK = 'postback'; 21 | 22 | export const QUICK_REPLY_TYPE_ACTION = 'action'; 23 | 24 | const client = axios.create({ 25 | baseURL: 'https://api.line.me', 26 | timeout: config.LINE_TIMEOUT, 27 | headers: { 28 | 'Accept-Encoding': 'gzip, deflate, compress', 29 | }, 30 | }); 31 | 32 | client.interceptors.request.use((c) => { 33 | c.headers.Authorization = `Bearer ${config.LINE_CHANNEL_ACCESS_TOKEN}`; 34 | return handleRequest(c); 35 | }); 36 | 37 | client.interceptors.response.use(handleFulfilled, (err) => { 38 | if (err.response?.data?.message) { 39 | err.message = err.response.data.message; 40 | } 41 | return handleRejected(err); 42 | }); 43 | 44 | const reply = ({ 45 | replyToken, 46 | messages, 47 | }) => client.post('/v2/bot/message/reply', { 48 | replyToken, 49 | messages, 50 | }); 51 | 52 | const fetchGroupSummary = ({ 53 | groupId, 54 | }) => client.get(`/v2/bot/group/${groupId}/summary`); 55 | 56 | const fetchProfile = ({ 57 | userId, 58 | }) => client.get(`/v2/bot/profile/${userId}`); 59 | 60 | const dataClient = axios.create({ 61 | baseURL: 'https://api-data.line.me', 62 | timeout: config.LINE_TIMEOUT, 63 | headers: { 64 | 'Accept-Encoding': 'gzip, deflate, compress', 65 | }, 66 | }); 67 | 68 | dataClient.interceptors.request.use((c) => { 69 | c.headers.Authorization = `Bearer ${config.LINE_CHANNEL_ACCESS_TOKEN}`; 70 | return handleRequest(c); 71 | }); 72 | 73 | dataClient.interceptors.response.use(handleFulfilled, (err) => { 74 | if (err.response?.data?.message) { 75 | err.message = err.response.data.message; 76 | } 77 | return handleRejected(err); 78 | }); 79 | 80 | const fetchContent = ({ 81 | messageId, 82 | }) => dataClient.get(`/v2/bot/message/${messageId}/content`, { 83 | responseType: 'arraybuffer', 84 | }); 85 | 86 | export { 87 | reply, 88 | fetchGroupSummary, 89 | fetchProfile, 90 | fetchContent, 91 | }; 92 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | const { env } = process; 4 | 5 | dotenv.config({ 6 | path: env.NODE_ENV ? `.env.${env.NODE_ENV}` : '.env', 7 | }); 8 | 9 | const config = Object.freeze({ 10 | APP_ENV: env.NODE_ENV || 'production', 11 | APP_DEBUG: env.APP_DEBUG === 'true' || false, 12 | APP_URL: env.APP_URL || null, 13 | APP_PORT: env.APP_PORT || null, 14 | APP_LANG: env.APP_LANG || 'zh_TW', 15 | APP_WEBHOOK_PATH: env.APP_WEBHOOK_PATH || '/webhook', 16 | APP_API_TIMEOUT: env.APP_API_TIMEOUT || 9000, 17 | APP_MAX_GROUPS: Number(env.APP_MAX_GROUPS) || 1000, 18 | APP_MAX_USERS: Number(env.APP_MAX_USERS) || 1000, 19 | APP_MAX_PROMPT_MESSAGES: Number(env.APP_MAX_PROMPT_MESSAGES) || 4, 20 | APP_MAX_PROMPT_TOKENS: Number(env.APP_MAX_PROMPT_TOKENS) || 256, 21 | APP_INIT_PROMPT: env.APP_INIT_PROMPT || '', 22 | HUMAN_NAME: env.HUMAN_NAME || '', 23 | HUMAN_INIT_PROMPT: env.HUMAN_INIT_PROMPT || '', 24 | BOT_NAME: env.BOT_NAME || 'AI', 25 | BOT_INIT_PROMPT: env.BOT_INIT_PROMPT || '', 26 | BOT_TONE: env.BOT_TONE || '', 27 | BOT_DEACTIVATED: env.BOT_DEACTIVATED === 'true' || false, 28 | ERROR_MESSAGE_DISABLED: env.ERROR_MESSAGE_DISABLED === 'true' || false, 29 | VERCEL_ENV: env.VERCEL_ENV || null, 30 | VERCEL_TIMEOUT: env.VERCEL_TIMEOUT || env.APP_API_TIMEOUT, 31 | VERCEL_PROJECT_NAME: env.VERCEL_PROJECT_NAME || env.VERCEL_GIT_REPO_SLUG || null, 32 | VERCEL_TEAM_ID: env.VERCEL_TEAM_ID || null, 33 | VERCEL_ACCESS_TOKEN: env.VERCEL_ACCESS_TOKEN || null, 34 | VERCEL_DEPLOY_HOOK_URL: env.VERCEL_DEPLOY_HOOK_URL || null, 35 | OPENAI_TIMEOUT: env.OPENAI_TIMEOUT || env.APP_API_TIMEOUT, 36 | OPENAI_API_KEY: env.OPENAI_API_KEY || null, 37 | OPENAI_BASE_URL: env.OPENAI_BASE_URL || 'https://api.openai.com', 38 | OPENAI_COMPLETION_MODEL: env.OPENAI_COMPLETION_MODEL || 'gpt-3.5-turbo', 39 | OPENAI_COMPLETION_TEMPERATURE: Number(env.OPENAI_COMPLETION_TEMPERATURE) || 1, 40 | OPENAI_COMPLETION_MAX_TOKENS: Number(env.OPENAI_COMPLETION_MAX_TOKENS) || 64, 41 | OPENAI_COMPLETION_FREQUENCY_PENALTY: Number(env.OPENAI_COMPLETION_FREQUENCY_PENALTY) || 0, 42 | OPENAI_COMPLETION_PRESENCE_PENALTY: Number(env.OPENAI_COMPLETION_PRESENCE_PENALTY) || 0.6, 43 | OPENAI_COMPLETION_STOP_SEQUENCES: env.OPENAI_COMPLETION_STOP_SEQUENCES ? String(env.OPENAI_COMPLETION_STOP_SEQUENCES).split(',') : [' assistant:', ' user:'], 44 | OPENAI_IMAGE_GENERATION_MODEL: env.OPENAI_IMAGE_GENERATION_MODEL || 'dall-e-2', 45 | OPENAI_IMAGE_GENERATION_SIZE: env.OPENAI_IMAGE_GENERATION_SIZE || '256x256', 46 | OPENAI_IMAGE_GENERATION_QUALITY: env.OPENAI_IMAGE_GENERATION_QUALITY || 'standard', 47 | OPENAI_VISION_MODEL: env.OPENAI_VISION_MODEL || 'gpt-4o', 48 | LINE_TIMEOUT: env.LINE_TIMEOUT || env.APP_API_TIMEOUT, 49 | LINE_CHANNEL_ACCESS_TOKEN: env.LINE_CHANNEL_ACCESS_TOKEN || null, 50 | LINE_CHANNEL_SECRET: env.LINE_CHANNEL_SECRET || null, 51 | SERPAPI_TIMEOUT: env.SERPAPI_TIMEOUT || env.APP_API_TIMEOUT, 52 | SERPAPI_API_KEY: env.SERPAPI_API_KEY || null, 53 | SERPAPI_LOCATION: env.SERPAPI_LOCATION || 'tw', 54 | }); 55 | 56 | export default config; 57 | -------------------------------------------------------------------------------- /services/openai.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import FormData from 'form-data'; 3 | import config from '../config/index.js'; 4 | import { handleFulfilled, handleRejected, handleRequest } from './utils/index.js'; 5 | 6 | export const ROLE_SYSTEM = 'system'; 7 | export const ROLE_AI = 'assistant'; 8 | export const ROLE_HUMAN = 'user'; 9 | 10 | export const FINISH_REASON_STOP = 'stop'; 11 | export const FINISH_REASON_LENGTH = 'length'; 12 | 13 | export const IMAGE_SIZE_256 = '256x256'; 14 | export const IMAGE_SIZE_512 = '512x512'; 15 | export const IMAGE_SIZE_1024 = '1024x1024'; 16 | 17 | export const MODEL_GPT_3_5_TURBO = 'gpt-3.5-turbo'; 18 | export const MODEL_GPT_4_OMNI = 'gpt-4o'; 19 | export const MODEL_WHISPER_1 = 'whisper-1'; 20 | export const MODEL_DALL_E_3 = 'dall-e-3'; 21 | 22 | const client = axios.create({ 23 | baseURL: config.OPENAI_BASE_URL, 24 | timeout: config.OPENAI_TIMEOUT, 25 | headers: { 26 | 'Accept-Encoding': 'gzip, deflate, compress', 27 | }, 28 | }); 29 | 30 | client.interceptors.request.use((c) => { 31 | c.headers.Authorization = `Bearer ${config.OPENAI_API_KEY}`; 32 | return handleRequest(c); 33 | }); 34 | 35 | client.interceptors.response.use(handleFulfilled, (err) => { 36 | if (err.response?.data?.error?.message) { 37 | err.message = err.response.data.error.message; 38 | } 39 | return handleRejected(err); 40 | }); 41 | 42 | const hasImage = ({ messages }) => ( 43 | messages.some(({ content }) => ( 44 | Array.isArray(content) && content.some((item) => item.image_url) 45 | )) 46 | ); 47 | 48 | const createChatCompletion = ({ 49 | model = config.OPENAI_COMPLETION_MODEL, 50 | messages, 51 | temperature = config.OPENAI_COMPLETION_TEMPERATURE, 52 | maxTokens = config.OPENAI_COMPLETION_MAX_TOKENS, 53 | frequencyPenalty = config.OPENAI_COMPLETION_FREQUENCY_PENALTY, 54 | presencePenalty = config.OPENAI_COMPLETION_PRESENCE_PENALTY, 55 | }) => { 56 | const body = { 57 | model: hasImage({ messages }) ? config.OPENAI_VISION_MODEL : model, 58 | messages, 59 | temperature, 60 | max_tokens: maxTokens, 61 | frequency_penalty: frequencyPenalty, 62 | presence_penalty: presencePenalty, 63 | }; 64 | return client.post('/v1/chat/completions', body); 65 | }; 66 | 67 | const createImage = ({ 68 | model = config.OPENAI_IMAGE_GENERATION_MODEL, 69 | prompt, 70 | size = config.OPENAI_IMAGE_GENERATION_SIZE, 71 | quality = config.OPENAI_IMAGE_GENERATION_QUALITY, 72 | n = 1, 73 | }) => { 74 | // set image size to 1024 when using the DALL-E 3 model and the requested size is 256 or 512. 75 | if (model === MODEL_DALL_E_3 && [IMAGE_SIZE_256, IMAGE_SIZE_512].includes(size)) { 76 | size = IMAGE_SIZE_1024; 77 | } 78 | 79 | return client.post('/v1/images/generations', { 80 | model, 81 | prompt, 82 | size, 83 | quality, 84 | n, 85 | }); 86 | }; 87 | 88 | const createAudioTranscriptions = ({ 89 | buffer, 90 | file, 91 | model = MODEL_WHISPER_1, 92 | }) => { 93 | const formData = new FormData(); 94 | formData.append('file', buffer, file); 95 | formData.append('model', model); 96 | return client.post('/v1/audio/transcriptions', formData.getBuffer(), { 97 | headers: formData.getHeaders(), 98 | }); 99 | }; 100 | 101 | export { 102 | createAudioTranscriptions, 103 | createChatCompletion, 104 | createImage, 105 | }; 106 | -------------------------------------------------------------------------------- /app/commands/index.js: -------------------------------------------------------------------------------- 1 | import { TYPE_ANALYZE, TYPE_SUM, TYPE_TRANSLATE } from '../../constants/command.js'; 2 | import COMMAND_ANALYZE_ANALYZE from './analyze-analyze.js'; 3 | import COMMAND_ANALYZE_LITERARILY from './analyze-literarily.js'; 4 | import COMMAND_ANALYZE_MATHEMATICALLY from './analyze-mathematically.js'; 5 | import COMMAND_ANALYZE_NUMEROLOGICALLY from './analyze-numerologically.js'; 6 | import COMMAND_ANALYZE_PHILOSOPHICALLY from './analyze-philosophically.js'; 7 | import COMMAND_ANALYZE_PSYCHOLOGICALLY from './analyze-psychologically.js'; 8 | import COMMAND_BOT_ACTIVATE from './bot-activate.js'; 9 | import COMMAND_BOT_CONTINUE from './bot-continue.js'; 10 | import COMMAND_BOT_DEACTIVATE from './bot-deactivate.js'; 11 | import COMMAND_BOT_DRAW_DEMO from './bot-draw-demo.js'; 12 | import COMMAND_BOT_DRAW from './bot-draw.js'; 13 | import COMMAND_BOT_FORGET from './bot-forget.js'; 14 | import COMMAND_BOT_RETRY from './bot-retry.js'; 15 | import COMMAND_BOT_SEARCH from './bot-search.js'; 16 | import COMMAND_BOT_SEARCH_DEMO from './bot-search-demo.js'; 17 | import COMMAND_BOT_SUMMON_DEMO from './bot-summon-demo.js'; 18 | import COMMAND_BOT_TALK_DEMO from './bot-talk-demo.js'; 19 | import COMMAND_BOT_TALK from './bot-talk.js'; 20 | import Command from './command.js'; 21 | import COMMAND_SUM_ADVISE from './sum-advise.js'; 22 | import COMMAND_SUM_APOLOGIZE from './sum-apologize.js'; 23 | import COMMAND_SUM_BLAME from './sum-blame.js'; 24 | import COMMAND_SUM_COMFORT from './sum-comfort.js'; 25 | import COMMAND_SUM_COMPLAIN from './sum-complain.js'; 26 | import COMMAND_SUM_ENCOURAGE from './sum-encourage.js'; 27 | import COMMAND_SUM_LAUGH from './sum-laugh.js'; 28 | import COMMAND_SUM_SUM from './sum-sum.js'; 29 | import COMMAND_SYS_COMMAND from './sys-command.js'; 30 | import COMMAND_SYS_DEPLOY from './sys-deploy.js'; 31 | import COMMAND_SYS_DOC from './sys-doc.js'; 32 | import COMMAND_SYS_REPORT from './sys-report.js'; 33 | import COMMAND_SYS_VERSION from './sys-version.js'; 34 | import COMMAND_TRANSLATE_TO_EN from './translate-to-en.js'; 35 | import COMMAND_TRANSLATE_TO_JA from './translate-to-ja.js'; 36 | 37 | export const ALL_COMMANDS = [ 38 | COMMAND_ANALYZE_ANALYZE, 39 | COMMAND_ANALYZE_LITERARILY, 40 | COMMAND_ANALYZE_MATHEMATICALLY, 41 | COMMAND_ANALYZE_NUMEROLOGICALLY, 42 | COMMAND_ANALYZE_PHILOSOPHICALLY, 43 | COMMAND_ANALYZE_PSYCHOLOGICALLY, 44 | COMMAND_BOT_ACTIVATE, 45 | COMMAND_BOT_CONTINUE, 46 | COMMAND_BOT_DEACTIVATE, 47 | COMMAND_BOT_DRAW_DEMO, 48 | COMMAND_BOT_DRAW, 49 | COMMAND_BOT_FORGET, 50 | COMMAND_BOT_RETRY, 51 | COMMAND_BOT_SEARCH, 52 | COMMAND_BOT_SUMMON_DEMO, 53 | COMMAND_BOT_TALK_DEMO, 54 | COMMAND_BOT_TALK, 55 | COMMAND_SUM_ADVISE, 56 | COMMAND_SUM_APOLOGIZE, 57 | COMMAND_SUM_BLAME, 58 | COMMAND_SUM_COMFORT, 59 | COMMAND_SUM_COMPLAIN, 60 | COMMAND_SUM_ENCOURAGE, 61 | COMMAND_SUM_LAUGH, 62 | COMMAND_SUM_SUM, 63 | COMMAND_SYS_COMMAND, 64 | COMMAND_SYS_DEPLOY, 65 | COMMAND_SYS_DOC, 66 | COMMAND_SYS_REPORT, 67 | COMMAND_SYS_VERSION, 68 | COMMAND_TRANSLATE_TO_EN, 69 | COMMAND_TRANSLATE_TO_JA, 70 | ]; 71 | 72 | export const INFO_COMMANDS = [ 73 | COMMAND_SYS_VERSION, 74 | COMMAND_SYS_DOC, 75 | COMMAND_SYS_REPORT, 76 | ]; 77 | 78 | export const GENERAL_COMMANDS = [ 79 | COMMAND_SYS_COMMAND, 80 | COMMAND_BOT_SUMMON_DEMO, 81 | COMMAND_BOT_TALK_DEMO, 82 | COMMAND_BOT_DRAW_DEMO, 83 | COMMAND_TRANSLATE_TO_EN, 84 | COMMAND_TRANSLATE_TO_JA, 85 | COMMAND_BOT_SEARCH_DEMO, 86 | COMMAND_BOT_FORGET, 87 | COMMAND_SUM_SUM, 88 | COMMAND_ANALYZE_ANALYZE, 89 | ]; 90 | 91 | export const ENQUIRE_COMMANDS = ALL_COMMANDS.filter(({ type }) => ( 92 | type === TYPE_SUM 93 | || type === TYPE_ANALYZE 94 | || type === TYPE_TRANSLATE 95 | )); 96 | 97 | export { 98 | COMMAND_ANALYZE_ANALYZE, 99 | COMMAND_ANALYZE_LITERARILY, 100 | COMMAND_ANALYZE_MATHEMATICALLY, 101 | COMMAND_ANALYZE_NUMEROLOGICALLY, 102 | COMMAND_ANALYZE_PHILOSOPHICALLY, 103 | COMMAND_ANALYZE_PSYCHOLOGICALLY, 104 | COMMAND_BOT_ACTIVATE, 105 | COMMAND_BOT_CONTINUE, 106 | COMMAND_BOT_DEACTIVATE, 107 | COMMAND_BOT_DRAW_DEMO, 108 | COMMAND_BOT_DRAW, 109 | COMMAND_BOT_FORGET, 110 | COMMAND_BOT_RETRY, 111 | COMMAND_BOT_SEARCH, 112 | COMMAND_BOT_SUMMON_DEMO, 113 | COMMAND_BOT_TALK_DEMO, 114 | COMMAND_BOT_TALK, 115 | Command, 116 | COMMAND_SUM_ADVISE, 117 | COMMAND_SUM_APOLOGIZE, 118 | COMMAND_SUM_BLAME, 119 | COMMAND_SUM_COMFORT, 120 | COMMAND_SUM_COMPLAIN, 121 | COMMAND_SUM_ENCOURAGE, 122 | COMMAND_SUM_LAUGH, 123 | COMMAND_SUM_SUM, 124 | COMMAND_SYS_COMMAND, 125 | COMMAND_SYS_DEPLOY, 126 | COMMAND_SYS_DOC, 127 | COMMAND_SYS_REPORT, 128 | COMMAND_SYS_VERSION, 129 | COMMAND_TRANSLATE_TO_EN, 130 | COMMAND_TRANSLATE_TO_JA, 131 | }; 132 | -------------------------------------------------------------------------------- /locales/zh.js: -------------------------------------------------------------------------------- 1 | const zh = { 2 | __COMMAND_ANALYZE_ANALYZE_LABEL: '分析', 3 | __COMMAND_ANALYZE_ANALYZE_TEXT: '分析', 4 | __COMMAND_ANALYZE_ANALYZE_PROMPT: '分析以下內容,並給予細節。', 5 | __COMMAND_ANALYZE_LITERARILY_LABEL: '文學分析', 6 | __COMMAND_ANALYZE_LITERARILY_TEXT: '文學分析', 7 | __COMMAND_ANALYZE_LITERARILY_PROMPT: '使用文學的角度分析以下內容,並給予細節。', 8 | __COMMAND_ANALYZE_MATHEMATICALLY_LABEL: '數學分析', 9 | __COMMAND_ANALYZE_MATHEMATICALLY_TEXT: '數學分析', 10 | __COMMAND_ANALYZE_MATHEMATICALLY_PROMPT: '使用數學的角度分析以下內容,並給予細節。', 11 | __COMMAND_ANALYZE_NUMEROLOGICALLY_LABEL: '命理學分析', 12 | __COMMAND_ANALYZE_NUMEROLOGICALLY_TEXT: '命理學分析', 13 | __COMMAND_ANALYZE_NUMEROLOGICALLY_PROMPT: '使用命理學的角度分析以下內容,並給予細節。', 14 | __COMMAND_ANALYZE_PHILOSOPHICALLY_LABEL: '哲學分析', 15 | __COMMAND_ANALYZE_PHILOSOPHICALLY_TEXT: '哲學分析', 16 | __COMMAND_ANALYZE_PHILOSOPHICALLY_PROMPT: '使用哲學的角度分析以下內容,並給予細節。', 17 | __COMMAND_ANALYZE_PSYCHOLOGICALLY_LABEL: '心理學分析', 18 | __COMMAND_ANALYZE_PSYCHOLOGICALLY_TEXT: '心理學分析', 19 | __COMMAND_ANALYZE_PSYCHOLOGICALLY_PROMPT: '使用心理學的角度分析以下內容,並給予細節。', 20 | __COMMAND_BOT_ACTIVATE_LABEL: '開啟自動回覆', 21 | __COMMAND_BOT_ACTIVATE_TEXT: '開啟自動回覆', 22 | __COMMAND_BOT_ACTIVATE_ALIASES: ['說話', '開始說話'], 23 | __COMMAND_BOT_ACTIVATE_REPLY: '已開啟自動回覆', 24 | __COMMAND_BOT_CONTINUE_LABEL: '繼續', 25 | __COMMAND_BOT_CONTINUE_TEXT: '繼續', 26 | __COMMAND_BOT_CONTINUE_ALIASES: [], 27 | __COMMAND_BOT_DEACTIVATE_LABEL: '關閉自動回覆', 28 | __COMMAND_BOT_DEACTIVATE_TEXT: '關閉自動回覆', 29 | __COMMAND_BOT_DEACTIVATE_ALIASES: ['閉嘴', '停止說話'], 30 | __COMMAND_BOT_DEACTIVATE_REPLY: '已關閉自動回覆', 31 | __COMMAND_BOT_DRAW_LABEL: '請畫', 32 | __COMMAND_BOT_DRAW_TEXT: '請畫', 33 | __COMMAND_BOT_DRAW_ALIASES: ['畫', '描繪'], 34 | __COMMAND_BOT_DRAW_DEMO_LABEL: '請畫', 35 | __COMMAND_BOT_DRAW_DEMO_TEXT: '請畫貓咪', 36 | __COMMAND_BOT_FORGET_LABEL: '忘記', 37 | __COMMAND_BOT_FORGET_TEXT: '忘記', 38 | __COMMAND_BOT_FORGET_ALIASES: ['重來'], 39 | __COMMAND_BOT_FORGET_REPLY: '已忘記', 40 | __COMMAND_BOT_RETRY_LABEL: '重試', 41 | __COMMAND_BOT_RETRY_TEXT: '重試', 42 | __COMMAND_BOT_RETRY_ALIASES: [], 43 | __COMMAND_BOT_SEARCH_LABEL: '查詢', 44 | __COMMAND_BOT_SEARCH_TEXT: '查詢', 45 | __COMMAND_BOT_SEARCH_ALIASES: ['搜尋', '查找'], 46 | __COMMAND_BOT_SEARCH_DEMO_LABEL: '查詢', 47 | __COMMAND_BOT_SEARCH_DEMO_TEXT: '查詢日期', 48 | __COMMAND_BOT_SUMMON_DEMO_LABEL: '呼叫', 49 | __COMMAND_BOT_SUMMON_DEMO_TEXT: '你好嗎?', 50 | __COMMAND_BOT_TALK_LABEL: '請問', 51 | __COMMAND_BOT_TALK_TEXT: '請問', 52 | __COMMAND_BOT_TALK_ALIASES: ['呼叫'], 53 | __COMMAND_BOT_TALK_DEMO_LABEL: '請問', 54 | __COMMAND_BOT_TALK_DEMO_TEXT: '請問你好嗎', 55 | __COMMAND_SUM_ADVISE_LABEL: '建議', 56 | __COMMAND_SUM_ADVISE_TEXT: '建議', 57 | __COMMAND_SUM_ADVISE_PROMPT: '總結以下內容,並給予適當的建議。', 58 | __COMMAND_SUM_APOLOGIZE_LABEL: '道歉', 59 | __COMMAND_SUM_APOLOGIZE_TEXT: '道歉', 60 | __COMMAND_SUM_APOLOGIZE_PROMPT: '總結以下內容,並給予誠懇的道歉。', 61 | __COMMAND_SUM_BLAME_LABEL: '譴責', 62 | __COMMAND_SUM_BLAME_TEXT: '譴責', 63 | __COMMAND_SUM_BLAME_PROMPT: '總結以下內容,並給予嚴厲的譴責。', 64 | __COMMAND_SUM_COMFORT_LABEL: '安慰', 65 | __COMMAND_SUM_COMFORT_TEXT: '安慰', 66 | __COMMAND_SUM_COMFORT_PROMPT: '總結以下內容,並給予溫暖的安慰。', 67 | __COMMAND_SUM_COMPLAIN_LABEL: '抱怨', 68 | __COMMAND_SUM_COMPLAIN_TEXT: '抱怨', 69 | __COMMAND_SUM_COMPLAIN_PROMPT: '總結以下內容,並給予輕微的抱怨。', 70 | __COMMAND_SUM_ENCOURAGE_LABEL: '鼓勵', 71 | __COMMAND_SUM_ENCOURAGE_TEXT: '鼓勵', 72 | __COMMAND_SUM_ENCOURAGE_PROMPT: '總結以下內容,並給予熱烈的鼓勵。', 73 | __COMMAND_SUM_LAUGH_LABEL: '嘲諷', 74 | __COMMAND_SUM_LAUGH_TEXT: '嘲諷', 75 | __COMMAND_SUM_LAUGH_PROMPT: '總結以下內容,並給予刻薄的嘲諷。', 76 | __COMMAND_SUM_SUM_LABEL: '總結', 77 | __COMMAND_SUM_SUM_TEXT: '總結', 78 | __COMMAND_SUM_SUM_PROMPT: '總結以下內容,並給予細節。', 79 | __COMMAND_SYS_COMMAND_LABEL: '指令', 80 | __COMMAND_SYS_COMMAND_TEXT: '指令', 81 | __COMMAND_SYS_DOC_LABEL: '查看文件', 82 | __COMMAND_SYS_DOC_TEXT: '文件', 83 | __COMMAND_SYS_DEPLOY_LABEL: '重新啟動', 84 | __COMMAND_SYS_DEPLOY_TEXT: '重新啟動', 85 | __COMMAND_SYS_DEPLOY_REPLY: '正在重新啟動', 86 | __COMMAND_SYS_REPORT_LABEL: '回報問題', 87 | __COMMAND_SYS_REPORT_TEXT: '回報', 88 | __COMMAND_SYS_VERSION_LABEL: '檢查更新', 89 | __COMMAND_SYS_VERSION_TEXT: '版本', 90 | __COMMAND_SYS_VERSION_REPLY: (version, isLatest) => `目前版本為 ${version}${isLatest ? ',已更新到最新版本' : ''}。`, 91 | __COMMAND_TRANSLATE_TO_EN_LABEL: '上一句翻成英文', 92 | __COMMAND_TRANSLATE_TO_EN_TEXT: '上一句翻成英文', 93 | __COMMAND_TRANSLATE_TO_EN_PROMPT: '將以下內容翻譯成英文。', 94 | __COMMAND_TRANSLATE_TO_JA_LABEL: '上一句翻成日文', 95 | __COMMAND_TRANSLATE_TO_JA_TEXT: '上一句翻成日文', 96 | __COMMAND_TRANSLATE_TO_JA_PROMPT: '將以下內容翻譯成日文。', 97 | __COMPLETION_DEFAULT_SYSTEM_PROMPT: '以下將使用繁體中文進行對話。', 98 | __COMPLETION_DEFAULT_HUMAN_PROMPT: (name) => (name ? `我是${name}` : '哈囉'), 99 | __COMPLETION_DEFAULT_AI_PROMPT: (name) => (name ? `我是${name}` : '哈囉'), 100 | __COMPLETION_DEFAULT_AI_TONE: (tone) => (tone ? `以${tone}的語氣回應我:` : ''), 101 | __COMPLETION_SEARCH: (a, q) => `根據「${a}」查詢結果,回答「${q}」問題`, 102 | __COMPLETION_SEARCH_NOT_FOUND: '查無資料', 103 | __COMPLETION_QUOTATION_MARK_OPENING: '「', 104 | __COMPLETION_QUOTATION_MARK_CLOSING: '」', 105 | __COMPLETION_VISION: '這張圖片裡有什麼?', 106 | __ERROR_ECONNABORTED: '這個問題太複雜了', 107 | __ERROR_UNKNOWN: '系統出了點狀況', 108 | __ERROR_MAX_GROUPS_REACHED: '群組數量到達上限了', 109 | __ERROR_MAX_USERS_REACHED: '用戶數量到達上限了', 110 | __ERROR_MISSING_ENV: (v) => `缺少環境變數:${v}`, 111 | __MESSAGE_NEW_VERSION_AVAILABLE: (version) => `最新版本為 ${version},請從 GitHub 更新。`, 112 | __SOURCE_NAME_SOME_GROUP: '某群組', 113 | __SOURCE_NAME_SOMEONE: '某用戶', 114 | }; 115 | 116 | export default zh; 117 | -------------------------------------------------------------------------------- /locales/ja.js: -------------------------------------------------------------------------------- 1 | const ja = { 2 | __COMMAND_ANALYZE_ANALYZE_LABEL: '分析して', 3 | __COMMAND_ANALYZE_ANALYZE_TEXT: '分析して', 4 | __COMMAND_ANALYZE_ANALYZE_PROMPT: '以下の内容を詳しく分析してください。', 5 | __COMMAND_ANALYZE_LITERARILY_LABEL: '文学的に分析して', 6 | __COMMAND_ANALYZE_LITERARILY_TEXT: '文学的に分析して', 7 | __COMMAND_ANALYZE_LITERARILY_PROMPT: '以下の内容を文学的に詳しく分析してください。', 8 | __COMMAND_ANALYZE_MATHEMATICALLY_LABEL: '数学的に分析して', 9 | __COMMAND_ANALYZE_MATHEMATICALLY_TEXT: '数学的に分析して', 10 | __COMMAND_ANALYZE_MATHEMATICALLY_PROMPT: '以下の内容を数学的に詳しく分析してください。', 11 | __COMMAND_ANALYZE_NUMEROLOGICALLY_LABEL: '算命学的に分析して', 12 | __COMMAND_ANALYZE_NUMEROLOGICALLY_TEXT: '算命学的に分析して', 13 | __COMMAND_ANALYZE_NUMEROLOGICALLY_PROMPT: '以下の内容を算命学的に詳しく分析してください。', 14 | __COMMAND_ANALYZE_PHILOSOPHICALLY_LABEL: '哲学的に分析して', 15 | __COMMAND_ANALYZE_PHILOSOPHICALLY_TEXT: '哲学的に分析して', 16 | __COMMAND_ANALYZE_PHILOSOPHICALLY_PROMPT: '以下の内容を哲学的に詳しく分析してください。', 17 | __COMMAND_ANALYZE_PSYCHOLOGICALLY_LABEL: '心理学的に分析して', 18 | __COMMAND_ANALYZE_PSYCHOLOGICALLY_TEXT: '心理学的に分析して', 19 | __COMMAND_ANALYZE_PSYCHOLOGICALLY_PROMPT: '以下の内容を心理学的に詳しく分析してください。', 20 | __COMMAND_BOT_ACTIVATE_LABEL: '自動応答をオンにする', 21 | __COMMAND_BOT_ACTIVATE_TEXT: '自動応答をオンにする', 22 | __COMMAND_BOT_ACTIVATE_ALIASES: [], 23 | __COMMAND_BOT_ACTIVATE_REPLY: 'オンにしました', 24 | __COMMAND_BOT_CONTINUE_LABEL: '続いて', 25 | __COMMAND_BOT_CONTINUE_TEXT: '続いて', 26 | __COMMAND_BOT_CONTINUE_ALIASES: [], 27 | __COMMAND_BOT_DEACTIVATE_LABEL: '自動応答をオフにする', 28 | __COMMAND_BOT_DEACTIVATE_TEXT: '自動応答をオフにする', 29 | __COMMAND_BOT_DEACTIVATE_ALIASES: [], 30 | __COMMAND_BOT_DEACTIVATE_REPLY: 'オフにしました', 31 | __COMMAND_BOT_DRAW_LABEL: '描いて', 32 | __COMMAND_BOT_DRAW_TEXT: '描いて', 33 | __COMMAND_BOT_DRAW_ALIASES: [], 34 | __COMMAND_BOT_DRAW_DEMO_LABEL: '描いて', 35 | __COMMAND_BOT_DRAW_DEMO_TEXT: '猫を描いて', 36 | __COMMAND_BOT_FORGET_LABEL: '忘記', // TODO 37 | __COMMAND_BOT_FORGET_TEXT: '忘記', // TODO 38 | __COMMAND_BOT_FORGET_ALIASES: [], 39 | __COMMAND_BOT_FORGET_REPLY: '已忘記', // TODO 40 | __COMMAND_BOT_RETRY_LABEL: 'リトライ', 41 | __COMMAND_BOT_RETRY_TEXT: 'リトライ', 42 | __COMMAND_BOT_RETRY_ALIASES: [], 43 | __COMMAND_BOT_SEARCH_LABEL: '查詢', // TODO 44 | __COMMAND_BOT_SEARCH_TEXT: '查詢', // TODO 45 | __COMMAND_BOT_SEARCH_ALIASES: [], 46 | __COMMAND_BOT_SEARCH_DEMO_LABEL: '查詢', // TODO 47 | __COMMAND_BOT_SEARCH_DEMO_TEXT: '查詢日期', // TODO 48 | __COMMAND_BOT_SUMMON_DEMO_LABEL: 'サモン', 49 | __COMMAND_BOT_SUMMON_DEMO_TEXT: '元気?', 50 | __COMMAND_BOT_TALK_LABEL: '話して', 51 | __COMMAND_BOT_TALK_TEXT: '話して', 52 | __COMMAND_BOT_TALK_ALIASES: [], 53 | __COMMAND_BOT_TALK_DEMO_LABEL: '話して', 54 | __COMMAND_BOT_TALK_DEMO_TEXT: '自分のことを話して', 55 | __COMMAND_SUM_ADVISE_LABEL: 'アドバイスをして', 56 | __COMMAND_SUM_ADVISE_TEXT: 'アドバイスをして', 57 | __COMMAND_SUM_ADVISE_PROMPT: '以下の内容を要約し、いいアドバイスをください。', 58 | __COMMAND_SUM_APOLOGIZE_LABEL: '謝って', 59 | __COMMAND_SUM_APOLOGIZE_TEXT: '謝って', 60 | __COMMAND_SUM_APOLOGIZE_PROMPT: '以下の内容を要約し、ちゃんと謝ってください。', 61 | __COMMAND_SUM_BLAME_LABEL: '責めて', 62 | __COMMAND_SUM_BLAME_TEXT: '責めて', 63 | __COMMAND_SUM_BLAME_PROMPT: '以下の内容を要約し、強く非難してください。', 64 | __COMMAND_SUM_COMFORT_LABEL: '慰めて', 65 | __COMMAND_SUM_COMFORT_TEXT: '慰めて', 66 | __COMMAND_SUM_COMFORT_PROMPT: '以下の内容を要約し、暖かく慰めてください。', 67 | __COMMAND_SUM_ENCOURAGE_LABEL: '励んで', 68 | __COMMAND_SUM_ENCOURAGE_TEXT: '励んで', 69 | __COMMAND_SUM_ENCOURAGE_PROMPT: '以下の内容を要約し、熱心に励んでください。', 70 | __COMMAND_SUM_COMPLAIN_LABEL: '愚痴を言って', 71 | __COMMAND_SUM_COMPLAIN_TEXT: '愚痴を言って', 72 | __COMMAND_SUM_COMPLAIN_PROMPT: '以下の内容を要約し、軽く愚痴を言ってください。', 73 | __COMMAND_SUM_LAUGH_LABEL: '笑って', 74 | __COMMAND_SUM_LAUGH_TEXT: '笑って', 75 | __COMMAND_SUM_LAUGH_PROMPT: '以下の内容を要約し、面白く笑ってください。', 76 | __COMMAND_SUM_SUM_LABEL: '要約して', 77 | __COMMAND_SUM_SUM_TEXT: '要約して', 78 | __COMMAND_SUM_SUM_PROMPT: '以下の内容を要約し、説明してください。', 79 | __COMMAND_SYS_COMMAND_LABEL: 'コマンド', 80 | __COMMAND_SYS_COMMAND_TEXT: 'コマンド', 81 | __COMMAND_SYS_DOC_LABEL: 'ドキュメンテーション', 82 | __COMMAND_SYS_DOC_TEXT: 'ドキュメンテーション', 83 | __COMMAND_SYS_DEPLOY_LABEL: '再起動', 84 | __COMMAND_SYS_DEPLOY_TEXT: '再起動', 85 | __COMMAND_SYS_DEPLOY_REPLY: '再起動しています', 86 | __COMMAND_SYS_REPORT_LABEL: 'バグレポート', 87 | __COMMAND_SYS_REPORT_TEXT: 'バグレポート', 88 | __COMMAND_SYS_VERSION_LABEL: 'バージョン', 89 | __COMMAND_SYS_VERSION_TEXT: 'バージョン', 90 | __COMMAND_SYS_VERSION_REPLY: (version, isLatest) => `Your version is ${isLatest ? 'up-to-date' : version}.`, 91 | __COMMAND_TRANSLATE_TO_EN_LABEL: '翻成英文', // TODO 92 | __COMMAND_TRANSLATE_TO_EN_TEXT: '翻成英文', // TODO 93 | __COMMAND_TRANSLATE_TO_EN_PROMPT: '請將以下內容翻譯成英文。', // TODO 94 | __COMMAND_TRANSLATE_TO_JA_LABEL: '翻成日文', // TODO 95 | __COMMAND_TRANSLATE_TO_JA_TEXT: '翻成日文', // TODO 96 | __COMMAND_TRANSLATE_TO_JA_PROMPT: '請將以下內容翻譯成日文。', // TODO 97 | __COMPLETION_DEFAULT_SYSTEM_PROMPT: '', // TODO 98 | __COMPLETION_DEFAULT_HUMAN_PROMPT: (name) => (name ? `私は${name}です` : 'こんにちは'), 99 | __COMPLETION_DEFAULT_AI_PROMPT: (name) => (name ? `私は${name}です` : 'こんにちは'), 100 | __COMPLETION_DEFAULT_AI_TONE: (tone) => (tone ? `以${tone}的語氣回應我:` : ''), // TODO 101 | __COMPLETION_SEARCH: (a, q) => `根據「${a}」查詢結果,回答「${q}」問題`, // TODO 102 | __COMPLETION_SEARCH_NOT_FOUND: '查無資料', // TODO 103 | __COMPLETION_QUOTATION_MARK_OPENING: '「', 104 | __COMPLETION_QUOTATION_MARK_CLOSING: '」', 105 | __COMPLETION_VISION: 'この画像には何がありますか?', 106 | __ERROR_ECONNABORTED: '接続がタイムアウトしました。', 107 | __ERROR_UNKNOWN: '技術的な問題が発生しています。', 108 | __ERROR_MAX_GROUPS_REACHED: '最大ユーザー数に達しています。', 109 | __ERROR_MAX_USERS_REACHED: '最大グループ数に達しています。', 110 | __ERROR_MISSING_ENV: (v) => `「${v}」環境変数が見つかりません。`, 111 | __MESSAGE_NEW_VERSION_AVAILABLE: (version) => `A new version ${version} is now available!`, 112 | __SOURCE_NAME_SOME_GROUP: 'あるグループ', 113 | __SOURCE_NAME_SOMEONE: 'あるユーザー', 114 | }; 115 | 116 | export default ja; 117 | -------------------------------------------------------------------------------- /locales/en.js: -------------------------------------------------------------------------------- 1 | const en = { 2 | __COMMAND_ANALYZE_ANALYZE_LABEL: 'Analyze', 3 | __COMMAND_ANALYZE_ANALYZE_TEXT: 'Analyze', 4 | __COMMAND_ANALYZE_ANALYZE_PROMPT: 'Please analyze the following statements.', 5 | __COMMAND_ANALYZE_LITERARILY_LABEL: 'Literarily', 6 | __COMMAND_ANALYZE_LITERARILY_TEXT: 'Analyze literarily', 7 | __COMMAND_ANALYZE_LITERARILY_PROMPT: 'Please analyze the following statements literarily.', 8 | __COMMAND_ANALYZE_MATHEMATICALLY_LABEL: 'Mathematically', 9 | __COMMAND_ANALYZE_MATHEMATICALLY_TEXT: 'Analyze mathematically', 10 | __COMMAND_ANALYZE_MATHEMATICALLY_PROMPT: 'Please analyze the following statements mathematically.', 11 | __COMMAND_ANALYZE_NUMEROLOGICALLY_LABEL: 'Numerologically', 12 | __COMMAND_ANALYZE_NUMEROLOGICALLY_TEXT: 'Analyze numerologically', 13 | __COMMAND_ANALYZE_NUMEROLOGICALLY_PROMPT: 'Please analyze the following statements numerologically.', 14 | __COMMAND_ANALYZE_PHILOSOPHICALLY_LABEL: 'Philosophically', 15 | __COMMAND_ANALYZE_PHILOSOPHICALLY_TEXT: 'Analyze philosophically', 16 | __COMMAND_ANALYZE_PHILOSOPHICALLY_PROMPT: 'Please analyze the following statements philosophically.', 17 | __COMMAND_ANALYZE_PSYCHOLOGICALLY_LABEL: 'Psychologically', 18 | __COMMAND_ANALYZE_PSYCHOLOGICALLY_TEXT: 'Analyze psychologically', 19 | __COMMAND_ANALYZE_PSYCHOLOGICALLY_PROMPT: 'Please analyze the following statements psychologically.', 20 | __COMMAND_BOT_ACTIVATE_LABEL: 'Activate', 21 | __COMMAND_BOT_ACTIVATE_TEXT: 'Activate', 22 | __COMMAND_BOT_ACTIVATE_ALIASES: [], 23 | __COMMAND_BOT_ACTIVATE_REPLY: 'Activated', 24 | __COMMAND_BOT_CONTINUE_LABEL: 'Continue', 25 | __COMMAND_BOT_CONTINUE_TEXT: 'Continue', 26 | __COMMAND_BOT_CONTINUE_ALIASES: [], 27 | __COMMAND_BOT_DEACTIVATE_LABEL: 'Deactivate', 28 | __COMMAND_BOT_DEACTIVATE_TEXT: 'Deactivate', 29 | __COMMAND_BOT_DEACTIVATE_ALIASES: [], 30 | __COMMAND_BOT_DEACTIVATE_REPLY: 'Deactivated', 31 | __COMMAND_BOT_DRAW_LABEL: 'Draw', 32 | __COMMAND_BOT_DRAW_TEXT: 'Draw', 33 | __COMMAND_BOT_DRAW_ALIASES: [], 34 | __COMMAND_BOT_DRAW_DEMO_LABEL: 'Draw', 35 | __COMMAND_BOT_DRAW_DEMO_TEXT: 'Draw a cat', 36 | __COMMAND_BOT_FORGET_LABEL: 'Forget', 37 | __COMMAND_BOT_FORGET_TEXT: 'Forget', 38 | __COMMAND_BOT_FORGET_ALIASES: [], 39 | __COMMAND_BOT_FORGET_REPLY: 'Forgot', 40 | __COMMAND_BOT_RETRY_LABEL: 'Retry', 41 | __COMMAND_BOT_RETRY_TEXT: 'Retry', 42 | __COMMAND_BOT_RETRY_ALIASES: [], 43 | __COMMAND_BOT_SEARCH_LABEL: 'Search', 44 | __COMMAND_BOT_SEARCH_TEXT: 'Search', 45 | __COMMAND_BOT_SEARCH_ALIASES: [], 46 | __COMMAND_BOT_SEARCH_DEMO_LABEL: 'Search', 47 | __COMMAND_BOT_SEARCH_DEMO_TEXT: 'Search date', 48 | __COMMAND_BOT_SUMMON_DEMO_LABEL: 'Summon', 49 | __COMMAND_BOT_SUMMON_DEMO_TEXT: 'What\'s up?', 50 | __COMMAND_BOT_TALK_LABEL: 'Talk', 51 | __COMMAND_BOT_TALK_TEXT: 'Talk', 52 | __COMMAND_BOT_TALK_ALIASES: [], 53 | __COMMAND_BOT_TALK_DEMO_LABEL: 'Talk', 54 | __COMMAND_BOT_TALK_DEMO_TEXT: 'Talk me about yourself', 55 | __COMMAND_SUM_ADVISE_LABEL: 'Advise', 56 | __COMMAND_SUM_ADVISE_TEXT: 'Advise', 57 | __COMMAND_SUM_ADVISE_PROMPT: 'Please summarize the following content briefly and advise appropriately.', 58 | __COMMAND_SUM_APOLOGIZE_LABEL: 'Apologize', 59 | __COMMAND_SUM_APOLOGIZE_TEXT: 'Apologize', 60 | __COMMAND_SUM_APOLOGIZE_PROMPT: 'Please summarize the following content briefly and apologize sincerely.', 61 | __COMMAND_SUM_BLAME_LABEL: 'Blame', 62 | __COMMAND_SUM_BLAME_TEXT: 'Blame', 63 | __COMMAND_SUM_BLAME_PROMPT: 'Please summarize the following content briefly and blame strongly.', 64 | __COMMAND_SUM_COMFORT_LABEL: 'Comfort', 65 | __COMMAND_SUM_COMFORT_TEXT: 'Comfort', 66 | __COMMAND_SUM_COMFORT_PROMPT: 'Please summarize the following content briefly and comfort warmly.', 67 | __COMMAND_SUM_COMPLAIN_LABEL: 'Complain', 68 | __COMMAND_SUM_COMPLAIN_TEXT: 'Complain', 69 | __COMMAND_SUM_COMPLAIN_PROMPT: 'Please summarize the following content briefly and complain gently.', 70 | __COMMAND_SUM_ENCOURAGE_LABEL: 'Encourage', 71 | __COMMAND_SUM_ENCOURAGE_TEXT: 'Encourage', 72 | __COMMAND_SUM_ENCOURAGE_PROMPT: 'Please summarize the following content briefly and encourage enthusiastically.', 73 | __COMMAND_SUM_LAUGH_LABEL: 'Laugh', 74 | __COMMAND_SUM_LAUGH_TEXT: 'Laugh', 75 | __COMMAND_SUM_LAUGH_PROMPT: 'Please summarize the following content briefly and laugh rudely.', 76 | __COMMAND_SUM_SUM_LABEL: 'Sum', 77 | __COMMAND_SUM_SUM_TEXT: 'Sum', 78 | __COMMAND_SUM_SUM_PROMPT: 'Please summarize the following content and provide some details.', 79 | __COMMAND_SYS_COMMAND_LABEL: 'Command', 80 | __COMMAND_SYS_COMMAND_TEXT: 'Command', 81 | __COMMAND_SYS_DOC_LABEL: 'Documentation', 82 | __COMMAND_SYS_DOC_TEXT: 'Documentation', 83 | __COMMAND_SYS_DEPLOY_LABEL: 'Restart', 84 | __COMMAND_SYS_DEPLOY_TEXT: 'Restart', 85 | __COMMAND_SYS_DEPLOY_REPLY: 'Restarting', 86 | __COMMAND_SYS_REPORT_LABEL: 'Report', 87 | __COMMAND_SYS_REPORT_TEXT: 'Report', 88 | __COMMAND_SYS_VERSION_LABEL: 'Version', 89 | __COMMAND_SYS_VERSION_TEXT: 'Version', 90 | __COMMAND_SYS_VERSION_REPLY: (version, isLatest) => `Your version is ${isLatest ? 'up-to-date' : version}.`, 91 | __COMMAND_TRANSLATE_TO_EN_LABEL: '翻成英文', // TODO 92 | __COMMAND_TRANSLATE_TO_EN_TEXT: '翻成英文', // TODO 93 | __COMMAND_TRANSLATE_TO_EN_PROMPT: '請將以下內容翻譯成英文。', // TODO 94 | __COMMAND_TRANSLATE_TO_JA_LABEL: '翻成日文', // TODO 95 | __COMMAND_TRANSLATE_TO_JA_TEXT: '翻成日文', // TODO 96 | __COMMAND_TRANSLATE_TO_JA_PROMPT: '請將以下內容翻譯成日文。', // TODO 97 | __COMPLETION_DEFAULT_SYSTEM_PROMPT: '', // TODO 98 | __COMPLETION_DEFAULT_HUMAN_PROMPT: (name) => (name ? `I am ${name}` : 'Hello'), 99 | __COMPLETION_DEFAULT_AI_PROMPT: (name) => (name ? `I am ${name}` : 'Hello'), 100 | __COMPLETION_DEFAULT_AI_TONE: (tone) => (tone ? `以${tone}的語氣回應我:` : ''), // TODO 101 | __COMPLETION_SEARCH: (a, q) => `根據「${a}」查詢結果,回答「${q}」問題`, // TODO 102 | __COMPLETION_SEARCH_NOT_FOUND: '查無資料', // TODO 103 | __COMPLETION_QUOTATION_MARK_OPENING: '"', 104 | __COMPLETION_QUOTATION_MARK_CLOSING: '"', 105 | __COMPLETION_VISION: 'What\'s in this image?', 106 | __ERROR_ECONNABORTED: 'Timed out', 107 | __ERROR_UNKNOWN: 'Something went wrong', 108 | __ERROR_MAX_GROUPS_REACHED: 'Maximum groups reached', 109 | __ERROR_MAX_USERS_REACHED: 'Maximum users reached', 110 | __ERROR_MISSING_ENV: (v) => `Missing environment variable: ${v}`, 111 | __MESSAGE_NEW_VERSION_AVAILABLE: (version) => `A new version ${version} is now available!`, 112 | __SOURCE_NAME_SOME_GROUP: 'Someone Group', 113 | __SOURCE_NAME_SOMEONE: 'Someone', 114 | }; 115 | 116 | export default en; 117 | -------------------------------------------------------------------------------- /app/context.js: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios'; 2 | import fs from 'fs'; 3 | import config from '../config/index.js'; 4 | import { t } from '../locales/index.js'; 5 | import { 6 | MESSAGE_TYPE_IMAGE, MESSAGE_TYPE_TEXT, SOURCE_TYPE_GROUP, SOURCE_TYPE_USER, 7 | } from '../services/line.js'; 8 | import { 9 | addMark, 10 | convertText, 11 | fetchAudio, 12 | fetchImage, 13 | fetchGroup, 14 | fetchUser, 15 | generateTranscription, 16 | } from '../utils/index.js'; 17 | import { Command, COMMAND_BOT_FORGET, COMMAND_BOT_RETRY } from './commands/index.js'; 18 | import { updateHistory } from './history/index.js'; 19 | import { 20 | ImageMessage, Message, TemplateMessage, TextMessage, 21 | } from './messages/index.js'; 22 | import { Bot, Event, Source } from './models/index.js'; 23 | import { getSources, setSources } from './repository/index.js'; 24 | 25 | class Context { 26 | /** 27 | * @type {Event} 28 | */ 29 | event; 30 | 31 | /** 32 | * @type {Source} 33 | */ 34 | source; 35 | 36 | /** 37 | * @type {string} 38 | */ 39 | transcription; 40 | 41 | /** 42 | * @type {Array} 43 | */ 44 | messages = []; 45 | 46 | /** 47 | * @param {Event} event 48 | */ 49 | constructor(event) { 50 | this.event = event; 51 | } 52 | 53 | get id() { 54 | if (this.event.isGroup) return this.event.source.groupId; 55 | return this.event.source.userId; 56 | } 57 | 58 | /** 59 | * @returns {string} 60 | */ 61 | get replyToken() { 62 | return this.event.replyToken; 63 | } 64 | 65 | /** 66 | * @returns {string} 67 | */ 68 | get groupId() { 69 | return this.event.groupId; 70 | } 71 | 72 | /** 73 | * @returns {string} 74 | */ 75 | get userId() { 76 | return this.event.userId; 77 | } 78 | 79 | /** 80 | * @returns {string} 81 | */ 82 | get trimmedText() { 83 | if (this.event.isText) { 84 | const text = this.event.text.replaceAll(' ', ' ').replace(config.BOT_NAME, '').trim(); 85 | return addMark(text); 86 | } 87 | if (this.event.isAudio) { 88 | const text = this.transcription.replace(config.BOT_NAME, '').trim(); 89 | return addMark(text); 90 | } 91 | if (this.event.isImage) { 92 | return this.transcription.trim(); 93 | } 94 | return '?'; 95 | } 96 | 97 | get hasBotName() { 98 | if (this.event.isText) { 99 | const text = this.event.text.replaceAll(' ', ' ').trim().toLowerCase(); 100 | return text.startsWith(config.BOT_NAME.toLowerCase()); 101 | } 102 | if (this.event.isAudio) { 103 | const text = this.transcription.toLowerCase(); 104 | return text.startsWith(config.BOT_NAME.toLowerCase()); 105 | } 106 | if (this.event.isImage) { 107 | const text = this.transcription.toLowerCase(); 108 | return text.startsWith(config.BOT_NAME.toLowerCase()); 109 | } 110 | return false; 111 | } 112 | 113 | async initialize() { 114 | try { 115 | this.validate(); 116 | await this.register(); 117 | } catch (err) { 118 | return this.pushError(err); 119 | } 120 | if (this.event.isAudio) { 121 | try { 122 | await this.transcribeAudio(); 123 | } catch (err) { 124 | return this.pushError(err); 125 | } 126 | } 127 | if (this.event.isImage) { 128 | try { 129 | await this.transcribeImage(); 130 | } catch (err) { 131 | return this.pushError(err); 132 | } 133 | } 134 | updateHistory(this.id, (history) => history.write(this.source.name, this.trimmedText)); 135 | return this; 136 | } 137 | 138 | /** 139 | * @throws {Error} 140 | */ 141 | validate() { 142 | const sources = getSources(); 143 | const groups = Object.values(sources).filter(({ type }) => type === SOURCE_TYPE_GROUP); 144 | const users = Object.values(sources).filter(({ type }) => type === SOURCE_TYPE_USER); 145 | if (this.event.isGroup && !sources[this.groupId] && groups.length >= config.APP_MAX_GROUPS) { 146 | throw new Error(t('__ERROR_MAX_GROUPS_REACHED')); 147 | } 148 | if (!sources[this.userId] && users.length >= config.APP_MAX_USERS) { 149 | throw new Error(t('__ERROR_MAX_USERS_REACHED')); 150 | } 151 | } 152 | 153 | async register() { 154 | const sources = getSources(); 155 | const newSources = {}; 156 | if (this.event.isGroup && !sources[this.groupId]) { 157 | const { groupName } = await fetchGroup(this.groupId); 158 | newSources[this.groupId] = new Source({ 159 | type: SOURCE_TYPE_GROUP, 160 | name: groupName, 161 | bot: new Bot({ 162 | isActivated: !config.BOT_DEACTIVATED, 163 | }), 164 | }); 165 | } 166 | if (!sources[this.userId]) { 167 | const { displayName } = await fetchUser(this.userId); 168 | newSources[this.userId] = new Source({ 169 | type: SOURCE_TYPE_USER, 170 | name: displayName, 171 | bot: new Bot({ 172 | isActivated: !config.BOT_DEACTIVATED, 173 | }), 174 | }); 175 | } 176 | Object.assign(sources, newSources); 177 | if (Object.keys(newSources).length > 0) await setSources(sources); 178 | this.source = new Source(sources[this.id]); 179 | } 180 | 181 | async transcribeAudio() { 182 | const buffer = await fetchAudio(this.event.messageId); 183 | const file = `/tmp/${this.event.messageId}.m4a`; 184 | fs.writeFileSync(file, buffer); 185 | const { text } = await generateTranscription({ file, buffer }); 186 | this.transcription = convertText(text); 187 | } 188 | 189 | async transcribeImage() { 190 | const base64String = await fetchImage(this.event.messageId); 191 | this.transcription = base64String; 192 | } 193 | 194 | /** 195 | * @param {Object} param 196 | * @param {string} param.text 197 | * @param {Array} param.aliases 198 | * @returns {boolean} 199 | */ 200 | hasCommand({ 201 | text, 202 | aliases, 203 | }) { 204 | const content = this.trimmedText.toLowerCase(); 205 | if (aliases.some((alias) => content.startsWith(alias.toLowerCase()))) return true; 206 | if (content.startsWith(text.toLowerCase())) return true; 207 | return false; 208 | } 209 | 210 | /** 211 | * @param {string} text 212 | * @param {Array} actions 213 | * @returns {Context} 214 | */ 215 | pushText(text, actions = []) { 216 | if (!text) return this; 217 | const message = new TextMessage({ 218 | type: MESSAGE_TYPE_TEXT, 219 | text: convertText(text), 220 | }); 221 | message.setQuickReply(actions); 222 | this.messages.push(message); 223 | return this; 224 | } 225 | 226 | /** 227 | * @param {string} url 228 | * @param {Array} actions 229 | * @returns {Context} 230 | */ 231 | pushImage(url, actions = []) { 232 | if (!url) return this; 233 | const message = new ImageMessage({ 234 | type: MESSAGE_TYPE_IMAGE, 235 | originalContentUrl: url, 236 | previewImageUrl: url, 237 | }); 238 | message.setQuickReply(actions); 239 | this.messages.push(message); 240 | return this; 241 | } 242 | 243 | /** 244 | * @param {string} url 245 | * @param {Array} buttons 246 | * @param {Array} actions 247 | * @returns {Context} 248 | */ 249 | pushTemplate(text, buttons = [], actions = []) { 250 | if (!text) return this; 251 | const message = new TemplateMessage({ 252 | text, 253 | actions: buttons, 254 | }); 255 | message.setQuickReply(actions); 256 | this.messages.push(message); 257 | return this; 258 | } 259 | 260 | /** 261 | * @param {AxiosError} err 262 | * @returns {Context} 263 | */ 264 | pushError(err) { 265 | this.error = err; 266 | console.log(this.error.message); 267 | if (err.code === 'ECONNABORTED') { 268 | if (config.ERROR_MESSAGE_DISABLED) return this; 269 | return this.pushText(t('__ERROR_ECONNABORTED'), [COMMAND_BOT_RETRY, COMMAND_BOT_FORGET]); 270 | } 271 | if (err.response?.status >= 500) { 272 | if (config.ERROR_MESSAGE_DISABLED) return this; 273 | return this.pushText(t('__ERROR_UNKNOWN'), [COMMAND_BOT_RETRY, COMMAND_BOT_FORGET]); 274 | } 275 | if (err.config?.baseURL) this.pushText(`${err.config.method.toUpperCase()} ${err.config.baseURL}${err.config.url}`); 276 | if (err.response) this.pushText(`Request failed with status code ${err.response.status}`); 277 | this.pushText(err.message); 278 | return this; 279 | } 280 | } 281 | 282 | export default Context; 283 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.9.1 (2024-07-10) 4 | 5 | ### Bug Fixes 6 | 7 | - Update `talk` command 8 | 9 | ## 4.9.0 (2024-07-10) 10 | 11 | ### New Features 12 | 13 | - Support `gpt-4o` model 14 | 15 | ## 4.8.4 (2024-07-06) 16 | 17 | ### Bug Fixes 18 | 19 | - Update status page 20 | 21 | ## 4.8.3 (2024-02-03) 22 | 23 | ### Bug Fixes 24 | 25 | - Fix `maxDuration` for `vercel.json` 26 | 27 | ## 4.8.2 (2024-02-03) 28 | 29 | ### Bug Fixes 30 | 31 | - Use `gl` param for SerpApi 32 | - Remove `SERPAPI_LANG` environment variable 33 | 34 | ## 4.8.1 (2024-02-03) 35 | 36 | ### Bug Fixes 37 | 38 | - Add `maxDuration` for `vercel.json` 39 | 40 | ## 4.8.0 (2023-12-07) 41 | 42 | ### New Features 43 | 44 | - Support fine-tuned models 45 | 46 | ## 4.7.6 (2023-11-18) 47 | 48 | ### Bug Fixes 49 | 50 | - Change default max groups to 1000 51 | - Change default max users to 1000 52 | - Change default max prompt messages to 4 53 | - Change default max prompt tokens to 160 54 | - Change default completion temperature to 1 55 | - Change default completion max tokens to 64 56 | 57 | ## 4.7.5 (2023-10-01) 58 | 59 | ### Bug Fixes 60 | 61 | - Update status page 62 | 63 | ## 4.7.4 (2023-08-26) 64 | 65 | ### Bug Fixes 66 | 67 | - Update status page 68 | 69 | ## 4.7.3 (2023-08-25) 70 | 71 | ### Bug Fixes 72 | 73 | - Fix commands 74 | 75 | ## 4.7.2 (2023-08-05) 76 | 77 | ### Bug Fixes 78 | 79 | - Fix `translate` command 80 | 81 | ## 4.7.1 (2023-08-01) 82 | 83 | ### Bug Fixes 84 | 85 | - Optimize `search` command 86 | - Add aliases for commands 87 | 88 | ## 4.7.0 (2023-06-08) 89 | 90 | ### New Features 91 | 92 | - Add `OPENAI_COMPLETION_STOP_SEQUENCES` environment variable 93 | 94 | ## 4.6.0 (2023-05-03) 95 | 96 | ### New Features 97 | 98 | - Support `gpt-4` model 99 | 100 | ## 4.5.0 (2023-04-27) 101 | 102 | ### New Features 103 | 104 | - Support `zh_CN` locale 105 | 106 | ## 4.4.4 (2023-03-21) 107 | 108 | ### Bug Fixes 109 | 110 | - Fix default value of `APP_MAX_GROUPS` environment variable 111 | - Fix default value of `APP_MAX_USERS` environment variable 112 | 113 | ## 4.4.3 (2023-03-11) 114 | 115 | ### Bug Fixes 116 | 117 | - Fix wording of `doc` and `report` commands 118 | 119 | ## 4.4.2 (2023-03-11) 120 | 121 | ### Bug Fixes 122 | 123 | - Add `ERROR_MESSAGE_DISABLED` environment variable 124 | - Deprecate `ERROR_TIMEOUT_DISABLED` environment variable 125 | 126 | ## 4.4.1 (2023-03-10) 127 | 128 | ### Bug Fixes 129 | 130 | - Add default max prompt tokens for chat completion api 131 | 132 | ## 4.4.0 (2023-03-08) 133 | 134 | ### New Features 135 | 136 | - Support snapshots of `gpt-3.5-turbo` model 137 | 138 | ## 4.3.0 (2023-03-08) 139 | 140 | ### New Features 141 | 142 | - Add `VERCEL_TEAM_ID` environment variable 143 | 144 | ## 4.2.2 (2023-03-08) 145 | 146 | ### Bug Fixes 147 | 148 | - Optimize error handling 149 | 150 | ## 4.2.1 (2023-03-07) 151 | 152 | ### Bug Fixes 153 | 154 | - Fix `add-mark` util 155 | 156 | ## 4.2.0 (2023-03-05) 157 | 158 | ### New Features 159 | 160 | - Add `APP_INIT_PROMPT` environment variable 161 | 162 | ## 4.1.3 (2023-03-05) 163 | 164 | ### Bug Fixes 165 | 166 | - Fix `add-mark` util 167 | 168 | ## 4.1.2 (2023-03-05) 169 | 170 | ### Bug Fixes 171 | 172 | - Update `add-mark` util 173 | 174 | ## 4.1.1 (2023-03-05) 175 | 176 | ### Bug Fixes 177 | 178 | - End text with dot 179 | 180 | ## 4.1.0 (2023-03-05) 181 | 182 | - Support `whisper-1` model 183 | - Add `opencc` text converter 184 | - Store display name and group name to storage 185 | 186 | ## 4.0.4 (2023-03-03) 187 | 188 | ### Bug Fixes 189 | 190 | - Optimize `search` command 191 | 192 | ## 4.0.3 (2023-03-03) 193 | 194 | ### Bug Fixes 195 | 196 | - Optimize `search` and `draw` commands 197 | 198 | ## 4.0.2 (2023-03-02) 199 | 200 | ### Bug Fixes 201 | 202 | - Fix prompt messages 203 | 204 | ## 4.0.1 (2023-03-02) 205 | 206 | ### Bug Fixes 207 | 208 | - Fix `enquire` command 209 | 210 | ## 4.0.0 (2023-03-02) 211 | 212 | ### New Features 213 | 214 | - Support `gpt-3.5-turbo` model 215 | 216 | ### Bug Fixes 217 | 218 | - Rename `APP_MAX_PROMPT_SENTENCES` environment variable to `APP_MAX_PROMPT_MESSAGES` 219 | 220 | ## 3.7.0 (2023-02-26) 221 | 222 | ### New Features 223 | 224 | - Add demo for `search` command 225 | - Add `SERPAPI_LOCATION` environment variable 226 | - Add `SERPAPI_LANG` environment variable 227 | 228 | ## 3.6.0 (2023-02-26) 229 | 230 | ### New Features 231 | 232 | - Add `APP_API_TIMEOUT` environment variable 233 | - Add `APP_MAX_PROMPT_SENTENCES` environment variable 234 | - Add `APP_MAX_PROMPT_TOKENS` environment variable 235 | 236 | ## 3.5.0 (2023-02-26) 237 | 238 | ### New Features 239 | 240 | - Rename `HUMAN_BACKGROUND` environment variable to `HUMAN_INIT_PROMPT` 241 | - Rename `BOT_BACKGROUND` environment variable to `BOT_INIT_PROMPT` 242 | 243 | ## 3.4.1 (2023-02-25) 244 | 245 | ### Bug Fixes 246 | 247 | - Fix default bot name 248 | 249 | ## 3.4.0 (2023-02-24) 250 | 251 | ### New Features 252 | 253 | - Add `info` endpoint 254 | 255 | ## 3.3.5 (2023-02-24) 256 | 257 | ### Bug Fixes 258 | 259 | - Fix prompt wording 260 | 261 | ## 3.3.4 (2023-02-24) 262 | 263 | ### Bug Fixes 264 | 265 | - Fix prompt wording 266 | 267 | ## 3.3.3 (2023-02-24) 268 | 269 | ### Bug Fixes 270 | 271 | - Fix tests 272 | 273 | ## 3.3.2 (2023-02-23) 274 | 275 | ### Bug Fixes 276 | 277 | - Fix prompt wording 278 | 279 | ## 3.3.1 (2023-02-23) 280 | 281 | ### Bug Fixes 282 | 283 | - Fix prompt wording 284 | 285 | ## 3.3.0 (2023-02-23) 286 | 287 | ### New Features 288 | 289 | - Add `BOT_TONE` environment variable 290 | 291 | ## 3.2.1 (2023-02-22) 292 | 293 | ### Bug Fixes 294 | 295 | - Fix timeout wording 296 | 297 | ## 3.2.0 (2023-02-22) 298 | 299 | ### New Features 300 | 301 | - Add `HUMAN_NAME` environment variable 302 | - Add `HUMAN_BACKGROUND` environment variable 303 | - Add `BOT_BACKGROUND` environment variable 304 | 305 | ## 3.1.0 (2023-02-21) 306 | 307 | ### New Features 308 | 309 | - Implement `forget` command 310 | 311 | ## 3.0.0 (2023-02-18) 312 | 313 | ### New Features 314 | 315 | - Implement `search` command 316 | 317 | ## 2.5.1 (2023-02-18) 318 | 319 | ### New Features 320 | 321 | - Rename `BOT_TIMEOUT_DISABLED` environment variable to `ERROR_TIMEOUT_DISABLED` 322 | 323 | ## 2.5.0 (2023-02-18) 324 | 325 | ### New Features 326 | 327 | - Add `BOT_TIMEOUT_DISABLED` environment variable 328 | 329 | ## 2.4.0 (2023-02-17) 330 | 331 | ### New Features 332 | 333 | - Add `BOT_DEACTIVATED` environment variable 334 | 335 | ## 2.3.0 (2023-02-11) 336 | 337 | ### New Features 338 | 339 | - Add `VERCEL_TIMEOUT` environment variable 340 | - Add `OPENAI_TIMEOUT` environment variable 341 | - Add `LINE_TIMEOUT` environment variable 342 | 343 | ## 2.2.0 (2023-02-04) 344 | 345 | ### New Features 346 | 347 | - Implement `retry` command 348 | 349 | ## 2.1.4 (2023-01-15) 350 | 351 | ### Bug Fixes 352 | 353 | - Ignore non-text message events 354 | 355 | ## 2.1.3 (2023-01-15) 356 | 357 | ### Bug Fixes 358 | 359 | - Add command aliases 360 | 361 | ## 2.1.2 (2023-01-15) 362 | 363 | ### Bug Fixes 364 | 365 | - Add command aliases 366 | 367 | ## 2.1.1 (2023-01-14) 368 | 369 | ### Bug Fixes 370 | 371 | - Fix `enquire` command 372 | 373 | ## 2.1.0 (2023-01-11) 374 | 375 | ### New Features 376 | 377 | - Add `VERCEL_PROJECT_NAME` environment variable 378 | 379 | ## 2.0.1 (2023-01-11) 380 | 381 | ### Bug Fixes 382 | 383 | - Add logs for webhook endpoint 384 | 385 | ## 2.0.0 (2023-01-10) 386 | 387 | ### New Features 388 | 389 | - Implement `sum` command 390 | - Implement `analyze` command 391 | - Implement `translate` command 392 | - Add `BOT_NAME` environment variable 393 | - Add `APP_MAX_GROUPS` environment variable 394 | - Add `APP_MAX_USERS` environment variable 395 | 396 | ### Bug Fixes 397 | 398 | - Remove `SETTING_AI_NAME` environment variable 399 | - Remove `SETTING_AI_ACTIVATED` environment variable 400 | - Refactor `storage` module 401 | - Refactor `prompt` module 402 | - Refactor `history` module 403 | 404 | ## 1.12.4 (2022-12-31) 405 | 406 | ### Bug Fixes 407 | 408 | - Rename `chat` command to `talk` 409 | 410 | ## 1.12.3 (2022-12-31) 411 | 412 | ### Bug Fixes 413 | 414 | - Update command template 415 | 416 | ## 1.12.2 (2022-12-30) 417 | 418 | ### Bug Fixes 419 | 420 | - Fix summarize request wording 421 | 422 | ## 1.12.1 (2022-12-30) 423 | 424 | ### Bug Fixes 425 | 426 | - Handle non-text messages 427 | 428 | ## 1.12.0 (2022-12-30) 429 | 430 | ### New Features 431 | 432 | - Implement `summarize` command 433 | 434 | ## 1.11.3 (2022-12-29) 435 | 436 | ### Bug Fixes 437 | 438 | - Add command aliases 439 | 440 | ## 1.11.2 (2022-12-26) 441 | 442 | ### Bug Fixes 443 | 444 | - Handle error messages in every commands 445 | 446 | ## 1.11.1 (2022-12-26) 447 | 448 | ### Bug Fixes 449 | 450 | - Trim AI Name when sending prompt 451 | 452 | ## 1.11.0 (2022-12-26) 453 | 454 | ### New Features 455 | 456 | - Implement `call` command 457 | - Add `SETTING_AI_NAME` environment variable 458 | - Add `SETTING_AI_ACTIVATED` environment variable 459 | 460 | ### Bug Fixes 461 | 462 | - Remove `APP_STORAGE` environment variable 463 | 464 | ## 1.10.2 (2022-12-25) 465 | 466 | ### Bug Fixes 467 | 468 | - Rename methods 469 | 470 | ## 1.10.1 (2022-12-25) 471 | 472 | ### Bug Fixes 473 | 474 | - Fix wording of commands 475 | 476 | ## 1.10.0 (2022-12-25) 477 | 478 | ### New Features 479 | 480 | - Add `OPENAI_IMAGE_GENERATION_SIZE` environment variable 481 | 482 | ### Bug Fixes 483 | 484 | - Remove `SETTING_IMAGE_GENERATION_SIZE` setting 485 | 486 | ## 1.9.1 (2022-12-24) 487 | 488 | ### Bug Fixes 489 | 490 | - Rename functions and variables 491 | 492 | ## 1.9.0 (2022-12-24) 493 | 494 | ### New Features 495 | 496 | - Implement dynamic configuration 497 | - Implement `configure` command 498 | - Add `SETTING_IMAGE_GENERATION_SIZE` setting 499 | 500 | ## 1.8.0 (2022-12-24) 501 | 502 | ### New Features 503 | 504 | - Implement `doc` command 505 | 506 | ### Bug Fixes 507 | 508 | - Rename `settings` command to `command` 509 | 510 | ## 1.7.1 (2022-12-23) 511 | 512 | ### Bug Fixes 513 | 514 | - Fix wording of commands 515 | 516 | ## 1.7.0 (2022-12-23) 517 | 518 | ### New Features 519 | 520 | - Implement localization 521 | - Implement command aliases 522 | 523 | ### Bug Fixes 524 | 525 | - Rename `OPENAI_COMPLETION_INIT_LANG` environment variable to `APP_LANG` 526 | 527 | ## 1.6.0 (2022-12-23) 528 | 529 | ### New Features 530 | 531 | - Implement `settings` command 532 | 533 | ### Bug Fixes 534 | 535 | - Rename `chat --auto-reply off` command to `deactivate` 536 | - Rename `chat --auto-reply on` command to `activate` 537 | - Rename `CHAT_AUTO_REPLY` setting to `AI_ACTIVATED` 538 | 539 | ## 1.5.0 (2022-12-22) 540 | 541 | ### New Features 542 | 543 | - Implement `continue` command with quick reply feature 544 | 545 | ### Bug Fixes 546 | 547 | - Change default max completion tokens to 160 548 | - Change default max prompt messages to 16 549 | 550 | ## 1.4.6 (2022-12-20) 551 | 552 | ### Bug Fixes 553 | 554 | - Add comments 555 | 556 | ## 1.4.5 (2022-12-19) 557 | 558 | ### Bug Fixes 559 | 560 | - Add `ja` initial language 561 | - Add `ai` alias for `chat` command 562 | 563 | ## 1.4.4 (2022-12-18) 564 | 565 | ### Bug Fixes 566 | 567 | - Rename `AI_AUTO_REPLY` setting to `CHAT_AUTO_REPLY` 568 | - Fix case sensitivity of command issues 569 | 570 | ## 1.4.3 (2022-12-18) 571 | 572 | ### Bug Fixes 573 | 574 | - Rename `ai` command to `chat` 575 | - Rename `ai --auto-reply off` command to `chat --auto-reply off` 576 | - Rename `ai --auto-reply on` command to `chat --auto-reply on` 577 | - Rename `image` command to `draw` 578 | 579 | ## 1.4.2 (2022-12-18) 580 | 581 | ### Bug Fixes 582 | 583 | - Refactor commands 584 | 585 | ## 1.4.1 (2022-12-18) 586 | 587 | ### Bug Fixes 588 | 589 | - Refactor tests 590 | 591 | ## 1.4.0 (2022-12-18) 592 | 593 | ### New Features 594 | 595 | - Implement `image` command 596 | 597 | ## 1.3.1 (2022-12-18) 598 | 599 | ### Bug Fixes 600 | 601 | - Rename `VERCEL_WEBHOOK_URL` environment variable to `VERCEL_DEPLOY_HOOK_URL` 602 | 603 | ## 1.3.0 (2022-12-18) 604 | 605 | ### New Features 606 | 607 | - Implement custom webhook path 608 | - Add `APP_WEBHOOK_PATH` environment variable 609 | 610 | ## 1.2.1 (2022-12-18) 611 | 612 | ### Bug Fixes 613 | 614 | - Refactor main functions 615 | 616 | ## 1.2.0 (2022-12-17) 617 | 618 | ### New Features 619 | 620 | - Implement `deploy` command 621 | - Add `VERCEL_WEBHOOK_URL` environment variable 622 | 623 | ## 1.1.3 (2022-12-17) 624 | 625 | ### Bug Fixes 626 | 627 | - Fix storage module 628 | - Fix `ai --auto-reply off` command 629 | - Fix `ai --auto-reply on` command 630 | 631 | ## 1.1.2 (2022-12-16) 632 | 633 | ### Bug Fixes 634 | 635 | - Refactor utility functions 636 | 637 | ## 1.1.1 (2022-12-16) 638 | 639 | ### Bug Fixes 640 | 641 | - Rename `VERCEL_API_KEY` environment variable to `VERCEL_ACCESS_TOKEN` 642 | - Rename `LINE_API_KEY` environment variable to `LINE_CHANNEL_ACCESS_TOKEN` 643 | - Rename `LINE_API_SECRET` environment variable to `LINE_CHANNEL_SECRET` 644 | 645 | ## 1.1.0 (2022-12-16) 646 | 647 | ### New Features 648 | 649 | - Implement `version` command 650 | - Implement `ai` command 651 | - Implement `ai --auto-reply off` command 652 | - Implement `ai --auto-reply on` command 653 | - Add Vercel API module 654 | - Add `VERCEL_API_KEY` environment variable 655 | - Add `LINE_API_SECRET` environment variable 656 | 657 | ### Bug Fixes 658 | 659 | - Fix timeout issues 660 | 661 | ## 1.0.0 (2022-12-11) 662 | 663 | ### New Features 664 | 665 | - Implement chat feature 666 | - Add OpenAI API module 667 | - Add LINE API module 668 | --------------------------------------------------------------------------------