From 371c083d6ae08daf4065270063e447508dc7f42b Mon Sep 17 00:00:00 2001 From: tetsuya-k <64536338+tetsuya-ki@users.noreply.github.com> Date: Wed, 25 Oct 2023 18:56:28 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=A0?= =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=83=A2=E3=82=B8=E3=83=A5=E3=83=BC=E3=83=AB=E3=81=AE=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.ts | 2 + src/index.ts | 2 + src/modules/check-custom-emojis/index.ts | 146 +++++++++++++++++++++++ src/serifs.ts | 5 + 4 files changed, 155 insertions(+) create mode 100644 src/modules/check-custom-emojis/index.ts diff --git a/src/config.ts b/src/config.ts index 1918b4f..a404782 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,6 @@ type Config = { host: string; + serverName: string; i: string; master?: string; wsUrl: string; @@ -9,6 +10,7 @@ type Config = { notingEnabled: boolean; chartEnabled: boolean; serverMonitoring: boolean; + checkEmojisEnabled: boolean; mecab?: string; mecabDic?: string; memoryDir?: string; diff --git a/src/index.ts b/src/index.ts index dca5703..c6c2fa8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,6 +34,7 @@ import SleepReportModule from './modules/sleep-report'; import NotingModule from './modules/noting'; import PollModule from './modules/poll'; import ReminderModule from './modules/reminder'; +import CheckCustomEmojisModule from './modules/check-custom-emojis'; console.log(' __ ____ _____ ___ '); console.log(' /__\\ (_ _)( _ )/ __)'); @@ -88,6 +89,7 @@ promiseRetry(retry => { new NotingModule(), new PollModule(), new ReminderModule(), + new CheckCustomEmojisModule(), ]); }).catch(e => { log(chalk.red('Failed to fetch the account')); diff --git a/src/modules/check-custom-emojis/index.ts b/src/modules/check-custom-emojis/index.ts new file mode 100644 index 0000000..70653be --- /dev/null +++ b/src/modules/check-custom-emojis/index.ts @@ -0,0 +1,146 @@ +import autobind from 'autobind-decorator'; +import * as loki from 'lokijs'; +import Module from '@/module'; +import serifs from '@/serifs'; +import config from '@/config'; +import Message from '@/message'; + +export default class extends Module { + public readonly name = 'checkCustomEmojis'; + + private lastEmoji: loki.Collection<{ + id: string; + updatedAt: number; + }>; + + @autobind + public install() { + if (config.checkEmojisEnabled === false) return {}; + this.lastEmoji = this.ai.getCollection('lastEmoji', { + indices: ['id'] + }); + + this.timeCheck(); + setInterval(this.timeCheck, 1000 * 60 * 3); + + return { + mentionHook: this.mentionHook + }; + } + + @autobind + private timeCheck() { + const now = new Date(); + if (now.getHours() !== 23) return; + const date = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`; + const data = this.getData(); + if (data.lastPosted == date) return; + data.lastPosted = date; + this.setData(data); + + this.log('Time to check custom emojis!'); + this.post(); + } + + @autobind + private async post() { + this.log('Start to check custom emojis.'); + const lastEmoji = this.lastEmoji.find({}); + // this.log('lastEmoji'); + // this.log(JSON.stringify(lastEmoji,null,'\t')); + + const lastId = lastEmoji.length != 0 ? lastEmoji[0].id : null; + const emojisData = await this.checkCumstomEmojis(lastId); + if (emojisData.length == 0) return; + + // 絵文字データが取得された場合、元々のデータを削除しておく + const emojiSize = emojisData.length; + this.lastEmoji.remove(lastEmoji); + + // 概要について投稿 + const server_name = config.serverName ? config.serverName : 'このサーバー'; + this.log('Posting...'); + this.log(serifs.checkCustomEmojis.post(server_name, emojiSize)); + await this.ai.post({ + text: serifs.checkCustomEmojis.post(server_name, emojiSize) + }); + + // 各絵文字について投稿 + for (const emoji of emojisData){ + await this.ai.post({ + text: serifs.checkCustomEmojis.emojiPost(emoji.name) + }); + this.log(serifs.checkCustomEmojis.emojiPost(emoji.name)); + } + + // データの保存 + this.log('Last custom emoji data saving...'); + this.log(JSON.stringify(emojisData[emojiSize-1],null,'\t')); + this.lastEmoji.insertOne({ + id: emojisData[emojiSize-1].id, + updatedAt: Date.now() + }); + this.log('Check custom emojis finished!'); + } + + @autobind + private async checkCumstomEmojis(lastId : any) { + this.log('CustomEmojis fetching...'); + let emojisData; + if(lastId != null){ + this.log('lastId is **not** null'); + emojisData = await this.ai.api('admin/emoji/list', { + sinceId: lastId, + limit: 30 + }); + } else { + this.log('lastId is null'); + emojisData = await this.ai.api('admin/emoji/list', { + limit: 100 + }); + + // 最後まで取得 + let beforeEmoji = null; + let afterEmoji = emojisData.length > 1 ? emojisData[0] : null; + while(emojisData.length == 100 && beforeEmoji != afterEmoji){ + const lastId = emojisData[emojisData.length-1].id; + // sinceIdを指定して再度取り直す + emojisData = await this.ai.api('admin/emoji/list', { + limit: 100, + sinceId: lastId + }); + beforeEmoji = afterEmoji; + afterEmoji = emojisData.length > 1 ? emojisData[0] : null; + await this.sleep(50); + } + + // sinceIdが未指定の場合、末尾から5件程度にしておく + let newJson: any[] = []; + for (let i = emojisData.length - 5; i < emojisData.length; i++) { + newJson.push(emojisData[i]); + } + emojisData = newJson; + } + return emojisData; + } + + @autobind + private async mentionHook(msg: Message) { + if (!msg.includes(['カスタムえもじチェック'])) { + return false; + } else { + this.log('Check custom emojis requested'); + } + + await this.post(); + + return { + reaction: 'like' + }; + } + + @autobind + private async sleep(ms: number) { + return new Promise((res) => setTimeout(res, ms)); + } +} diff --git a/src/serifs.ts b/src/serifs.ts index 15ae16e..32515d5 100644 --- a/src/serifs.ts +++ b/src/serifs.ts @@ -381,6 +381,11 @@ export default { foryou: '描きました!' }, + checkCustomEmojis: { + post: (server_name, num) => `${server_name}に${num}件の絵文字が追加されました!`, + emojiPost: emoji => `:${emoji}:\n(\`${emoji}\`) #AddCustomEmojis` + }, + sleepReport: { report: hours => `んぅ、${hours}時間くらい寝ちゃってたみたいです`, reportUtatane: 'ん... うたた寝しちゃってました', From 1ebf2c7d372fcd74d5798003b371293047fb67fd Mon Sep 17 00:00:00 2001 From: tetsuya-k <64536338+tetsuya-ki@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:18:11 +0900 Subject: [PATCH 2/9] =?UTF-8?q?=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=A0?= =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=83=A2=E3=82=B8=E3=83=A5=E3=83=BC=E3=83=AB=E3=81=AE=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.mdやtorisetu.mdに記載 - カスタム絵文字投稿をまとめる設定を追加(checkEmojisAtOnce) --- README.md | 4 ++ src/config.ts | 5 ++- src/modules/check-custom-emojis/index.ts | 54 +++++++++++++++--------- src/serifs.ts | 4 +- torisetu.md | 3 ++ 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 87dcf11..042e75e 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Misskey用の日本語Botです。 "chartEnabled": "チャート機能を無効化する場合は false を入れてください", "reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false)", "serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)", + "checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)", + "checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)", "mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab)", "mecabDic": "MeCab の辞書ファイルパス (オプション)", "memoryDir": "memory.jsonの保存先(オプション、デフォルトは'.'(レポジトリのルートです))" @@ -40,6 +42,8 @@ Misskey用の日本語Botです。 "chartEnabled": "チャート機能を無効化する場合は false を入れてください", "reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false)", "serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)", + "checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)", + "checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)", "mecab": "/usr/bin/mecab", "mecabDic": "/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/", "memoryDir": "data" diff --git a/src/config.ts b/src/config.ts index a404782..59154a3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ type Config = { host: string; - serverName: string; + serverName?: string; i: string; master?: string; wsUrl: string; @@ -10,7 +10,8 @@ type Config = { notingEnabled: boolean; chartEnabled: boolean; serverMonitoring: boolean; - checkEmojisEnabled: boolean; + checkEmojisEnabled?: boolean; + checkEmojisAtOnce?: boolean; mecab?: string; mecabDic?: string; memoryDir?: string; diff --git a/src/modules/check-custom-emojis/index.ts b/src/modules/check-custom-emojis/index.ts index 70653be..d164c59 100644 --- a/src/modules/check-custom-emojis/index.ts +++ b/src/modules/check-custom-emojis/index.ts @@ -15,7 +15,7 @@ export default class extends Module { @autobind public install() { - if (config.checkEmojisEnabled === false) return {}; + if (!config.checkEmojisEnabled) return {}; this.lastEmoji = this.ai.getCollection('lastEmoji', { indices: ['id'] }); @@ -38,49 +38,65 @@ export default class extends Module { data.lastPosted = date; this.setData(data); - this.log('Time to check custom emojis!'); + this.log('Time to Check CustomEmojis!'); this.post(); } @autobind private async post() { - this.log('Start to check custom emojis.'); + this.log('Start to Check CustomEmojis.'); const lastEmoji = this.lastEmoji.find({}); - // this.log('lastEmoji'); - // this.log(JSON.stringify(lastEmoji,null,'\t')); const lastId = lastEmoji.length != 0 ? lastEmoji[0].id : null; const emojisData = await this.checkCumstomEmojis(lastId); - if (emojisData.length == 0) return; + if (emojisData.length == 0) { + this.log('No CustomEmojis Added.'); + return; + } // 絵文字データが取得された場合、元々のデータを削除しておく const emojiSize = emojisData.length; this.lastEmoji.remove(lastEmoji); - // 概要について投稿 const server_name = config.serverName ? config.serverName : 'このサーバー'; this.log('Posting...'); - this.log(serifs.checkCustomEmojis.post(server_name, emojiSize)); - await this.ai.post({ - text: serifs.checkCustomEmojis.post(server_name, emojiSize) - }); - // 各絵文字について投稿 - for (const emoji of emojisData){ + // 一気に投稿しないver + if (!config.checkEmojisAtOnce){ + // 概要について投稿 + this.log(serifs.checkCustomEmojis.post(server_name, emojiSize)); await this.ai.post({ - text: serifs.checkCustomEmojis.emojiPost(emoji.name) + text: serifs.checkCustomEmojis.post(server_name, emojiSize) + }); + + // 各絵文字について投稿 + for (const emoji of emojisData){ + await this.ai.post({ + text: serifs.checkCustomEmojis.emojiPost(emoji.name) + }); + this.log(serifs.checkCustomEmojis.emojiPost(emoji.name)); + } + } else { + // 一気に投稿ver + let text = ''; + for (const emoji of emojisData){ + text += serifs.checkCustomEmojis.emojiOnce(emoji.name); + } + const message = serifs.checkCustomEmojis.postOnce(server_name, emojiSize, text); + this.log(message); + await this.ai.post({ + text: message }); - this.log(serifs.checkCustomEmojis.emojiPost(emoji.name)); } // データの保存 - this.log('Last custom emoji data saving...'); + this.log('Last CustomEmojis data saving...'); this.log(JSON.stringify(emojisData[emojiSize-1],null,'\t')); this.lastEmoji.insertOne({ id: emojisData[emojiSize-1].id, updatedAt: Date.now() }); - this.log('Check custom emojis finished!'); + this.log('Check CustomEmojis finished!'); } @autobind @@ -126,10 +142,10 @@ export default class extends Module { @autobind private async mentionHook(msg: Message) { - if (!msg.includes(['カスタムえもじチェック'])) { + if (!msg.includes(['カスタムえもじチェック','カスタムえもじを調べて','カスタムえもじを確認'])) { return false; } else { - this.log('Check custom emojis requested'); + this.log('Check CustomEmojis requested'); } await this.post(); diff --git a/src/serifs.ts b/src/serifs.ts index 32515d5..49fc8d4 100644 --- a/src/serifs.ts +++ b/src/serifs.ts @@ -383,7 +383,9 @@ export default { checkCustomEmojis: { post: (server_name, num) => `${server_name}に${num}件の絵文字が追加されました!`, - emojiPost: emoji => `:${emoji}:\n(\`${emoji}\`) #AddCustomEmojis` + emojiPost: emoji => `:${emoji}:\n(\`${emoji}\`) #AddCustomEmojis`, + postOnce: (server_name, num, text) => `${server_name}に${num}件の絵文字が追加されました!\n${text} #AddCustomEmojis`, + emojiOnce: emoji => `:${emoji}:(\`${emoji}\`)` }, sleepReport: { diff --git a/torisetu.md b/torisetu.md index 3b1c13b..0d29985 100644 --- a/torisetu.md +++ b/torisetu.md @@ -75,6 +75,9 @@ Misskeyにアカウントを作成して初めて投稿を行うと、藍がネ ### ping PONGを返します。生存確認にどうぞ +### カスタム絵文字チェック +1日に1回、カスタム絵文字の追加を監視してくれます。「カスタムえもじチェック」または「カスタムえもじを確認して」ですぐに確認してくれます。 + ### その他反応するフレーズ (トークのみ) * かわいい * なでなで From 47e17052c5f67c9b384002f7bf848e268492a7d7 Mon Sep 17 00:00:00 2001 From: tetsuya-ki <64536338+tetsuya-ki@users.noreply.github.com> Date: Sun, 25 Feb 2024 19:24:53 +0900 Subject: [PATCH 3/9] =?UTF-8?q?Feat:=20=20aichat=E6=A9=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 現時点ではGemini APIのみ対応 --- README.md | 4 ++ src/config.ts | 2 + src/index.ts | 2 + src/modules/aichat/index.ts | 128 ++++++++++++++++++++++++++++++++++++ src/serifs.ts | 5 ++ torisetu.md | 6 ++ 6 files changed, 147 insertions(+) create mode 100644 src/modules/aichat/index.ts diff --git a/README.md b/README.md index 042e75e..cfc8bb9 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ Misskey用の日本語Botです。 "serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)", "checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)", "checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)", + "geminiProApiKey": "Gemini APIキー。2024年初頭は無料で取得可能。詳細は"", + "prompt": "aichatで使われるプロンプト。こだわりがなければ右文章を使う(「」は不要)「返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください。\n\n質問:」", "mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab)", "mecabDic": "MeCab の辞書ファイルパス (オプション)", "memoryDir": "memory.jsonの保存先(オプション、デフォルトは'.'(レポジトリのルートです))" @@ -44,6 +46,8 @@ Misskey用の日本語Botです。 "serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)", "checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)", "checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)", + "geminiProApiKey": "Gemini APIキー。2024年初頭は無料で取得可能。詳細は"", + "prompt": "aichatで使われるプロンプト。こだわりがなければ右文章を使う(「」は不要)「返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください。\n\n質問:」", "mecab": "/usr/bin/mecab", "mecabDic": "/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/", "memoryDir": "data" diff --git a/src/config.ts b/src/config.ts index 9466b0c..119daa8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,6 +12,8 @@ type Config = { serverMonitoring: boolean; checkEmojisEnabled?: boolean; checkEmojisAtOnce?: boolean; + geminiProApiKey?: string; + prompt?: string; mecab?: string; mecabDic?: string; memoryDir?: string; diff --git a/src/index.ts b/src/index.ts index 2f85f93..991a6d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,6 +34,7 @@ import NotingModule from './modules/noting/index.js'; import PollModule from './modules/poll/index.js'; import ReminderModule from './modules/reminder/index.js'; import CheckCustomEmojisModule from './modules/check-custom-emojis/index.js'; +import AiChatModule from './modules/aichat/index.js'; console.log(' __ ____ _____ ___ '); console.log(' /__\\ (_ _)( _ )/ __)'); @@ -96,6 +97,7 @@ promiseRetry(retry => { new PollModule(), new ReminderModule(), new CheckCustomEmojisModule(), + new AiChatModule(), ]); }).catch(e => { log(chalk.red('Failed to fetch the account')); diff --git a/src/modules/aichat/index.ts b/src/modules/aichat/index.ts new file mode 100644 index 0000000..5c6dfbc --- /dev/null +++ b/src/modules/aichat/index.ts @@ -0,0 +1,128 @@ +import { bindThis } from '@/decorators.js'; +import Module from '@/module.js'; +import serifs from '@/serifs.js'; +import Message from '@/message.js'; +import config from '@/config.js'; +import got from 'got'; + +type AiChat = { + question: string; + prompt: string; + api: string; + key: string; +}; +const GEMINI_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent'; + +export default class extends Module { + public readonly name = 'aichat'; + + @bindThis + public install() { + return { + mentionHook: this.mentionHook + }; + } + + @bindThis + private async genTextByGemini(aiChat: AiChat) { + this.log('Generate Text By Gemini...'); + var options = { + url: aiChat.api, + searchParams: { + key: aiChat.key, + }, + json: { + contents: [{ + parts:[{ + text: aiChat.prompt + aiChat.question + }] + }] + }, + }; + let res_data:any = null; + try { + res_data = await got.post(options, + {parseJson: res => JSON.parse(res)}).json(); + this.log(JSON.stringify(res_data)); + if (res_data.hasOwnProperty('candidates')) { + if (res_data.candidates.length > 0) { + if (res_data.candidates[0].hasOwnProperty('content')) { + if (res_data.candidates[0].content.hasOwnProperty('parts')) { + if (res_data.candidates[0].content.parts.length > 0) { + if (res_data.candidates[0].content.parts[0].hasOwnProperty('text')) { + return res_data.candidates[0].content.parts[0].text; + } + } + } + } + } + } + } catch (err: unknown) { + this.log('Error By Call Gemini'); + if (err instanceof Error) { + this.log(`${err.name}\n${err.message}`); + } + } + return null; + } + + @bindThis + private async mentionHook(msg: Message) { + if (!msg.includes([this.name])) { + return false; + } else { + this.log('AiChat requested'); + } + + const kigo = '&'; + let type = 'gemini'; + if (msg.includes([kigo + 'gemini'])) { + type = 'gemini'; + } else if (msg.includes([kigo + 'chatgpt4'])) { + type = 'chatgpt4'; + } else if (msg.includes([kigo + 'chatgpt'])) { + type = 'chatgpt3.5'; + } + const question = msg.extractedText + .replace(this.name, '') + .replace(kigo + type, '') + .trim(); + + let text; + let prompt = ''; + if (config.prompt) { + prompt = config.prompt; + } + switch(type) { + case 'gemini': + if (!config.geminiProApiKey) { + msg.reply(serifs.aichat.nothing(type)); + return false; + } + const aiChat = { + question: question, + prompt: prompt, + api: GEMINI_API, + key: config.geminiProApiKey + }; + text = await this.genTextByGemini(aiChat); + break; + default: + msg.reply(serifs.aichat.nothing(type)); + return false; + } + + if (text == null) { + this.log('The result is invalid. It seems that tokens and other items need to be reviewed.') + msg.reply(serifs.aichat.nothing(type)); + return false; + } + + this.log('Replying...'); + msg.reply(serifs.aichat.post(text, type)); + + return { + reaction: 'like' + }; + } +} diff --git a/src/serifs.ts b/src/serifs.ts index 49fc8d4..2fe0c15 100644 --- a/src/serifs.ts +++ b/src/serifs.ts @@ -388,6 +388,11 @@ export default { emojiOnce: emoji => `:${emoji}:(\`${emoji}\`)` }, + aichat: { + nothing: type => `あぅ... ${type}のAPIキーが登録されてないみたいです`, + post: (text, type) => `${text} (${type}) #aichat`, + }, + sleepReport: { report: hours => `んぅ、${hours}時間くらい寝ちゃってたみたいです`, reportUtatane: 'ん... うたた寝しちゃってました', diff --git a/torisetu.md b/torisetu.md index 0d29985..4ab4b97 100644 --- a/torisetu.md +++ b/torisetu.md @@ -78,6 +78,12 @@ PONGを返します。生存確認にどうぞ ### カスタム絵文字チェック 1日に1回、カスタム絵文字の追加を監視してくれます。「カスタムえもじチェック」または「カスタムえもじを確認して」ですぐに確認してくれます。 +### aichat +``` +@ai aichat 部屋の片付けの手順を教えて +``` +のようにメンションを飛ばすと、GoogleのGemini APIなどを使って返答してくれます(今のバージョンではGemini APIのみ対応)。利用するにはAPIキーの登録が必要です。 + ### その他反応するフレーズ (トークのみ) * かわいい * なでなで From c30659e526fff60b6f350971eb976e25bb3017ae Mon Sep 17 00:00:00 2001 From: tetsuya-ki <64536338+tetsuya-ki@users.noreply.github.com> Date: Fri, 8 Mar 2024 23:25:17 +0900 Subject: [PATCH 4/9] =?UTF-8?q?enhance:=20aichat=E6=A9=9F=E8=83=BD(?= =?UTF-8?q?=E7=94=BB=E5=83=8F=E5=AF=BE=E5=BF=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/aichat/index.ts | 75 +++++++++++++++++++++++++++++++------ src/utils/url2base64.ts | 16 ++++++++ 2 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 src/utils/url2base64.ts diff --git a/src/modules/aichat/index.ts b/src/modules/aichat/index.ts index 5c6dfbc..deb4466 100644 --- a/src/modules/aichat/index.ts +++ b/src/modules/aichat/index.ts @@ -3,6 +3,7 @@ import Module from '@/module.js'; import serifs from '@/serifs.js'; import Message from '@/message.js'; import config from '@/config.js'; +import urlToBase64 from '@/utils/url2base64.js'; import got from 'got'; type AiChat = { @@ -11,7 +12,12 @@ type AiChat = { api: string; key: string; }; +type Base64Image = { + type: string; + base64: string; +}; const GEMINI_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent'; +const GEMINI_VISION_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent'; export default class extends Module { public readonly name = 'aichat'; @@ -24,21 +30,34 @@ export default class extends Module { } @bindThis - private async genTextByGemini(aiChat: AiChat) { + private async genTextByGemini(aiChat: AiChat, image:Base64Image|null) { this.log('Generate Text By Gemini...'); - var options = { + let parts: ({ text: string; inline_data?: undefined; } | { inline_data: { mime_type: string; data: string; }; text?: undefined; })[]; + if (image === null) { + // 画像がない場合、メッセージのみで問い合わせ + parts = [{text: aiChat.prompt + aiChat.question}]; + } else { + // 画像が存在する場合、画像を添付して問い合わせ + parts = [ + { text: aiChat.prompt + aiChat.question }, + { + inline_data: { + mime_type: image.type, + data: image.base64, + }, + }, + ]; + } + let options = { url: aiChat.api, searchParams: { key: aiChat.key, }, json: { - contents: [{ - parts:[{ - text: aiChat.prompt + aiChat.question - }] - }] + contents: {parts: parts} }, }; + this.log(JSON.stringify(options)); let res_data:any = null; try { res_data = await got.post(options, @@ -60,7 +79,35 @@ export default class extends Module { } catch (err: unknown) { this.log('Error By Call Gemini'); if (err instanceof Error) { - this.log(`${err.name}\n${err.message}`); + this.log(`${err.name}\n${err.message}\n${err.stack}`); + } + } + return null; + } + + @bindThis + private async note2base64Image(notesId: string) { + const noteData = await this.ai.api('notes/show', { noteId: notesId }); + let fileType: string | undefined,thumbnailUrl: string | undefined; + if (noteData !== null && noteData.hasOwnProperty('files')) { + if (noteData.files.length > 0) { + if (noteData.files[0].hasOwnProperty('type')) { + fileType = noteData.files[0].type; + } + if (noteData.files[0].hasOwnProperty('thumbnailUrl')) { + thumbnailUrl = noteData.files[0].thumbnailUrl; + } + } + if (fileType !== undefined && thumbnailUrl !== undefined) { + try { + const image = await urlToBase64(thumbnailUrl); + const base64Image:Base64Image = {type: fileType, base64: image}; + return base64Image; + } catch (err: unknown) { + if (err instanceof Error) { + this.log(`${err.name}\n${err.message}\n${err.stack}`); + } + } } } return null; @@ -88,8 +135,8 @@ export default class extends Module { .replace(kigo + type, '') .trim(); - let text; - let prompt = ''; + let text:string, aiChat:AiChat; + let prompt:string = ''; if (config.prompt) { prompt = config.prompt; } @@ -99,13 +146,17 @@ export default class extends Module { msg.reply(serifs.aichat.nothing(type)); return false; } - const aiChat = { + const base64Image:Base64Image|null = await this.note2base64Image(msg.id); + aiChat = { question: question, prompt: prompt, api: GEMINI_API, key: config.geminiProApiKey }; - text = await this.genTextByGemini(aiChat); + if (base64Image !== null) { + aiChat.api = GEMINI_VISION_API; + } + text = await this.genTextByGemini(aiChat, base64Image); break; default: msg.reply(serifs.aichat.nothing(type)); diff --git a/src/utils/url2base64.ts b/src/utils/url2base64.ts new file mode 100644 index 0000000..7350a66 --- /dev/null +++ b/src/utils/url2base64.ts @@ -0,0 +1,16 @@ +import log from '@/utils/log.js'; +import got from 'got'; + +export default async function(url: string): Promise { + try { + const buffer = await got(url).buffer(); + const base64Image = buffer.toString('base64'); + return base64Image; + } catch (err: unknown) { + log('Error in url2base64'); + if (err instanceof Error) { + log(`${err.name}\n${err.message}\n${err.stack}`); + } + throw err; + } +} From c545a3045c679b9b063754293d1262899bf8cca8 Mon Sep 17 00:00:00 2001 From: tetsuya-ki <64536338+tetsuya-ki@users.noreply.github.com> Date: Thu, 22 Aug 2024 09:34:49 +0900 Subject: [PATCH 5/9] fix #2 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5a5fcf1..0685bfc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN if [ $enable_mecab -ne 0 ]; then apt-get update \ && apt-get clean \ && rm -rf /var/lib/apt-get/lists/* \ && cd /opt \ - && git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git \ + && git clone --depth 1 https://github.com/yokomotod/mecab-ipadic-neologd.git \ && cd /opt/mecab-ipadic-neologd \ && ./bin/install-mecab-ipadic-neologd -n -y \ && rm -rf /opt/mecab-ipadic-neologd \ From 3056d0ead517a5327a8a91ae9e42b0705d50209c Mon Sep 17 00:00:00 2001 From: tetsuya-ki <64536338+tetsuya-ki@users.noreply.github.com> Date: Thu, 22 Aug 2024 09:44:26 +0900 Subject: [PATCH 6/9] fix #4 --- src/modules/aichat/index.ts | 14 ++++++++------ src/serifs.ts | 1 + 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/modules/aichat/index.ts b/src/modules/aichat/index.ts index deb4466..817e1d8 100644 --- a/src/modules/aichat/index.ts +++ b/src/modules/aichat/index.ts @@ -16,8 +16,8 @@ type Base64Image = { type: string; base64: string; }; -const GEMINI_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent'; -const GEMINI_VISION_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent'; +const GEMINI_15_FLASH_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent'; +const GEMINI_15_PRO_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent'; export default class extends Module { public readonly name = 'aichat'; @@ -131,6 +131,7 @@ export default class extends Module { type = 'chatgpt3.5'; } const question = msg.extractedText + .toLowerCase() .replace(this.name, '') .replace(kigo + type, '') .trim(); @@ -142,6 +143,7 @@ export default class extends Module { } switch(type) { case 'gemini': + // geminiの場合、APIキーが必須 if (!config.geminiProApiKey) { msg.reply(serifs.aichat.nothing(type)); return false; @@ -150,11 +152,11 @@ export default class extends Module { aiChat = { question: question, prompt: prompt, - api: GEMINI_API, + api: GEMINI_15_PRO_API, key: config.geminiProApiKey }; - if (base64Image !== null) { - aiChat.api = GEMINI_VISION_API; + if (msg.includes([kigo + 'gemini-flash'])) { + aiChat.api = GEMINI_15_FLASH_API; } text = await this.genTextByGemini(aiChat, base64Image); break; @@ -165,7 +167,7 @@ export default class extends Module { if (text == null) { this.log('The result is invalid. It seems that tokens and other items need to be reviewed.') - msg.reply(serifs.aichat.nothing(type)); + msg.reply(serifs.aichat.error(type)); return false; } diff --git a/src/serifs.ts b/src/serifs.ts index 2fe0c15..4bf19b2 100644 --- a/src/serifs.ts +++ b/src/serifs.ts @@ -390,6 +390,7 @@ export default { aichat: { nothing: type => `あぅ... ${type}のAPIキーが登録されてないみたいです`, + error: type => `うぇ...${type}でエラーが発生しちゃったみたいです。gemini-flashだと動くかも?`, post: (text, type) => `${text} (${type}) #aichat`, }, From 43cc2457b5224b6e7c75ec1b9077239d4c5a7daa Mon Sep 17 00:00:00 2001 From: tetsuya-ki <64536338+tetsuya-ki@users.noreply.github.com> Date: Thu, 22 Aug 2024 09:47:32 +0900 Subject: [PATCH 7/9] fix #5 --- src/config.ts | 1 + src/modules/aichat/index.ts | 62 ++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 119daa8..5f93baa 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,6 +13,7 @@ type Config = { checkEmojisEnabled?: boolean; checkEmojisAtOnce?: boolean; geminiProApiKey?: string; + pLaMoApiKey?: string; prompt?: string; mecab?: string; mecabDic?: string; diff --git a/src/modules/aichat/index.ts b/src/modules/aichat/index.ts index 817e1d8..3ba07fc 100644 --- a/src/modules/aichat/index.ts +++ b/src/modules/aichat/index.ts @@ -18,6 +18,7 @@ type Base64Image = { }; const GEMINI_15_FLASH_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent'; const GEMINI_15_PRO_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent'; +const PLAMO_API = 'https://platform.preferredai.jp/api/completion/v1/chat/completions'; export default class extends Module { public readonly name = 'aichat'; @@ -85,6 +86,47 @@ export default class extends Module { return null; } + @bindThis + private async genTextByPLaMo(aiChat: AiChat) { + this.log('Generate Text By PLaMo...'); + + let options = { + url: aiChat.api, + headers: { + Authorization: 'Bearer ' + aiChat.key + }, + json: { + model: 'plamo-beta', + messages: [ + {role: 'system', content: aiChat.prompt}, + {role: 'user', content: aiChat.question}, + ], + }, + }; + this.log(JSON.stringify(options)); + let res_data:any = null; + try { + res_data = await got.post(options, + {parseJson: res => JSON.parse(res)}).json(); + this.log(JSON.stringify(res_data)); + if (res_data.hasOwnProperty('choices')) { + if (res_data.choices.length > 0) { + if (res_data.choices[0].hasOwnProperty('message')) { + if (res_data.choices[0].message.hasOwnProperty('content')) { + return res_data.choices[0].message.content; + } + } + } + } + } catch (err: unknown) { + this.log('Error By Call PLaMo'); + if (err instanceof Error) { + this.log(`${err.name}\n${err.message}\n${err.stack}`); + } + } + return null; + } + @bindThis private async note2base64Image(notesId: string) { const noteData = await this.ai.api('notes/show', { noteId: notesId }); @@ -129,6 +171,8 @@ export default class extends Module { type = 'chatgpt4'; } else if (msg.includes([kigo + 'chatgpt'])) { type = 'chatgpt3.5'; + } else if (msg.includes([kigo + 'plamo'])) { + type = 'plamo'; } const question = msg.extractedText .toLowerCase() @@ -160,7 +204,23 @@ export default class extends Module { } text = await this.genTextByGemini(aiChat, base64Image); break; - default: + + case 'PLaMo': + // PLaMoの場合、APIキーが必須 + if (!config.pLaMoApiKey) { + msg.reply(serifs.aichat.nothing(type)); + return false; + } + aiChat = { + question: question, + prompt: prompt, + api: PLAMO_API, + key: config.pLaMoApiKey + }; + text = await this.genTextByPLaMo(aiChat); + break; + + default: msg.reply(serifs.aichat.nothing(type)); return false; } From ed3f60cb418e0fbcbec6e27e311fa059e5de7e08 Mon Sep 17 00:00:00 2001 From: tetsuya-ki <64536338+tetsuya-ki@users.noreply.github.com> Date: Thu, 22 Aug 2024 09:55:25 +0900 Subject: [PATCH 8/9] =?UTF-8?q?README.md=E3=81=ABPLaMo=E3=81=AB=E3=81=A4?= =?UTF-8?q?=E3=81=84=E3=81=A6=E8=A8=98=E8=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cfc8bb9..aca38a7 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ Misskey用の日本語Botです。 "serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)", "checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)", "checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)", - "geminiProApiKey": "Gemini APIキー。2024年初頭は無料で取得可能。詳細は"", + "geminiProApiKey": "Gemini APIキー。2024年初頭は無料で取得可能。詳細は", + "pLaMoApiKey": "PLaMo APIキー。2024年8月〜10月(予定)は無料でトライアル可能。詳細は", "prompt": "aichatで使われるプロンプト。こだわりがなければ右文章を使う(「」は不要)「返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください。\n\n質問:」", "mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab)", "mecabDic": "MeCab の辞書ファイルパス (オプション)", @@ -46,7 +47,8 @@ Misskey用の日本語Botです。 "serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)", "checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)", "checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)", - "geminiProApiKey": "Gemini APIキー。2024年初頭は無料で取得可能。詳細は"", + "geminiProApiKey": "Gemini APIキー。2024年初頭は無料で取得可能。詳細は", + "pLaMoApiKey": "PLaMo APIキー。2024年8月〜10月(予定)は無料でトライアル可能。詳細は", "prompt": "aichatで使われるプロンプト。こだわりがなければ右文章を使う(「」は不要)「返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください。\n\n質問:」", "mecab": "/usr/bin/mecab", "mecabDic": "/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/", From 95b5e5e7023f4d71fbf876d8b24224b28727765b Mon Sep 17 00:00:00 2001 From: tetsuya-ki <64536338+tetsuya-ki@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:27:33 +0900 Subject: [PATCH 9/9] =?UTF-8?q?aichat=E3=81=AE=E6=96=87=E5=AD=97=E5=88=97?= =?UTF-8?q?=E7=BD=AE=E6=8F=9B=E3=82=92=E6=AD=A3=E8=A6=8F=E8=A1=A8=E7=8F=BE?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/aichat/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modules/aichat/index.ts b/src/modules/aichat/index.ts index 3ba07fc..861e5b7 100644 --- a/src/modules/aichat/index.ts +++ b/src/modules/aichat/index.ts @@ -174,10 +174,11 @@ export default class extends Module { } else if (msg.includes([kigo + 'plamo'])) { type = 'plamo'; } + const reName = RegExp(this.name, "i"); + const reKigoType = RegExp(kigo + type, "i"); const question = msg.extractedText - .toLowerCase() - .replace(this.name, '') - .replace(kigo + type, '') + .replace(reName, '') + .replace(reKigoType, '') .trim(); let text:string, aiChat:AiChat;