Local LLMを試してみた(Radeon => NVIDIA)
この記事は、8ヶ月か前に下書きしていたものを少しだけアップデートしたものです。情報としてはあまり新しくありませんが、Local LLMをお気軽に試したい人には特に問題ない内容かなと思います。Advent Calendarのネタがないので、引っ張り出してきました。
8ヶ月前くらい(2025年4月くらい)のAIのコードは正直全然信用できなくて、クラウドサービスを使うべきとは思いつつ、信頼できないものにお金が流れていくのも嫌な気分になるので、Local LLMなら、電気代以外は固定費用なのでまぁ、いいかという気分でやってみました。
最初に結論を書くと、用途によっては使えそうだけど、ケチらないほうが良さそうだ。ということがわかりました。安物買いの銭失いというやつですね(いや、そんなに安くはないんだけどね...)。
なお、現在は、Claude Codeを常用していて、Local LLMは全然使っていません。
最初にやったこと
Rustでコードを書くのに手持ちのPCが非力だったので以前にPCを買っていました。とはいえ、Mini PCで以下のようなスペックです。
- Mnisforum UM890 Pro
- OSはWindows 11 pro
ここ数十年、Linuxしか使ってないのですが、UM890 ProはWindows 11 Proだし、あえてWindowsで使ってみようというということで、そのままにしていました。WSLもあるし。 Local LLMはLinuxで動かしたほうがパフォーマンスは良いようですが、一旦試すのは、Windowsのままでやりました。
GPUは、書いての通りNVIDIAではありませんが、一応試すことはできます。
Local LLMというのはどうやって動かすのか?
全然知らなかったので、ChatGPTに聞いてみたところ、Ollamaというソフトウェアを使うといいらしいということがわかりました。ただ、NVIDIAではないので、ちょっとだけ、めんどくさかったです。
以下の、780Mがファイル名に入っている(お持ちのGPUによります)、rocm.gfx1103.AMD.780M.phoenix.V5.0.for.hip.sdk.6.2.4.7z をダウンロードします。
- ROCmを入れる
- 先程ダウンロードしたファイルで、ROCmのファイルを上書きする
チャットとして使ってみる
チャットとして使う分には、コマンドラインから、ollama run モデル名 とすることで、チャットができます。チャット用途なら遅いけれど使えなくはなかったです。

モデルをダウンロードするだけなら、ollama pull モデル名とします。
RooCodeでAgentとして使う
無理でした。エラーが出まくります。ただ、モデルの選択が悪かった可能性もあります。
ハードウェアを追加
はい。GPUを買いました。UM890 Proには、Oculinkというインターフェースを付けられるので、そこから、eGPU(外付けGPU)を追加することにしました。
- DEG1 ... minisforumから出ている外付けのGPUキット。電源とGPUを付ける土台ですね
- RTX 3090 ... VRAM24G。中古です
- CORSAIR RM850e ... 850Wの電源
ちなみに、機器選定についても、ChatGPTに聞きました。RTX 3090は、おすすめされていなかったのですが、4090は高いし、メモリは同じ24Gなので、まぁ、いいかというところで、RTX 3090としました。
個人でグラボとか電源とか買ったの大学生ぶりくらいじゃない?と思いつつ、コネクタ等はちゃんとハマるところにしかハマらないようになっているので、特に説明とかを読む必要もなく接続できました。
Ocluinkのケーブルも買ったのですが、DEG1に付属していたので、買う必要がなかったです。そして、買ったやつは認識してくれなかったので返品しました。
見た目は、こんな感じになりました。

DEG1は電源とグラボを金具で接続するので、グラグラしたりはしないですが、むき出しなので、ちょっと気を使いますね。スタンディングデスクの下においているので、うっかり飲み物とかをこぼすと危険。
なお、BitLockerで暗号化しているのですが、OculinkでeGPUをつなぐと、回復キーの入力が求められました。Oculinkを外した場合も、回復キーの入力が求められました。BitLockerを使っていると、付け外しはちょっとめんどくさいですね(稼働中の付け外しは厳禁です)。BigLockerの回復キー長いし。もっと短くても良くない?っていつも思う。
再度、Ollamaを使ってみる
NVIDIAのドライバをインストールして、Ollamaも再インストールしました。
うってかわって、軽いモデルなら爆速になりました。ChatGPTやGemini使うよりも全然速いです(8ヶ月前比)。まぁ、品質はモデルに依るので、イマイチなときももちろんあります。
モデルによっては、なかなかぶっ飛んでるのもいました。狂信者みたいな返答をしてくる(何のモデルかは忘れました)。
RooCodeを使ってみる
当初の目的のコーディングエージェント、RooCodeで使ってみました。が、いろんなモデルを試しまくったものの、最初は全然だめでした。途中でエラーになったり、同じことを繰り返したりしてまったく使い物になりません。
検索したところ、微妙に動くものを見つけました。 Redditで紹介されていた、hhao/qwen2.5-coder-tools:32b
このモデルだと、動きはしました。が、遅いですし、タスクによっては同じことを繰り返し続けたりはします。任せるタスクに依るという感じですが、あまり使いものはならなかった。
Kilo Codeを使ってみる
mychen76/qwen3_cline_roocode:8b という割と小さめのモデルで使ってみました。コンテキストサイズは128Kを指定してみました。
Roo Codeでは、エラーになってうんともすんともでしたが、とりあえず、動いているようです。が、めちゃくちゃ遅いのと、最終的にはやはり動かなくなりました。ちょっと読ませるコード量が多すぎたかもしれません。
AIにコードを書かせるなら、Local LLMに限らずですが、1ファイルは小さくしたほうが良いですね。
Gooseを使ってみる
Gooseは、デスクトップとCUIがあります。 デスクトップで使ってみましたが、あまりうまくいきませんでした。
qwen2.5 Corder 32B instruct で、contextを16Kくらいでなんとか動いたり、動かなかったり。
最初、全然ツールを使ってくれなくて、ログの出力を、Detailedにしたら使うようになりました。
なんでやねん。
いずれにせよ、同じ命令を繰り返したりしますね。
やはりスペックが足りないのか。
LibreChatを使ってみる
ollama run モデル名でコマンドラインからチャットをするのは不便なので、LibreChatを使ってみることにしました(LM Studio使えばチャットできますけどね)。LibreChatはChatGPTライクなインターフェースのチャットアプリです。Dockerでも動きます。
Ollamaを使うためには、少し設定が必要です。なお、Ollamaのバージョンは、0.13.1です。LibreChatのバージョンは、0.7.8です。
cp librechat.example.yaml librechat.yaml
customのところに追加します。
- name: "Ollama" apiKey: "ollama" baseURL: "http://127.0.0.1:11434/v1/" models: default: [ "llama2", "gemma3", "mistral", "codellama", "dolphin-mixtral", "mistral-openorca" ] fetch: true titleConvo: true titleModel: "current_model" modelDisplayLabel: "Ollama" capabilities: ["execute_code", "file_search", "actions", "tools"]
modelsはの、defaultは空でも良いはずです。
docker-compose.ymlに以下のように、上で作ったlibrechat.yamlをvalumesに加えます。
diff --git a/docker-compose.yml b/docker-compose.yml index e16f93f4..c6b0fd95 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,9 @@ services: - ./images:/app/client/public/images - ./uploads:/app/uploads - ./logs:/app/api/logs + - type: bind + source: ./librechat.yaml + target: /app/librechat.yaml mongodb: container_name: chat-mongodb image: mongo
.env に、以下を追加します。
CONFIG_PATH=/app/librechat.yaml
これで、docker-compose up -d すれば、http://127.0.0.1:3080/ でアクセスできるようになります。
ユーザー登録が必要ですが、多分ローカルに保存されるだけなので、気にせず登録すればよいかと思います。 また、LAN内とかで使うのであれば、下記の設定もしておいてもよいかと思います。
BAN_VIOLATIONS=false
もともとtrueですが、falseに変更すれば、利用制限やログイン失敗等で、BANされなくなります。
クラウドサービスのAPIとかも登録できるので、サービスやモデルを切り替えて使いたい人にも便利かもしれません。
なお、Ollamaで新しいモデルをpullした場合は、LibreChatの再起動をしないとモデルの選択項目に現れなかったので、注意してください。
LibreChatをOllamaが動いているPCと別で動かす場合
Ollamaは127.0.0.1:11434をlistenしているので、設定を変える必要があります。
環境変数のOLLAMA_HOSTに0.0.0.0:11434か、そのPCのIPとお好きなポートを設定してやれば良いです。
benchllamaの結果(Windows)
becnhllamaというツールでベンチマークが取れるので、phi4とhhao/qwen2.5-coder-tools:3でとってみました。
~/.local/bin/benchllama evaluate --samples 50 --models phi4 --models hhao/qwen2.5-coder-tools:32b --provider-url http://127.0.0.1:11434
/

Ollama をWSL2で動かす
Windowsで動かすよりも、WSL2で動かしたほうが速いらしい。Linux直で動かすのが一番よいようですが、WSL2でもいい感じらしいので、そちらでも動かしてみました。
サービスの設定
/etc/systemd/system/ollama.serviceに、Environment="0.0.0.0:11434" を書きます。
※ ただし、Ollamaを再インストールすると上書きされます
そして、サービスを再起動しましょう。
sudo service ollama restart
以下のようになっていればOKでしょう。
% sudo ss -tulnp |grep 11434
tcp LISTEN 0 4096 *:11434 *:* users:(("ollama",pid=5102,fd=3))
Networkの設定
PowerShellで以下を実行することで、WSL上で動いているollamaにホストからアクセスできます。
netsh interface portproxy add v4tov4 listenport=11434 listenaddress=0.0.0.0 connectport=11434 connectaddress=[wslのIPアドレス]
wslのIPアドレスはWSL上でip addr|grep 192 とでもすればわかります。
他のPCからアクセスする場合は、WindowsのFirewallの設定を行って、11434へのアクセスを許可してください。

上記のように設定することで、他のPCからもアクセスできます。
WSL2で動かした場合のbenchllamaの結果

微妙に遅いです...ね。
WindowsとWSL2でOllamaのモデルを共有したい
Windowsのモデルを共有する(無理)
WSL2では、/usr/share/ollama/.ollama/ 以下にデータがあるので、WSL2から/mnt/C/Users/ユーザー名/.ollama のsymlink作れば良いんじゃないかと思いました。要は以下をしたわけですが
sudo systemctl stop ollama.service cd /usr/share/ollama sudo mv .ollama .ollama.back sudo ln -s /mnt/C/Users/ユーザー名/.ollama . sudo systemctl start ollama.service
が、とても重くなってしまいました。一回読み込んだら速いかなぁと思ったのですが、そうでもなく、遅いままでした。使えませんね。
WSL2のモデルを共有する(微妙)
環境変数OLLAMA_MODELSを指定すれば良いです(Linuxも環境変数を使っても良いです)。以下のようなパスを環境変数に設定してあげれば良いでしょう。
\\wsl.localhost\Ubuntu-22.04\usr\share\ollama\.ollama\models\
ちなみに、mklink でリンクを作るのはうまく行きませんでした。
パフォーマンスは、特に変わらずという感じでしたので、WSL2からもWindowsからも試したいという人は、WSL2のモデルのパスをOLLAMA_MODELSに指定すると良いでしょう。

ちなみに、その前に、benchllamaで計測したところ、激重になっていてなんでやろと思ったら、Radeonを使うようになっていました。意図せずして、Radeonのパフォーマンスを計測することができました。

雲泥の差ですね。
ただ、うまく行かないことも多かったので、素直にそれぞれの環境にモデルをダウンロードすべきかと思います。
LM Studioを使ってみる
Ollama以外にも、LM Studioというソフトウェアがあります。こちらのほうがGUIがあり、簡単に扱えます。
LM Sttudio であればサーバを有効にするには、以下の設定を有効にします。

Server Portは、デフォルトでは、1234ですが、Ollamaを停止して、同じポートにしました(諸々の設定をしなくて済むので)。
他のPCから使うのであれば、Serve on Local Network を on にして、Status Stopped のスイッチをオン(画像はスイッチがオンなので、Status Runningとなっています)すればよいです。簡単。
Context lengthを追加する場合は、ファイルのタブから、それぞれのギアアイコンから設定できます。

Roo Codeや、Kilo Codeにも LM Studio の設定が存在するので、簡単に使えます。
Gooseに関しては、Add Custom Providerで、OpenAI Compatibleとして、登録すればよいです。

LM Studio はGUIがあるので、モデルの検索、ダウンロード、ロードするモデルのコンテキストの調整などが手軽にできるのは便利です。
終わりに感想
色々試しましたが、コーディングエージェントとしての実用性はちょっと厳しいですね。
ASUS Ascent GX10 を買うしか無いかも。
不動産名義変更でわかる国のシステムを使う苦痛
結構前に引っ越したのに所有不動産の登記変更を放置していました。 本来、2年以内にしないといけないし、罰金もあるのですが、多分あまり請求されないのだと思います。住民票も14日以内に払わないと罰金ですが、あんまり請求されてなさそうです(参考)。
で、司法書士に頼めば楽なのですが、自分でできるっぽいのでやってみました。司法書士に頼むと、必要経費プラス1-2万円くらいかかるそうです。
やってみたところ、確かに自分でできますし、言うほど難しくないのですが...。「うーん?」という気分にはなりました。
どこからするのか?
検索するとすぐ見つかりますが、登記・供託オンライン申請システム 登記ねっと 供託ねっと というサイトがあります。 Web上から申請できるようですが、専用ソフトウェアもあるということなので、僕はそちらを使いました。YouTubeに導入前の説明動画があったります。 動画内にメリットとして「法務局の時間外でも申請できる」と言わていましたが...
平日 8:30〜21:00までしか使えません。
いや、受付くらいしてよ。なんのためのインターネット…。
ちなみに、このサイト、統一感がまるでなくて、ページによっては違うサイト?って感じでして、使っていて非常に気持ち悪いです。出来の悪いフィッシングサイト?とか思っちゃいます。
例として、以下のような種類のヘッダがあります。
1,
2.
3.
(まぁ、これはヘルプだから、まぁ...?)
なんとかして
後、申請ソフトウェアとこのサイト特に情報の連携はされていないようでして、進捗の確認とかは、ソフトウェアで申請した場合は、ソフトウェア上でしか見れません。
不便
誰が作ったんだろう...。Copyrightは、Ministry of Justiceなので、法務省ですね。法務省の英語って、そうなんだ。米司法省もDepartment of Justiceらしいので、まぁ、そうなんですね。
Geminiに聞いてみた所、最初は、「法務省が作っています」という返答だったのですが、「さすがに公開されているのではないでしょうか?非公開とするとMinistry of Justiceの名にふさわしくないですね」と聞いたところ、教えてくれました。
富士通株式会社ということでしたが、2023年のデータで「更改」って書いてるので、今のサイトは別の会社が作った可能性もありますが、まぁ、おそらく同じ会社でしょう。
別に富士通が悪いと言いたいわけではないですが、誰が設計したか知らんし。
より良い設計を働きかけることができなかったのかとか、したけれども法務省が拒否ったのか、それとも、また別の何かがあったのか。
ぜひ、「プロジェクトX」とかでやってほしいですね。
まぁ、テレビないんですけどね。
閑話休題。
本題に戻ります。
1. 申請者登録&申請アプリのダウンロード
フォーム入力項目は結構多いですが、申請者登録を行うことで使えるようになります。 ちなみに、申請者登録の利用規約を読まされるページのヘッダは...

カオス
まぁ、ともかく、申請者登録をして、ソフトウェアをダウンロードすれば良いです。
ちなみに、このアプリ、毎回ログインを求めてきます。
イラッ☆
2. 申請の種類を選ぶ
これがですね、選択するリストが長くてですね...。
今回は、「登記申請書(権利に関する登記)」の「(22)登記名義人の住所変更」がそれっぽいです(Geminiに聞いてもあってるって教えてくれました。あってるか知らんけど)。
検索したい!
ですが、そんな機能はないですね。
申請の種類を選ぶと後はフォームに入力していくだけです。
3. フォームに入力する
不動産の登記簿から色々写さないといけないのだが、なんで写さないといけないんでしょう...? それ、登記簿に書いてあるやん...。
超謎
特にどこかというと「不動産の表示」のところなんですけどね。
※記入されているのは、テンプレートにもともと記入されていた例です
まぁ、疑問を抱いても進まないので、とりあえず、全コピー。
多分、一番重要なのは、不動産の順位番号。順位番号に対して、新しい情報(引越し先の住所)を追加するというような処理が行われるようです。
順位番号に対して、引越し前の住所氏名が書かれているので、名義変更前に同姓同名の人が前の住所に引っ越してきたらやばそうなシステムなのかな?知らんけど。
なお、いわゆる「住所」と、登記簿にかかれている「住所」(?)は異なるので、ご注意ください。
4. 署名
申請書にマイナンバーカードで署名する必要があります。カードリーダーを使って署名します。 毎度暗証番号がどれ使うのかよくわからなくてロックしちゃうことが多いのですが、今回は無事にできました。
5. 住民票の添付
今回の申請は、添付文書が住民票とあるので、紙の住民票を取ってきました。 が、「住民票コード」がアスタリスクでつぶされている。
申請書の項目にあるのだが...。どうやら、コンビニからの取得では出てこないらしく、窓口でとってこないといけない。
知らんわ
まぁ、区役所の出張所が近くてよかった。窓口に「住民票コードがついているものがほしい」と伝えて、無事にもらいました。
スキャンした住民票を申請アプリで添付するんだろうか? そんな甘々なわけないよなぁ、と思ったら、まぁ駄目でした。
電子署名された住民票が必要らしいですが、ネットで調べたのと、電話でも確認したところ、個人が署名付きの電子データとしての住民票を受け取ることは、現時点(2025年3月)ではできないらしい。
なんじゃそりゃ
また、提出方法について提出先の法務局に電話したところ、たとえ電子でおくったとしても、結局郵送で送付しないといけないそうだ。
謎すぎる
というか、申請書はマイナンバーカードで署名しているんだから、代理申請じゃない場合に、住民票いる?
追記: 住民票コードがあれば住民票の添付は不要でした(必要な場合もある)。でも、署名してるんだからコードもいらんような...。

参照: https://houmukyoku.moj.go.jp/homu/content/001392663.pdf
普通に考えたらいらなくね?
まぁ、はてなしか浮かんでこないシステムなんですが、愚痴っても終わらないので置いときましょう。
で、紙を送らないといけないのですが、2日以内に送れとのことです。 消印有効で、多少遅れても良いらしいけど、まぁ、予め準備しといたほうが良いですね。 申請には受付番号というのがあるので、その番号がわかるように、紙に受付番号を書いて、同封しました。
なお、添付文書の住民票には、住民票コードが入っていなくても良いとのことでした。申請でも別になくてもよいのかもしれんけど。まぁわかりかねます。
法務局が住民票を受け付けると、アプリの申請一覧のところの「お知らせ」のところから連絡が来ます。 メールでも連絡は来ますが、お知らせが来たことしかわかりません。
申請者 様 申請番号 **************** 手続名 登記申請書(権利に関する登記) 上記の手続について,お知らせがありますので,お知らせ本文を御確認願います。
概要くらい書いても良くない?
6. 納付
これは、Webから普通にできます。僕の使っているちょっとマイナーな銀行でも振り込みできました。
7. 補正
「補正」というのが申請アプリにでてきたので、何かと思って電話で確認したところ。納付金額不足ということでした。テンプレに入っていた2000円で納付したのですが、3000円とのことでした。
どういう計算かというと、「対象の件数 x 1000円」ということのようです。僕の持っている物件(ワンルーム)が、「土地x2」と「建物x1」だったので、3 x 1000 = 3000円ということでした。
いや、ソフトウェア内で計算してよ...
一応、YouTubeの説明動画内での説明はありましたけどね...。
7. 補正申請
納付金額が間違ってるだけなのですが、補正内容を申請する必要があります。
申請書を右クリックすると、「補正」のサブメニューがあるため、こちらをクリックして、補正の申請をします。そうすると、補正用の申請書ができあがるので、また、再度署名を行います。
後は、申請、納付と同じ要領で行います。
申請書の「既納付」のところに、既に納付した金額をいれておくのを忘れないようにしましょう。
追加納付だけできればいいんじゃないん?
申請なんかいる?
8. 取り下げ
最初に申請したとき、納付って全部終わった後にやるのかなぁ、と思って放置してたら「納付期限切れ」になってしまいました。
納付期限が切れたものは、連絡すれば延ばしてもらえるそうですが、ただ、納付せずに放置はだめで、申請自体をしないのなら、取り下げ申請をする必要があります。
最初の申請書は書き間違いもあったので、取り下げ申請をすることにしました。 名義人変更の場合、権利の項目の取り下げになります。
なんでもかんでも申請です。取り下げくらい申請しなくてもよくないかなぁ?
百歩譲って、申請から「キャンセル」ってやったら勝手に取り下げ申請出してほしいわ。
9. 手続き完了
こんな感じのメールが来ます。このメールに関しては内容がわかりますね。やったね。
申請者 様 申請番号 ************************** 手続名 登記申請書(権利に関する登記) 上記の手続について,電子公文書が発行されましたので,速やかに取得願います。
ともかくこれで終わりです。
「速やかに取得」とあるので、アプリから取得して、NASなり、Google Driveなりに保存しておきましょう。印刷もしておくとよいのではないでしょうか。知らんけど。
まとめ
最短経路でうまく行く手順をまとめますと、
- 申請者登録&ソフトウェアDL
- 申請書をつくる(基本、登記簿謄本と住民票からコピペ)
- 申請書に署名する(マイナンバーカードと、カードリーダーを使う)
- 納付
- 受付番号をメモって法務局に住民票を送付(消印有効)ただし、多少遅れても大丈夫
- 手続き完了するまで待つ
ということのようです。
おまけ1
今回、神戸と新宿の法務局で申請を行っていたのですが、神戸の法務局は1〜数日で手続き完了しましたが、新宿はとても時間がかかります。以下のサイトに目安時間が書いてあります。気長に待ちましょう。
https://houmukyoku.moj.go.jp/tokyo/static/kanryoyotei.htm
おまけ2
実際にどのように書き換わったか?を見ることは申請が終わってもわかりません。
登記情報提供サービスというのがありまして、こちらから取得することができます。 法務省から委託されて運営されているシステムのようです。
「一時利用」「個人利用」「法人利用」「公共機関利用」がありますが、今回のケースでしたら「一時利用」か「個人利用」ですが、「個人利用」の場合、申請してから郵送で初期パスワードが送られてきますので、すぐに使おうと思うなら一時利用になります。
今どき郵送かい
ともあれ、登記簿の情報を取得することができます。
モザイク多めであれですが、こんな感じで情報が書き換わっていることがわかります。
右上に旧住所が入っていて、右下に新住所が入っている感じです。
左上のモザイクが「順位番号」というやつです。
おまけ3
なお、登記情報提供サービスは、一般財団法人民事法務協会が運営してるようですが、開発ベンダーの情報はわかりませんでした。
Geminiの言ってることなので、真偽は不明ですが、システム自体は国が作って、運営自体は、一般財団法人民事法務協会が運営しているようです。オンライン情報で入札情報等がみつからないのは、システムが古いからでは?ということでした。
んー。システムは国の費用で作って、一般財団法人民事法務協会は、システム手数料もらってるのかー。 システム保守料金は国が払ってるのかなぁ?
平成 21 年5月 14 日時点の資料ですが「各府省等からの再就職者が5代以上続いている独立行政法人・特殊法人等・公益法人」の概要 というのが総務省から出ていて、「財団法人民事法務協会」が入っているので、天下り先ということでしょうかね?
感想
このようにいたるところに、使いにくいシステムが乱造されて、本来発生する必要のない無駄な労力や人件費が発生しているのであろうなぁ、ということが想像に難くなくて、頭が痛くなってきましたね。 まともな会社が、まともに設計すれば、多分、少なくとも数百億円は、普通に削減できるんじゃない?知らんけど。
と思っていたら、昔こんなん書いてたなぁ、というのを思い出したので、また、頭が痛くなってきましたね。 ktat.hatenadiary.jp
AIによるChrome拡張の改善
前回、Claude Codeを使って、Chrome拡張を作ってみましたが、使ってみると色々修正したいところが出てきます。
今回、どのように改善していったかを書いていきます。
CLAUDE.mdを書く
GitHubにおいているとおりですが、2つのルールを設定しました。ざっくり以下のようなものです。本来、アーキテクチャとかも書いたほうが良いのでしょうか、コードが少ないのでなくても問題がなさそうです。
改善の仕方
do improvement と言ったら下記を行う。
improvement以下にある.mdファイルに書かれている内容を一つずつやる- 毎回現在のブランチから新しいブランチを作る
- ファイルがなくなるまで続ける
- ドキュメント更新を行う
ドキュメントの更新の仕方
update docs と言ったら下記を行う。
docs/.current-docs-shaに書かれているコミットハッシュから差分を見る- 差分に従ってドキュメントを書く
- commit後に、最新のコミットハッシュを
docs/.current-docs-shaに書く - commitして終わり
改善の仕方
ルール通りですね。簡単な指示を、improvement/*.md に記載して、後は、claude コマンドから、do improvement っていうだけです。
動作確認して追加でやってほしい内容があったら、プロンプトから指示したりしますが、たまにあるくらいで、あんまりないです。
ただ、指示自体が実現不可能だったりしても、「実装してしまう」ことや、「実装方針が間違っている」ということはありました。
例えば、今回のものだと、popup.htmlからのボタンクリックで、gather.townのHTML内から直接クリックイベントを起こそうとしたのですが、できなかったので、別の方法で行うように指示をしたりしました。
ですが、そういう機会はあまりなく、大抵のケースは、Markdownを書いて、do improvement と言うだけで済みました。
また、ちょっとびっくりしたのですが、プロンプトで質問したところ、自分で、improvement/15.md を作ってくれました。
偉すぎでは...?
終わり
CLAUDE.mdに書いた英語がだいぶ怪しいんですが、こちらの意図通りに動いていますね。
しかし、マジで仕様を考える以外にやることないなぁ...(Makefileは手で書いたけど、それくらい)。
AIによるChrome拡張の開発
仕事で、Gatherを使っているのですが、Ubuntuだとデスクトップアプリがないので、ブラウザで使っています。が、通知に気づきにくいのですよね...。Chrome Web Storeに拡張機能が一応あったのですが、そんなにユーザーもいないし...ということで、AIに作らせてみることにしました。
作られたものは、以下になります。
というわけで、以下、僕の指示を中心に書いていきます。
Cloude Codeとのやりとり
仕様を伝える
かなりざっくり伝えてみました。
Google Chromeの拡張を作ろうと思います。仕様としては単純なものです。
gather.town をブラウザで使っていますが、通知に気づきにくいので、waveされたときに、「https://cdn.gather.town/v0/b/gather-town.appspot.com/o/fx%2Fwave_lottie.json 」のNetwork通信が起きるので、Chromeの通信状況を監視して、以下のURLが起きたときに、Desktop通知を行い、このブラウザ拡張のアイコンに通知があることが視認できるようにしたいです。
まとめると、
- gather.townが開かれているタブの通信状況を監視
- https://cdn.gather.town/v0/b/gather-town.appspot.com/o/fx%2Fwave_lottie.json の通信があった場合
- Desktop通知を行う
- ブラウザ拡張のアイコンに通知があることを示す(以下、通知の表示)
- ブラウザ拡張のアイコンをクリックしたら、「通知の表示」は削除し、gather.townが開かれているタブをactiveにする
ちなみに、この指示は間違いだったのですが(という予感はちょっとしてたのですが)、気にせず行きましょう。この指示で普通に一発でChrome拡張機能ができあがりました。インストールして使ってみると、こんな感じになりました。

通知があるときは、アイコンにバッジがつきます。デザインの指示や通知クリアボタンなどについては何も指示していないのに、勝手につけてくれました。
ただ、gatherのタブに移動しても、通知されっぱなしなのは気になります。後、通知バッジだけだとちょっと気づきにくいなーと思い、次の指示(以降は、だいぶざっくり指示になります)。
修正指示1
ktat> gatherのタブに移動した際に、通知のバッジもクリアしたいです。また、通知がオンの間音楽を流すこともできる?
この指示で、通知のバッジクリアはうまくいきました。ですが、音楽を流すのはうまくいきませんでした。
修正指示2
こちらの環境を伝えてみました。
返答は以下。
Claude Code> ● Chrome拡張機能のService Workerでは直接Audio APIが使えない制限があります。代わりにoffscreenドキュメントを使用する方法に変更しましょう。
とのことでした。この変更でうまく動きました。ですが、最初からの指示ミスで、監視してもらったURLは、他の人にWaveしたときにもアクセスされてしまいました。
他の人へのWaveでも通知が来て音が鳴る。
これは、うざい。
最初に書いた通り、はじめからそんな気はしていたんですが、修正してくれるのかも確認したかったというのもあったので、まぁいいやと進めたのでした。
修正指示3
ここは人力で行ったところですが、色々動作確認してみて、通信をみるのはきびしそうでした。ですが、consoleのログをみていると、Waveしたタイミングで「Alerting Wave event」と Chatしたタイミングで「Skipping ChatV2 notification」というログが流れていました。
というわけで、下記の指示をしてみました。
ktat> 通知のトリガーをURLではなくて、consoleのログをチェックすることはできますか?
Skipping ChatV2 notificationAlerting Wave eventが出力されたら、に変更したいです。
以下の返答がありました。
Claude Code> ● コンソールログを監視する方法に変更します。これにはcontent scriptを使用してページ内のコンソールログを傍受します。
傍受って、そういうAPIでもあるのかなぁと思いましたが、特にコードは確認していません。 作ってきたものを試すも、うまく動かず。
修正指示4
続けて、以下の指示。
ktat> consoleのログの監視はうまくできていないようです。
返答は以下。
Claude Code> ● コンソールログの監視方法を改善します。より確実にログを捕捉できるように別のアプローチを試してみましょう。
Claude Codeが出力している差分を眺めたところ、console.*を上書きしているようでした。
今回の修正では、console.* ではなくて、window.console.* を上書きするように変えたようです。
ただ、この修正でもうまく行かないようです。
Claude Code 的には、「Wave Notifier: Monitoring」がconsoleのログに出ていれば成功のようですが、ログにはでているものの、うまく行っていません。
修正指示5
仕方ないので、以下のデバッグ用の指示をだしました。
ktat> コンソールに定期的に「Wave Notifier: Monitoring」は表示されていますが、テキストを拾えていないようです。動作確認のために、拾ったテキストを、コンソールに出力してみてください。ただし、無限ループにならないように、プレフィックスをつけて、プレフィックスがついたものは無視しましょう。
作られたものを確認したところ、プレフィックスのついたログが出てこないので、全然駄目なようでした。
修正指示6
この段階で初めてコードを見て、console系の上書きがうまく行ってないみたいだなーということで、以下の指示。
ktat> console.系のメソッドの上書きができていないように見えます。
この指示の後に、以下のように変更したようです。
10 // より確実にconsoleを取得
11 const consoleObj = window.console || console;
12
13 // 元のconsole関数を保存(Object.getOwnPropertyDescriptorを使用)
14 const originalMethods = {};
15 ['log', 'warn', 'error', 'info', 'debug'].forEach(method => {
16 const descriptor = Object.getOwnPropertyDescriptor(consoleObj, method);
17 if (descriptor && descriptor.value) {
18 originalMethods[method] = descriptor.value.bind(consoleObj);
19 } else {
20 originalMethods[method] = consoleObj[method].bind(consoleObj);
21 }
22 });
これで、ようやく動くようになりました。また上書きするタイミングを調整したようです。正直、コード見なくても、「まだ、consoleのログを拾えていないようです」とでも書けば、直してくれたのではないかと思います。
完成
かかったコストは、$3.1とのことでした(Free Planで、$5だけクレジットを購入しました)。これだけ作ってくれるなら、激安だなーと思いました。時間的には、動作確認とかも含めて1時間くらいでしょうか。初回の指示や各修正指示にかかった時間は1, 2分くらいじゃないでしょうか。
後で思ったのですが、指示のたびにコミットすれば良かったな...。
最後にドキュメントを書かせたところ、$0.7くらい追加で消費されました。
感想
割と雑な仕様と雑な指示でしたが、必要十分なものができたし、何より僕はコードの確認をほとんどしていない(ぉ。滅茶楽ですねー。
さすがに、仕事の本筋のコードならレビューはしますが、これはクリティカルなツールではないので、コードレビューする必要もないので。
とりあえず、明日から使ってみて、問題がでないことを祈ります。
おまけ
以下、全ログになります。
╭───────────────────────────────────────────────────╮ │ ✻ Welcome to Claude Code! │ │ │ │ /help for help, /status for your current setup │ │ │ │ cwd: /home/ktat/git/github │ ╰───────────────────────────────────────────────────╯ Tips for getting started: 1. Run /init to create a CLAUDE.md file with instructions for Claude 2. Use Claude to help with file analysis, editing, bash commands and git 3. Be as specific as you would with another engineer for the best results > Google Chromeの拡張を作ろうと思います。仕様としては単純なものです。 gather.town をブラウザで使っていますが、通知に気づきにくいので、waveされたときに、「https://cdn.gather.town/v0/b/ gather-town.appspot.com/o/fx%2Fwave_lottie.json 」のNetwork通信が起きるので、Chromeの通信状況を監視して、以下のUR Lが起きたときに、Desktop通知を行い、このブラウザ拡張のアイコンに通知があることが視認できるようにしたいです。 まとめると、 1. gather.townが開かれているタブの通信状況を監視 2. https://cdn.gather.town/v0/b/gather-town.appspot.com/o/fx%2Fwave_lottie.json の通信があった場合 3. Desktop通知を行う 4. ブラウザ拡張のアイコンに通知があることを示す(以下、通知の表示) 5. ブラウザ拡張のアイコンをクリックしたら、「通知の表示」は削除し、gather.townが開かれているタブをactiveにする ⎿ Credit balance too low · Add funds: https://console.anthropic.com/settings/billing
Freeプランだったので、お金を入れろということでしたの、ここでクレジットを買って、仕切り直し。
> Google Chromeの拡張を作ろうと思います。仕様としては単純なものです。
gather.town をブラウザで使っていますが、通知に気づきにくいので、waveされたときに、「https://cdn.gather.town/v0/b/
gather-town.appspot.com/o/fx%2Fwave_lottie.json 」のNetwork通信が起きるので、Chromeの通信状況を監視して、以下のUR
Lが起きたときに、Desktop通知を行い、このブラウザ拡張のアイコンに通知があることが視認できるようにしたいです。
まとめると、
1. gather.townが開かれているタブの通信状況を監視
2. https://cdn.gather.town/v0/b/gather-town.appspot.com/o/fx%2Fwave_lottie.json の通信があった場合
3. Desktop通知を行う
4. ブラウザ拡張のアイコンに通知があることを示す(以下、通知の表示)
5. ブラウザ拡張のアイコンをクリックしたら、「通知の表示」は削除し、gather.townが開かれているタブをactiveにする
● gather-notifierディレクトリでChrome拡張機能を作成します。
● Update Todos
⎿ ☐ manifest.jsonを作成
☐ background.jsでネットワーク監視とデスクトップ通知機能を実装
☐ popup.htmlとpopup.jsでアイコンクリック時の動作を実装
☐ アイコンファイルを作成
● List(gather-notfier)
⎿ Listed 2 paths (ctrl+r to expand)
● Update Todos
⎿ ☐ manifest.jsonを作成
☐ background.jsでネットワーク監視とデスクトップ通知機能を実装
☐ popup.htmlとpopup.jsでアイコンクリック時の動作を実装
☐ アイコンファイルを作成
● Write(gather-notfier/manifest.json)
⎿ Wrote 42 lines to gather-notfier/manifest.json
{
"manifest_version": 3,
"name": "Gather.town Wave Notifier",
"version": "1.0",
"description": "Notifies when someone waves at you in gather.town",
"permissions": [
"webRequest",
"notifications",
"tabs",
… +32 lines (ctrl+r to expand)
● Update Todos
⎿ ☒ manifest.jsonを作成
☐ background.jsでネットワーク監視とデスクトップ通知機能を実装
☐ popup.htmlとpopup.jsでアイコンクリック時の動作を実装
☐ アイコンファイルを作成
● Write(gather-notfier/background.js)
⎿ Wrote 90 lines to gather-notfier/background.js
let gatherTabs = new Set();
let hasNotification = false;
// タブの更新を監視してgather.townのタブを追跡
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (tab.url && (tab.url.includes('gather.town') ||
tab.url.includes('app.gather.town'))) {
gatherTabs.add(tabId);
}
});
… +80 lines (ctrl+r to expand)
● Update Todos
⎿ ☒ manifest.jsonを作成
☒ background.jsでネットワーク監視とデスクトップ通知機能を実装
☐ popup.htmlとpopup.jsでアイコンクリック時の動作を実装
☐ アイコンファイルを作成
● Write(gather-notfier/popup.html)
⎿ Wrote 58 lines to gather-notfier/popup.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
width: 200px;
padding: 10px;
font-family: Arial, sans-serif;
}
… +48 lines (ctrl+r to expand)
● Write(gather-notfier/popup.js)
⎿ Wrote 64 lines to gather-notfier/popup.js
document.addEventListener('DOMContentLoaded', async () => {
const statusDiv = document.getElementById('status');
const clearBtn = document.getElementById('clearBtn');
const goToGatherBtn = document.getElementById('goToGatherBtn');
// 現在の通知状態を取得して表示
function updateStatus() {
chrome.storage.local.get(['hasNotification'], (result) => {
const hasNotification = result.hasNotification || false;
… +54 lines (ctrl+r to expand)
● Update Todos
⎿ ☒ manifest.jsonを作成
☒ background.jsでネットワーク監視とデスクトップ通知機能を実装
☒ popup.htmlとpopup.jsでアイコンクリック時の動作を実装
☐ アイコンファイルを作成
● Bash(cd /home/ktat/git/github/gather-notfier && convert -size 16x16 xc:blue -fill white
-gravity center -pointsize 10 -annotate +0+0 "G" icon16.png)
⎿ Error: /bin/bash: line 1: convert: command not found
● Bash(cd /home/ktat/git/github/gather-notfier && convert -size 32x32 xc:blue -fill white
-gravity center -pointsize 20 -annotate +0+0 "G" icon32.png)
⎿ Error: /bin/bash: line 1: convert: command not found
● Bash(cd /home/ktat/git/github/gather-notfier && convert -size 48x48 xc:blue -fill white
-gravity center -pointsize 30 -annotate +0+0 "G" icon48.png)
⎿ Error: /bin/bash: line 1: convert: command not found
● Bash(cd /home/ktat/git/github/gather-notfier && convert -size 128x128 xc:blue -fill white
-gravity center -pointsize 80 -annotate +0+0 "G" icon128.png)
⎿ Error: /bin/bash: line 1: convert: command not found
● Write(gather-notfier/create-icons.html)
⎿ Wrote 48 lines to gather-notfier/create-icons.html
<!DOCTYPE html>
<html>
<head>
<title>Create Extension Icons</title>
</head>
<body>
<canvas id="canvas" width="128" height="128"></canvas>
<div>
<button onclick="downloadIcon(16)">Download 16x16</button>
<button onclick="downloadIcon(32)">Download 32x32</button>
… +38 lines (ctrl+r to expand)
● Update Todos
⎿ ☒ manifest.jsonを作成
☒ background.jsでネットワーク監視とデスクトップ通知機能を実装
☒ popup.htmlとpopup.jsでアイコンクリック時の動作を実装
☒ アイコンファイルを作成
● Write(gather-notfier/README.md)
⎿ Wrote 29 lines to gather-notfier/README.md
# Gather.town Wave Notifier
gather.townでwaveされたときにデスクトップ通知を表示するChrome拡張機能です。
## 機能
1. gather.townのタブでwave_lottie.jsonリクエストを監視
2. waveが検出されたときにデスクトップ通知を表示
3. 拡張機能アイコンにバッジで通知状態を表示
4. アイコンクリックで通知をクリアしてgather.townタブをアクティブ化
… +19 lines (ctrl+r to expand)
● Chrome拡張機能が完成しました。以下の機能を実装しています:
- gather.townでのwave通知の監視
- デスクトップ通知表示
- アイコンバッジによる通知状態表示
- クリックでgather.townタブをアクティブ化
create-icons.htmlをブラウザで開いてアイコンファイルをダウンロードし、Chrome拡張機能として
読み込んでください。
> gatherのタブに移動した際に、通知のバッジもクリアしたいです。また、通知がオンの間音楽を流す
こともできる?
● Update Todos
⎿ ☐ gatherタブがアクティブになったときに通知バッジをクリアする機能を追加
☐ 通知中に音楽を流す機能を追加
● Update Todos
⎿ ☐ gatherタブがアクティブになったときに通知バッジをクリアする機能を追加
☐ 通知中に音楽を流す機能を追加
● Read(gather-notfier/background.js)
⎿ Read 90 lines (ctrl+r to expand)
● Update(gather-notfier/background.js)
⎿ Updated gather-notfier/background.js with 1 addition
1 let gatherTabs = new Set();
2 let hasNotification = false;
3 let audioPlayer = null;
4
5 // タブの更新を監視してgather.townのタブを追跡
6 chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
● Update(gather-notfier/background.js)
⎿ Updated gather-notfier/background.js with 18 additions
9 }
10 });
11
12 // タブがアクティブになったときの処理
13 chrome.tabs.onActivated.addListener(async (activeInfo) => {
14 try {
15 const tab = await chrome.tabs.get(activeInfo.tabId);
16 if (tab.url && (tab.url.includes('gather.town') || tab.url.includes('app.gathe
r.town'))) {
17 // gather.townタブがアクティブになったら通知をクリア
18 if (hasNotification) {
19 hasNotification = false;
20 updateBadge();
21 stopNotificationSound();
22 chrome.storage.local.set({ hasNotification: false });
23 }
24 }
25 } catch (error) {
26 console.error('Error checking active tab:', error);
27 }
28 });
29
30 // タブが閉じられたときの処理
31 chrome.tabs.onRemoved.addListener((tabId) => {
32 gatherTabs.delete(tabId);
● Update(gather-notfier/background.js)
⎿ Updated gather-notfier/background.js with 1 addition
50 // 通知フラグを設定
51 hasNotification = true;
52 updateBadge();
53 playNotificationSound();
54
55 // ストレージに保存
56 chrome.storage.local.set({ hasNotification: true });
● Update(gather-notfier/background.js)
⎿ Updated gather-notfier/background.js with 1 addition
76 // 通知をクリア
77 hasNotification = false;
78 updateBadge();
79 stopNotificationSound();
80 chrome.storage.local.set({ hasNotification: false });
81
82 // gather.townのタブを探してアクティブにする
● Update(gather-notfier/background.js)
⎿ Updated gather-notfier/background.js with 27 additions
103 });
104 });
105
106 // 音声再生関数
107 function playNotificationSound() {
108 try {
109 // 既存の音声を停止
110 stopNotificationSound();
111
112 // 新しい音声を再生(ループ)
113 audioPlayer = new Audio();
114 audioPlayer.src = 'data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8A
AEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt55
9NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+LvuWweBTSH0fPTgjMGHm7A7+OZXQ0P
Vqzn77BdGgU2jdXzzn0vBSF+zPLaizsIGGS57OihUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+
mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsI
GGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zP
LaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3ww
BSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jd
T0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blw
HgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUa
rm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mj
UgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGG
S56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLa
izsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBS
J+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0
z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHg
U2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm
7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUg
wOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS5
6+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaiz
sIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+
zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3
wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2
jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7b
lwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwO
Uarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+
mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsI
GGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zP
LaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3ww
BSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jd
T0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7';
115 audioPlayer.loop = true;
116 audioPlayer.volume = 0.3;
117 audioPlayer.play().catch(error => {
118 console.log('Audio play failed:', error);
119 });
120 } catch (error) {
121 console.error('Error playing notification sound:', error);
122 }
123 }
124
125 function stopNotificationSound() {
126 if (audioPlayer) {
127 audioPlayer.pause();
128 audioPlayer.currentTime = 0;
129 audioPlayer = null;
130 }
131 }
132
133 // インストール時の初期化
134 chrome.runtime.onInstalled.addListener(() => {
135 hasNotification = false;
● Update Todos
⎿ ☒ gatherタブがアクティブになったときに通知バッジをクリアする機能を追加
☐ 通知中に音楽を流す機能を追加
● Read(gather-notfier/popup.js)
⎿ Read 64 lines (ctrl+r to expand)
● Update(gather-notfier/popup.js)
⎿ Updated gather-notfier/popup.js with 4 additions
22 clearBtn.addEventListener('click', () => {
23 chrome.storage.local.set({ hasNotification: false });
24 chrome.action.setBadgeText({ text: '' });
25
26 // バックグラウンドスクリプトに音声停止を伝える
27 chrome.runtime.sendMessage({ action: 'stopSound' });
28
29 updateStatus();
30 });
31
● Update(gather-notfier/popup.js)
⎿ Updated gather-notfier/popup.js with 3 additions
45 chrome.storage.local.set({ hasNotification: false });
46 chrome.action.setBadgeText({ text: '' });
47
48 // バックグラウンドスクリプトに音声停止を伝える
49 chrome.runtime.sendMessage({ action: 'stopSound' });
50
51 // ポップアップを閉じる
52 window.close();
53 } else {
● Update(gather-notfier/background.js)
⎿ Updated gather-notfier/background.js with 7 additions
130 }
131 }
132
133 // ポップアップからのメッセージを処理
134 chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
135 if (message.action === 'stopSound') {
136 stopNotificationSound();
137 }
138 });
139
140 // インストール時の初期化
141 chrome.runtime.onInstalled.addListener(() => {
142 hasNotification = false;
● Update Todos
⎿ ☒ gatherタブがアクティブになったときに通知バッジをクリアする機能を追加
☒ 通知中に音楽を流す機能を追加
● Read(gather-notfier/README.md)
⎿ Read 29 lines (ctrl+r to expand)
● Update(gather-notfier/README.md)
⎿ Updated gather-notfier/README.md with 2 additions
8 2. waveが検出されたときにデスクトップ通知を表示
9 3. 拡張機能アイコンにバッジで通知状態を表示
10 4. アイコンクリックで通知をクリアしてgather.townタブをアクティブ化
11 5. gather.townタブがアクティブになると自動的に通知をクリア
12 6. 通知中に音楽をループ再生(通知クリア時に停止)
13
14 ## インストール方法
15
● 機能を追加しました:
1. gather.townタブがアクティブになったときの自動通知クリア -
background.js:12-28でタブのアクティベーション監視を追加
2. 通知中の音楽再生 - background.js:106-131で音声再生/停止機能を実装、通知時にループ音楽を
再生し、通知クリア時に停止
これで要求された機能が実装されています。
> UbuntuのChromeで試していますが、音が鳴っていないようです。
● Chrome拡張機能のService Workerでは直接Audio
APIが使えない制限があります。代わりにoffscreenドキュメントを使用する方法に変更しましょう。
● Update Todos
⎿ ☐ Chrome拡張のService
Worker音声制限に対応するためoffscreenドキュメントを使用する方法に変更
● Update Todos
⎿ ☐ Chrome拡張のService
Worker音声制限に対応するためoffscreenドキュメントを使用する方法に変更
● Read(gather-notfier/manifest.json)
⎿ Read 42 lines (ctrl+r to expand)
● Update(gather-notfier/manifest.json)
⎿ Updated gather-notfier/manifest.json with 2 additions and 1 removal
9 "notifications",
10 "tabs",
11 "activeTab",
12 "storage"
12 "storage",
13 "offscreen"
14 ],
15
16 "host_permissions": [
● Write(gather-notfier/offscreen.html)
⎿ Wrote 10 lines to gather-notfier/offscreen.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Gather.town Wave Notifier - Audio Player</title>
</head>
<body>
<script src="offscreen.js"></script>
</body>
</html>
● Write(gather-notfier/offscreen.js)
⎿ Wrote 70 lines to gather-notfier/offscreen.js
let audioPlayer = null;
// バックグラウンドスクリプトからのメッセージを受信
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'playSound') {
playNotificationSound();
sendResponse({ success: true });
} else if (message.action === 'stopSound') {
stopNotificationSound();
sendResponse({ success: true });
… +60 lines (ctrl+r to expand)
● Update(gather-notfier/background.js)
⎿ Updated gather-notfier/background.js with 1 addition and 1 removal
1 let gatherTabs = new Set();
2 let hasNotification = false;
3 let audioPlayer = null;
3 let offscreenCreated = false;
4
5 // タブの更新を監視してgather.townのタブを追跡
6 chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
● Update(gather-notfier/background.js)
⎿ Error: String to replace not found in file.
String: // 音声再生関数
function playNotificationSound() {
try {
// 既存の音声を停止
stopNotificationSound();
// 新しい音声を再生(ループ)
audioPlayer = new Audio();
audioPlayer.src = 'data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAA
BAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmM
cBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+LvuWweBTSH0fPTgjMGHm7A7+OZXQ0PVqzn77BdGgU2jdXzzn0
vBSF+zPLaizsIGGS57OihUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7';
… +17 lines (ctrl+r to see all)
● Read(gather-notfier/background.js)
⎿ Read 46 lines (ctrl+r to expand)
● Update(gather-notfier/background.js)
⎿ Error: String to replace not found in file.
String: // 音声再生関数
function playNotificationSound() {
try {
// 既存の音声を停止
stopNotificationSound();
// 新しい音声を再生(ループ)
audioPlayer = new Audio();
audioPlayer.src = 'data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAA
BAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmM
cBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+LvuWweBTSH0fPTgjMGHm7A7+OZXQ0PVqzn77BdGgU2jdXzzn0
vBSF+zPLaizsIGGS57OihUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3wwBSJ+zPLaizsIGGS56+mjUgwOUarm7blwHgU2jdT0z3w
wBSJ+zPLaizsIGGS56+mjUgwOUarm7';
… +17 lines (ctrl+r to see all)
● Write(gather-notfier/background_new.js)
⎿ Wrote 157 lines to gather-notfier/background_new.js
let gatherTabs = new Set();
let hasNotification = false;
let offscreenCreated = false;
// タブの更新を監視してgather.townのタブを追跡
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (tab.url && (tab.url.includes('gather.town') ||
tab.url.includes('app.gather.town'))) {
gatherTabs.add(tabId);
}
});
… +147 lines (ctrl+r to expand)
● Bash(cd /home/ktat/git/github/gather-notfier && mv background.js background_old.js && mv
background_new.js background.js)
⎿ (No content)
● Update Todos
⎿ ☒ Chrome拡張のService
Worker音声制限に対応するためoffscreenドキュメントを使用する方法に変更
● Chrome拡張のService
Worker音声制限に対応するため、offscreenドキュメントを使用する方法に変更しました:
1. manifest.jsonにoffscreen権限を追加
2. offscreen.htmlとoffscreen.jsを作成して音声再生を担当
3. background.jsでoffscreenドキュメントを作成・管理
offscreenドキュメントを使用することで、Chrome拡張のService
Workerでも音声再生が可能になります。拡張機能を再読み込みしてテストしてください。
> 通知のトリガーをURLではなくて、consoleのログをチェックすることはできますか?`Skipping
ChatV2 notification` `Alerting Wave event` が出力されたら、に変更したいです。
● コンソールログを監視する方法に変更します。これにはcontent
scriptを使用してページ内のコンソールログを傍受します。
● Update Todos
⎿ ☐ URL監視からコンソールログ監視に変更
☐ content scriptを追加してコンソールログを傍受
● Update Todos
⎿ ☐ URL監視からコンソールログ監視に変更
☐ content scriptを追加してコンソールログを傍受
● Read(manifest.json)
⎿ Read 43 lines (ctrl+r to expand)
● Update(manifest.json)
⎿ Updated manifest.json with 1 removal
5 "description": "Notifies when someone waves at you in gather.town",
6
7 "permissions": [
8 "webRequest",
8 "notifications",
9 "tabs",
10 "activeTab",
● Update(manifest.json)
⎿ Updated manifest.json with 8 additions
22 "service_worker": "background.js"
23 },
24
25 "content_scripts": [
26 {
27 "matches": ["*://gather.town/*", "*://app.gather.town/*"],
28 "js": ["content.js"],
29 "run_at": "document_start"
30 }
31 ],
32
33 "action": {
34 "default_popup": "popup.html",
35 "default_title": "Gather.town Wave Notifier",
● Update Todos
⎿ ☒ URL監視からコンソールログ監視に変更
☐ content scriptを追加してコンソールログを傍受
● Write(content.js)
⎿ Wrote 75 lines to content.js
// コンソールログを傍受するためのスクリプト
(function() {
// 元のconsole.logを保存
const originalLog = console.log;
const originalWarn = console.warn;
const originalError = console.error;
const originalInfo = console.info;
// console.logを上書き
console.log = function(...args) {
… +65 lines (ctrl+r to expand)
● Read(background.js)
⎿ Read 35 lines (ctrl+r to expand)
● Update(background.js)
⎿ Updated background.js with 20 additions and 28 removals
32 gatherTabs.delete(tabId);
33 });
34
35 // ネットワークリクエストを監視
36 chrome.webRequest.onBeforeRequest.addListener(
37 (details) => {
38 // wave_lottie.jsonのリクエストを検出
39 if (details.url.includes('wave_lottie.json')) {
40 console.log('Wave detected!', details);
41
42 // デスクトップ通知を表示
43 chrome.notifications.create({
44 type: 'basic',
45 iconUrl: 'icon48.png',
46 title: 'Gather.town Wave!',
47 message: 'Someone waved at you in gather.town!'
48 });
49
50 // 通知フラグを設定
51 hasNotification = true;
52 updateBadge();
53 playNotificationSound();
54
55 // ストレージに保存
56 chrome.storage.local.set({ hasNotification: true });
57 }
58 },
59 {
60 urls: ["*://cdn.gather.town/v0/b/gather-town.appspot.com/o/fx%2Fwave_lottie.js
on*"]
61 }
62 );
35 // wave検出時の処理
36 function handleWaveDetection(message) {
37 console.log('Wave detected from console:', message);
38
39 // デスクトップ通知を表示
40 chrome.notifications.create({
41 type: 'basic',
42 iconUrl: 'icon48.png',
43 title: 'Gather.town Wave!',
44 message: 'Someone waved at you in gather.town!'
45 });
46
47 // 通知フラグを設定
48 hasNotification = true;
49 updateBadge();
50 playNotificationSound();
51
52 // ストレージに保存
53 chrome.storage.local.set({ hasNotification: true });
54 }
55
56 // バッジの更新
57 function updateBadge() {
● Read(background.js)
⎿ Read 10 lines (ctrl+r to expand)
● Update(background.js)
⎿ Updated background.js with 4 additions and 1 removal
131 }
132 }
133
134 // ポップアップからのメッセージを処理
134 // ポップアップとcontent scriptからのメッセージを処理
135 chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
136 if (message.action === 'stopSound') {
137 stopNotificationSound();
138 } else if (message.action === 'waveDetected') {
139 // content scriptからのwave検出メッセージ
140 handleWaveDetection(message.message);
141 } else if (message.action === 'playSound' || message.action === 'stopSound') {
142 // offscreenドキュメントからのメッセージは無視
143 return;
● Update Todos
⎿ ☒ URL監視からコンソールログ監視に変更
☒ content scriptを追加してコンソールログを傍受
● Read(README.md)
⎿ Read 31 lines (ctrl+r to expand)
● Update(README.md)
⎿ Updated README.md with 1 addition and 1 removal
4
5 ## 機能
6
7 1. gather.townのタブでwave_lottie.jsonリクエストを監視
7 1. gather.townのコンソールログを監視(`Alerting Wave event` `Skipping ChatV2
notification`)
8 2. waveが検出されたときにデスクトップ通知を表示
9 3. 拡張機能アイコンにバッジで通知状態を表示
10 4. アイコンクリックで通知をクリアしてgather.townタブをアクティブ化
● Update(README.md)
⎿ Updated README.md with 4 additions and 1 removal
25 ## ファイル構成
26
27 - `manifest.json` - 拡張機能の設定
28 - `background.js` - バックグラウンドスクリプト(ネットワーク監視・通知)
28 - `background.js` - バックグラウンドスクリプト(通知処理)
29 - `content.js` - コンテンツスクリプト(コンソールログ監視)
30 - `offscreen.html` - オフスクリーンドキュメント(音声再生用)
31 - `offscreen.js` - 音声再生処理
32 - `popup.html` - ポップアップUI
33 - `popup.js` - ポップアップの動作
34 - `icon*.png` - 拡張機能のアイコン
35 \ No newline at end of file
● URL監視からコンソールログ監視に変更完了しました:
1. manifest.json - webRequest権限を削除し、content_scriptsを追加
2. content.js - コンソールログを傍受してAlerting Wave eventとSkipping ChatV2
notificationを検出
3. background.js - ネットワーク監視コードを削除し、content
scriptからのメッセージ処理を追加
これで拡張機能を再読み込みすると、gather.townページのコンソールログを監視してwave通知を検
出するようになります。
> consoleのログの監視はうまくできていないようです。
● コンソールログの監視方法を改善します。より確実にログを捕捉できるように別のアプローチを試してみましょう。
● Update Todos
⎿ ☐ コンソールログ監視の方法を改善
● Update Todos
⎿ ☐ コンソールログ監視の方法を改善
● Update(content.js)
⎿ Updated content.js with 69 additions and 58 removals
1 // コンソールログを傍受するためのスクリプト
2 // より確実にログを捕捉するために、script要素としてページに注入
3
4 // ページに直接注入するスクリプトを作成
5 const script = document.createElement('script');
6 script.textContent = `
7 (function() {
8 // 元のconsole.logを保存
9 const originalLog = console.log;
10 const originalWarn = console.warn;
11 const originalError = console.error;
12 const originalInfo = console.info;
8 console.log('Gather.town Wave Notifier: Script injected successfully');
9
10 // console.logを上書き
11 console.log = function(...args) {
12 checkForWaveEvents(args);
10 // 元のconsole関数を保存
11 const originalLog = window.console.log;
12 const originalWarn = window.console.warn;
13 const originalError = window.console.error;
14 const originalInfo = window.console.info;
15 const originalDebug = window.console.debug;
16
17 // ログチェック関数
18 function checkForWaveEvents(args, type) {
19 try {
20 const message = Array.isArray(args) ? args.join(' ') : String(args);
21
22 // wave関連のメッセージを検出
23 if (message.includes('Alerting Wave event') || message.includes('Skipping Ch
atV2 notification')) {
24 console.log('🌊 Wave event detected:', message);
25
26 // カスタムイベントを発火してcontent scriptに通知
27 window.dispatchEvent(new CustomEvent('waveDetected', {
28 detail: { message: message, type: type }
29 }));
30 }
31 } catch (error) {
32 console.error('Error checking wave events:', error);
33 }
34 }
35
36 // console関数を上書き
37 window.console.log = function(...args) {
38 checkForWaveEvents(args, 'log');
39 return originalLog.apply(console, args);
40 };
41
42 console.warn = function(...args) {
43 checkForWaveEvents(args);
42 window.console.warn = function(...args) {
43 checkForWaveEvents(args, 'warn');
44 return originalWarn.apply(console, args);
45 };
46
47 console.error = function(...args) {
48 checkForWaveEvents(args);
47 window.console.error = function(...args) {
48 checkForWaveEvents(args, 'error');
49 return originalError.apply(console, args);
50 };
51
52 console.info = function(...args) {
53 checkForWaveEvents(args);
52 window.console.info = function(...args) {
53 checkForWaveEvents(args, 'info');
54 return originalInfo.apply(console, args);
55 };
56
57 // ログメッセージをチェックしてwave関連のメッセージを検出
58 function checkForWaveEvents(args) {
59 const message = args.join(' ');
60
61 // "Alerting Wave event" または "Skipping ChatV2 notification"
が含まれているかチェック
62 if (message.includes('Alerting Wave event') || message.includes('Skipping Chat
V2 notification')) {
63 console.log('Wave event detected in console:', message);
64
65 // バックグラウンドスクリプトに通知を送信
66 chrome.runtime.sendMessage({
67 action: 'waveDetected',
68 message: message
69 }).catch(error => {
70 console.error('Error sending wave detection message:', error);
71 });
72 }
73 }
57 window.console.debug = function(...args) {
58 checkForWaveEvents(args, 'debug');
59 return originalDebug.apply(console, args);
60 };
61
62 // ページロード完了後にも追加のログ監視を設定
63 if (document.readyState === 'loading') {
64 document.addEventListener('DOMContentLoaded', setupAdditionalLogging);
65 } else {
66 setupAdditionalLogging();
67 }
62 console.log('Gather.town Wave Notifier: Console override completed');
63 })();
64 `;
65
66 // スクリプトをページのheadに注入
67 (document.head || document.documentElement).appendChild(script);
68
69 // カスタムイベントをリッスン
70 window.addEventListener('waveDetected', function(event) {
71 console.log('Wave event received in content script:', event.detail);
72
73 function setupAdditionalLogging() {
74 // WebSocketやその他のネットワーク通信もログに出力される場合があるので、
75 // さらに包括的な監視を行う
76
77 // windowのerrorイベントも監視
78 window.addEventListener('error', function(event) {
79 const message = event.message || '';
80 if (message.includes('Alerting Wave event') || message.includes('Skipping Ch
atV2 notification')) {
81 chrome.runtime.sendMessage({
82 action: 'waveDetected',
83 message: message
84 }).catch(error => {
85 console.error('Error sending wave detection message:', error);
86 });
87 }
88 });
89
90 // gather.townのページが完全に読み込まれたことをログ出力
91 console.log('Gather.town Wave Notifier: Console logging interceptor initialize
d');
92 }
93 })();
73 \ No newline at end of file
74 // バックグラウンドスクリプトに通知を送信
75 chrome.runtime.sendMessage({
76 action: 'waveDetected',
77 message: event.detail.message,
78 type: event.detail.type
79 }).catch(error => {
80 console.error('Error sending wave detection message:', error);
81 });
82 });
83
84 // 注入後にスクリプト要素を削除
85 script.remove();
86
87 console.log('Gather.town Wave Notifier: Content script initialized');
88 \ No newline at end of file
● さらに、デバッグ用にテスト機能も追加しましょう。
● Write(test-wave.js)
⎿ Wrote 23 lines to test-wave.js
// テスト用のスクリプト
// gather.townページでこのコードを実行してwave検出をテストできます
// 手動でwaveイベントをテスト
function testWaveEvent() {
console.log('Alerting Wave event - TEST MESSAGE');
console.log('Testing wave detection system');
}
// 手動でChatV2通知をテスト
… +13 lines (ctrl+r to expand)
● Update(content.js)
⎿ Updated content.js with 12 additions and 1 removal
83 // 注入後にスクリプト要素を削除
84 script.remove();
85
86 console.log('Gather.town Wave Notifier: Content script initialized');
86 \ No newline at end of file
87 console.log('Gather.town Wave Notifier: Content script initialized');
88
89 // デバッグ用: テストログを定期的に出力
90 setInterval(() => {
91 console.log('Wave Notifier: Monitoring active at', new Date().toLocaleTimeString
());
92 }, 30000); // 30秒ごと
93
94 // デバッグ用: 手動テスト関数をウィンドウに追加
95 window.testWaveNotifier = function() {
96 console.log('Alerting Wave event - MANUAL TEST');
97 console.log('This should trigger a wave notification');
98 };
99 \ No newline at end of file
● Update Todos
⎿ ☒ コンソールログ監視の方法を改善
● コンソールログ監視を改善しました:
1. script injection方式 -
ページのコンテキストに直接スクリプトを注入してコンソール関数をオーバーライド
2. カスタムイベント - 検出したらcontent scriptにカスタムイベントで通知
3. デバッグ機能 - 30秒ごとの監視状況ログと手動テスト関数を追加
テスト方法:
1. 拡張機能を再読み込み
2. gather.townページでF12を開いてコンソールタブを表示
3. testWaveNotifier()と入力して実行
4. wave通知が発生するかテスト
コンソールに「Wave Notifier: Monitoring
active」が定期的に表示されれば監視が動作しています。
> コンソールに定期的に「Wave Notifier:
Monitoring」は表示されていますが、テキストを拾えていないようです。動作確認のために、拾った
テキストを、コンソールに出力してみてください。ただし、無限ループにならないように、プレフィ
ックスをつけて、プレフィックスがついたものは無視しましょう。
● コンソールログの監視を改善して、拾ったテキストを出力するようにします。無限ループを避けるた
めプレフィックスを使います。
● Update Todos
⎿ ☐ コンソールログ監視のデバッグ機能を改善
● Update Todos
⎿ ☐ コンソールログ監視のデバッグ機能を改善
● Update(content.js)
⎿ Updated content.js with 13 additions and 4 removals
5 const script = document.createElement('script');
6 script.textContent = `
7 (function() {
8 console.log('Gather.town Wave Notifier: Script injected successfully');
8 const PREFIX = '[WAVE-NOTIFIER]';
9 console.log(PREFIX + ' Script injected successfully');
10
11 // 元のconsole関数を保存
12 const originalLog = window.console.log;
...
19 try {
20 const message = Array.isArray(args) ? args.join(' ') : String(args);
21
22 // プレフィックスがついたメッセージは無視(無限ループ防止)
23 if (message.includes(PREFIX)) {
24 return;
25 }
26
27 // デバッグ: すべてのログメッセージを表示
28 originalLog.call(console, PREFIX + ' Intercepted ' + type + ':', message);
29
30 // wave関連のメッセージを検出
31 if (message.includes('Alerting Wave event') || message.includes('Skipping Ch
atV2 notification')) {
32 console.log('🌊 Wave event detected:', message);
32 originalLog.call(console, PREFIX + ' 🌊 Wave event detected:', message);
33
34 // カスタムイベントを発火してcontent scriptに通知
35 window.dispatchEvent(new CustomEvent('waveDetected', {
...
29 }));
30 }
31 } catch (error) {
32 console.error('Error checking wave events:', error);
32 originalError.call(console, PREFIX + ' Error checking wave events:', error);
33 }
34 }
35
...
59 return originalDebug.apply(console, args);
60 };
61
62 console.log('Gather.town Wave Notifier: Console override completed');
62 originalLog.call(console, PREFIX + ' Console override completed');
63 })();
64 `;
65
● Update(content.js)
⎿ Updated content.js with 7 additions and 1 removal
96
97 // デバッグ用: テストログを定期的に出力
98 setInterval(() => {
99 console.log('Wave Notifier: Monitoring active at', new
Date().toLocaleTimeString());
g());
99 console.log('[WAVE-NOTIFIER-CONTENT] Monitoring active at', new
Date().toLocaleTimeString());
TimeString());
100 }, 30000); // 30秒ごと
101
102 // デバッグ用: 手動テスト関数をウィンドウに追加
103 window.testWaveNotifier = function() {
104 console.log('Alerting Wave event - MANUAL TEST');
105 console.log('This should trigger a wave notification');
106 };
107
108 // デバッグ用: ChatV2テスト関数も追加
109 window.testChatV2Notifier = function() {
110 console.log('Skipping ChatV2 notification - MANUAL TEST');
111 console.log('This should trigger a ChatV2 notification');
112 };
113 \ No newline at end of file
● Update Todos
⎿ ☒ コンソールログ監視のデバッグ機能を改善
● デバッグ機能を改善しました:
1. プレフィックス - [WAVE-NOTIFIER]を使って無限ループを防止
2. 全ログ表示 - 拾ったすべてのログメッセージをコンソールに出力
3. テスト関数 - testWaveNotifier()とtestChatV2Notifier()を追加
確認方法:
1. 拡張機能を再読み込み
2. gather.townページでF12を開いてコンソールを確認
3. [WAVE-NOTIFIER] Intercepted log: で始まるメッセージが表示されるかチェック
4. testWaveNotifier()を実行してwave検出をテスト
これで、どのようなログメッセージが拾えているかが分かります。もしログメッセージが表示されな
い場合は、gather.townがログ出力していない可能性があります。
> console.系のメソッドの上書きができていないように見えます。
● console系メソッドの上書きが正しく動作していない問題を修正します。より確実な方法を試してみ
ましょう。
● Update Todos
⎿ ☐ console系メソッドの上書き方法を修正
● Update Todos
⎿ ☐ console系メソッドの上書き方法を修正
● Update(content.js)
⎿ Updated content.js with 50 additions and 33 removals
6 script.textContent = `
7 (function() {
8 const PREFIX = '[WAVE-NOTIFIER]';
9 console.log(PREFIX + ' Script injected successfully');
9
10 // 元のconsole関数を保存
11 const originalLog = window.console.log;
12 const originalWarn = window.console.warn;
13 const originalError = window.console.error;
14 const originalInfo = window.console.info;
15 const originalDebug = window.console.debug;
10 // より確実にconsoleを取得
11 const consoleObj = window.console || console;
12
13 // 元のconsole関数を保存(Object.getOwnPropertyDescriptorを使用)
14 const originalMethods = {};
15 ['log', 'warn', 'error', 'info', 'debug'].forEach(method => {
16 const descriptor = Object.getOwnPropertyDescriptor(consoleObj, method);
17 if (descriptor && descriptor.value) {
18 originalMethods[method] = descriptor.value.bind(consoleObj);
19 } else {
20 originalMethods[method] = consoleObj[method].bind(consoleObj);
21 }
22 });
23
24 originalMethods.log(PREFIX + ' Script injected, original methods saved');
25
26 // ログチェック関数
27 function checkForWaveEvents(args, type) {
28 try {
29 const message = Array.isArray(args) ? args.join(' ') : String(args);
29 // argsを文字列に変換
30 const message = args.map(arg => {
31 if (typeof arg === 'object') {
32 try {
33 return JSON.stringify(arg);
34 } catch {
35 return String(arg);
36 }
37 }
38 return String(arg);
39 }).join(' ');
40
41 // プレフィックスがついたメッセージは無視(無限ループ防止)
42 if (message.includes(PREFIX)) {
...
26 }
27
28 // デバッグ: すべてのログメッセージを表示
29 originalLog.call(console, PREFIX + ' Intercepted ' + type + ':', message);
29 originalMethods.log(PREFIX + ' Intercepted ' + type + ':',
message.substring(0, 200));
(0, 200));
30
31 // wave関連のメッセージを検出
32 if (message.includes('Alerting Wave event') || message.includes('Skipping Ch
atV2 notification')) {
33 originalLog.call(console, PREFIX + ' 🌊 Wave event detected:', message);
33 originalMethods.log(PREFIX + ' 🌊 Wave event detected:', message);
34
35 // カスタムイベントを発火してcontent scriptに通知
36 window.dispatchEvent(new CustomEvent('waveDetected', {
...
38 }));
39 }
40 } catch (error) {
41 originalError.call(console, PREFIX + ' Error checking wave events:', error);
41 originalMethods.error(PREFIX + ' Error checking wave events:', error);
42 }
43 }
44
45 // console関数を上書き
46 window.console.log = function(...args) {
47 checkForWaveEvents(args, 'log');
48 return originalLog.apply(console, args);
49 };
45 // console関数を上書き(definePropertyを使用)
46 function overrideConsoleMethod(methodName) {
47 const original = originalMethods[methodName];
48 Object.defineProperty(consoleObj, methodName, {
49 value: function(...args) {
50 checkForWaveEvents(args, methodName);
51 return original.apply(this, args);
52 },
53 writable: true,
54 configurable: true
55 });
56 }
57
58 window.console.warn = function(...args) {
59 checkForWaveEvents(args, 'warn');
60 return originalWarn.apply(console, args);
61 };
58 // 各メソッドを上書き
59 ['log', 'warn', 'error', 'info', 'debug'].forEach(overrideConsoleMethod);
60
61 window.console.error = function(...args) {
62 checkForWaveEvents(args, 'error');
63 return originalError.apply(console, args);
64 };
61 originalMethods.log(PREFIX + ' Console methods overridden successfully');
62
63 window.console.info = function(...args) {
64 checkForWaveEvents(args, 'info');
65 return originalInfo.apply(console, args);
63 // テスト用の関数を追加
64 window.testConsoleOverride = function() {
65 console.log('Test message for console override');
66 console.warn('Test warning for console override');
67 console.error('Test error for console override');
68 };
69
70 window.console.debug = function(...args) {
71 checkForWaveEvents(args, 'debug');
72 return originalDebug.apply(console, args);
73 };
74
75 originalLog.call(console, PREFIX + ' Console override completed');
70 originalMethods.log(PREFIX + ' Test function added:
window.testConsoleOverride()');
');
71 })();
72 `;
73
● さらに、より早い段階でスクリプトを注入する方法も試してみましょう。
● Update(manifest.json)
⎿ Updated manifest.json with 2 additions and 1 removal
26 {
27 "matches": ["*://gather.town/*", "*://app.gather.town/*"],
28 "js": ["content.js"],
29 "run_at": "document_start"
29 "run_at": "document_start",
30 "world": "MAIN"
31 }
32 ],
33
● Write(content_main.js)
⎿ Wrote 92 lines to content_main.js
// MAIN worldで実行されるスクリプト
// より直接的にconsoleメソッドを上書き
(function() {
const PREFIX = '[WAVE-NOTIFIER-MAIN]';
console.log(PREFIX + ' Main world script starting');
// 元のconsole関数を保存
const originalConsole = {
… +82 lines (ctrl+r to expand)
● Update(manifest.json)
⎿ Updated manifest.json with 7 additions and 1 removal
25 "content_scripts": [
26 {
27 "matches": ["*://gather.town/*", "*://app.gather.town/*"],
28 "js": ["content.js"],
28 "js": ["content_main.js"],
29 "run_at": "document_start",
30 "world": "MAIN"
31 },
32 {
33 "matches": ["*://gather.town/*", "*://app.gather.town/*"],
34 "js": ["content.js"],
35 "run_at": "document_start",
36 "world": "ISOLATED"
37 }
38 ],
39
● Update(content.js)
⎿ Updated content.js with 16 additions and 2 removals
92 // スクリプトをページのheadに注入
93 (document.head || document.documentElement).appendChild(script);
94
95 // カスタムイベントをリッスン
95 // カスタムイベントをリッスン(MAIN worldからのイベント)
96 window.addEventListener('waveDetectedMain', function(event) {
97 console.log('[WAVE-NOTIFIER-ISOLATED] Wave event received from main world:', ev
ent.detail);
98
99 // バックグラウンドスクリプトに通知を送信
100 chrome.runtime.sendMessage({
101 action: 'waveDetected',
102 message: event.detail.message,
103 type: event.detail.type
104 }).catch(error => {
105 console.error('Error sending wave detection message:', error);
106 });
107 });
108
109 // 従来のカスタムイベントもリッスン
110 window.addEventListener('waveDetected', function(event) {
111 console.log('Wave event received in content script:', event.detail);
111 console.log('[WAVE-NOTIFIER-ISOLATED] Wave event received (legacy):',
event.detail);
ail);
112
113 // バックグラウンドスクリプトに通知を送信
114 chrome.runtime.sendMessage({
● Update Todos
⎿ ☒ console系メソッドの上書き方法を修正
● console系メソッドの上書き方法を大幅に改善しました:
1. MAIN worldでの実行 - content_main.jsをMAIN worldで実行してページのconsoleを直接上書き
2. Object.defineProperty使用 - より確実にconsoleメソッドを上書き
3. デュアルアプローチ - MAINとISOLATED worldの両方でスクリプトを実行
テスト方法:
1. 拡張機能を再読み込み
2. gather.townページでF12を開いてコンソールを確認
3. [WAVE-NOTIFIER-MAIN]で始まるメッセージが表示されるかチェック
4. testMainConsole()を実行してテスト
5. 通常のコンソールログに[WAVE-NOTIFIER-MAIN] Interceptedが表示されるかチェック
この方法でconsoleの上書きがより確実に動作するはずです。
> 次回あなたが機能改修するときのために、最低限必要なドキュメントを、docs/specディレクトリ以
下に簡潔に記してください。
● Update Todos
⎿ ☐ docs/specディレクトリに最低限必要なドキュメントを作成
● Update Todos
⎿ ☐ docs/specディレクトリに最低限必要なドキュメントを作成
● Bash(mkdir -p docs/spec)
⎿ (No content)
● Write(docs/spec/architecture.md)
⎿ Wrote 34 lines to docs/spec/architecture.md
# Architecture
## 概要
gather.townでのwave通知を検出してデスクトップ通知とサウンド再生を行うChrome拡張機能
## コンポーネント構成
### Core Components
- **manifest.json** - 拡張機能の設定・権限定義
- **background.js** - メイン処理(通知管理・状態管理)
… +24 lines (ctrl+r to expand)
● Write(docs/spec/detection-logic.md)
⎿ Wrote 39 lines to docs/spec/detection-logic.md
# 検出ロジック
## 検出対象
gather.townページのコンソールログで以下の文字列を検出:
- `Alerting Wave event`
- `Skipping ChatV2 notification`
## 検出方法
### Console Override
… +29 lines (ctrl+r to expand)
● Write(docs/spec/state-management.md)
⎿ Wrote 38 lines to docs/spec/state-management.md
# 状態管理
## 通知状態
- **hasNotification** (boolean) - 未読通知の有無
- **offscreenCreated** (boolean) - offscreenドキュメント作成状態
## 状態保存
- `chrome.storage.local` - ブラウザ再起動後も状態保持
- メモリ変数 - 即座なアクセス用
… +28 lines (ctrl+r to expand)
● Write(docs/spec/audio-system.md)
⎿ Wrote 34 lines to docs/spec/audio-system.md
# 音声システム
## Offscreen Document方式
Chrome MV3のService Worker音声制限を回避するためoffscreenドキュメントを使用
## 実装構造
- **offscreen.html** - 音声再生専用のHTMLページ
- **offscreen.js** - Web Audio API使用の音声生成・再生
- **background.js** - offscreenドキュメント管理・メッセージ送信
… +24 lines (ctrl+r to expand)
● Write(docs/spec/troubleshooting.md)
⎿ Wrote 31 lines to docs/spec/troubleshooting.md
# トラブルシューティング
## 音声が鳴らない
1. **権限確認**: manifest.jsonの`offscreen`権限
2. **ブラウザ設定**: サイトの音声許可設定
3. **デバッグ**: F12でoffscreen.jsのエラーログ確認
4. **再作成**: 拡張機能の再読み込み
## ログ検出されない
1. **スクリプト注入確認**: `[WAVE-NOTIFIER-MAIN]`ログの存在
… +21 lines (ctrl+r to expand)
● Write(docs/spec/testing.md)
⎿ Wrote 43 lines to docs/spec/testing.md
# テスト手順
## 基本動作テスト
### 1. インストール確認
```
1. chrome://extensions/ を開く
2. 拡張機能が読み込まれていることを確認
3. gather.townページでF12を開く
4. [WAVE-NOTIFIER-MAIN] ログが表示されることを確認
… +33 lines (ctrl+r to expand)
● Write(docs/spec/README.md)
⎿ Wrote 35 lines to docs/spec/README.md
# Gather.town Wave Notifier - 技術仕様書
## ドキュメント一覧
### [architecture.md](./architecture.md)
システム全体のアーキテクチャ、コンポーネント構成、データフロー
### [detection-logic.md](./detection-logic.md)
コンソールログ検出の仕組み、オーバーライド方法、検出ロジック
… +25 lines (ctrl+r to expand)
● Update Todos
⎿ ☒ docs/specディレクトリに最低限必要なドキュメントを作成
● docs/specディレクトリに技術仕様書を作成しました:
作成したドキュメント
1. README.md - ドキュメント一覧と開発時の注意事項
2. architecture.md - システム構成とデータフロー
3. detection-logic.md - コンソールログ検出の仕組み
4. state-management.md - 通知状態とタブ管理
5. audio-system.md - Offscreen Document音声システム
6. troubleshooting.md - 問題解決とデバッグ方法
7. testing.md - 動作テスト手順
これらのドキュメントで、次回の機能改修時に必要な技術情報を網羅しています。
╭───────────────────────────────────────────────────────────────────────────────────────────────╮
│ > │
╰───────────────────────────────────────────────────────────────────────────────────────────────╯
⏵⏵ auto-accept edits on (shift+tab to cycle) ✓ Update installed · Restart to apply
wave(👋)のことをwave(🌊)と勘違いしているようですね。ご愛敬。
ワールドトリガー28巻に学ぶ成長の話
ワールドトリガーの28巻を読んで、ヒュースの話がめっちゃ良いと思ったので、紹介したいと思いつつ、うまく書きにくいなーと放置していたのですが、旅行中暇だったので書いてみました。この巻はネットでもかなり話題になっていますので、今更感はあるのですがね。
ワールドトリガーが分からないので、内容がわからないよ…とならないように説明しているつもりです。わからなかったら原作読んでください!
ワールドトリガーの(この話を理解するのに必要な)最低限の粗筋
ワールドトリガーの主人公たちは異世界人と戦う戦闘員です。異世界からの侵攻がないときは、それぞれ鍛錬を積んでいます。また、個人同士で対戦することで各個人に対してランクが付けられています。また、個人はチームに属しており、チーム戦も行われており、チームについてもランクが付けられています。
このエピソードの登場人物
- 修 ... 主人公。個人の才能(≒戦闘力)は低いのに、チーム戦では頭角を現している
- 麓郎 ... 修の活躍を見て、焦りを感じているが、何をしたら良いのか自分では答えを出せない
- ヒュース ... 修のチームメイト。メチャ強い
- 香取 ... 麓郎と同じチームメイト。エース
- 犬飼 ... 麓郎の師匠
28巻の話の筋
麓郎は自分が伸び悩んでいることを自覚していて、同じくらいの才能だと思っていた修が頭角を現していることに焦りを感じていまいた。そこで実力者であるヒュースに自分はどうしたらよいのかを思い切って聞いてみたところ、ヒュースに正論で完膚なきまでにへこまされる、という話です。
なのですが、話をそのままトレースするのは無理なので、話されている主題をメインに書こうと思います。
自分で考させることの重要性
これは、教える側の話ですが、一から十まで教えるのではなく、教えられている側に自分で考えされることが重要だという話。
麓郎の師匠の犬飼が訓練の意図を麓郎に伝えていなかった理由について問われた時に、「麓郎に自分で気づいてほしかった」という意図があったと答えています。
いつまでも一から十まで教え続けていては本人が独り立ちできない。独り立ちするためには自分自身で考えたことにチャレンジして、その結果を引き受ける必要性があるということを述べています。
「ワールドトリガー28巻」 p.199
個人的に時間がないので答えを言ってしまうことが多いので反省…。
実力とは結果である
麓郎は実力についてヒュースに尋ねますが、ヒュースの回答はシンプルでただの「結果」だといいます。 この質問は、麓郎としては、自分に実力がないから結果が出せないと思っていることを暗に示しています。
ですが、ヒュースはそれを否定します。たい焼き屋を例に出して、要は「美味しく作ることができる」ことが実力だといいます。 実際には、これは、美味しければ結果的に売り上げが上がるということを暗に示しています。 個人的な実力としては「美味く作れる」ことで、タイ焼き屋(チーム)としては「よく売れたほう」が実力があることになります。 そして焼く能力に限らず、売る能力でもよいので、「結果(売上)にプラスを与える能力」が「実力」だといいます。
「ワールドトリガー28巻」 p.207
目標に対する期限の重要性
自分がたてている目標には明確な期限を設けるべきだと述べています。 なぜなら、目標に期限がない場合「失敗を正しく認識できない」からだと言います。
期限までに達成できなかった場合、それに費やしたもの(時間、コスト、信用、信頼など)を「失った」と認識できますが、 期限がない場合は、無限に先送りできるため、「まだ何も失っていない」ことになります。
人は「’失ったこと」を認識して初めて、現実的な反省や改善ができるといい、「失ったこと」をいつまでも認識できずに、反省も改善もしない状態を「足踏み」と呼んでいます。
「ワールドトリガー28巻」 p.214
修には明確な期限があり、結果を出すために今何をしなければならないかということを常に考え続けていたからこそ、結果を出すことができた。一方、麓郎は目標は立てていたが明確な期限を設けていなかったので、結果を出していないにも関わらず「足踏み」してしまっていた。と結論づけています。
正しい自己認識の重要性
ワールドトリガーには粗筋に書いた通り、個人戦とチーム戦というものがあります。修は戦闘能力自体は低いので、個人戦では麓郎よりも下位になります。ですが、チーム戦では仲間の特性や対戦相手の特性をよく考えた戦略を練ることで勝利しているため、チーム戦では麓郎のチームより修のチームほうが上ということになります。
麓郎のチームメイトの香取も入れて、チーム戦でのそれぞれの実力を、麓郎の認識で並べると、修(A)、香取(Bの上)、麓郎(Bの中)の順となりました。ですが、ヒュースに言わせると、麓郎のレベルは「C」だと断定します。陸郎の自己認識よりももっと低いということを突きつけます。
チームに香取という強すぎるメンバーがいたため、麓郎は自分の実力に見合わない順位になった。そして、結果的に麓郎は自分の実力に向き合うきっかけを得られなかったと言っています。
麓郎は自分の実力を客観視できていないため、実力を上げるために「何か」をしようと思うものの、適正な段階に挑戦しようとしていないため、結果的に何をしたら良いのかわからなくなってしまって、人に答えを求めてしまうという事態になってしまっていると、ヒュースは結論づけています。
「ワールドトリガー28巻」 p.233
成長するには正しい自己認識を持って、正しい目標(壁)に臨まなければいけない、ということです。
目標を設定し、達成できる内容になるまで刻む
麓郎は「自己認識を正したうえで設定した、低い目標すら達成できなかったら.…」ということを恐れますが、ヒュースは「目標を刻め」と言います。「自分の実力でできるであろう目標を設定しそれをやり遂げる」ことが重要であると述べます。
「ワールドトリガー28巻」 p.237
麓郎はここに至って、成長したいと思いつつも、挑戦を避けることで、自分自身の無力さに向き合う結果になることから逃避していたということに気づかされます。
ですが、「何もできない」などということはほとんどの場合なく、人によって時間の問題はあれ「何かしらできるようになり」ます。ヒュースは自転車に乗るということを例にしていますが、「自転車に乗れなかった状態」から「乗れるようになった」時点で、できないことができるようになっています。
もちろん、人によって学習スピードや結果をだすまでのスピードには差がありますが、そこは人と比べてもどうしようもないことなので、自分と向き合うしかありません。ヒュースは、それを「努力」だと言っています。
そして、「努力」して、できなかったことができるようになったことで「自信」になるのだと述べています。
「ワールドトリガー28巻」 p.244
まとめ
麓郎は正しい自己認識を持っていなかったため、現状の自分に見合わない壁を越えようとして、何をしたらいいのかわからなくなっていた。そのため、自分に何が足りないのか人に答えを教えてもらおうとしていた。また、目標をたてていたものの、明確な期限を設けていなかったため、失敗を正しく認識できず、必要な改善や反省をすることができず、足踏みをしてしまっていた。また、成長したいとは思いながらも、「成長できなかったら…」という不安から、挑戦することから逃げていた、ということに気づかされ、打ちのめされてしまった、ということです。
では、どうしたら良いのか?
麓郎を半面教師とすると、以下のようにすればよさそうですね。
- 自分の実力を正しく把握する
- 目標を立て、期限を設ける
- 最終的な目標は高くてもよいが、その目標を今の自分の実力に見合うように、段階を刻む
- できないことができるようになったことで、自信をつける
最後のところは、できるようになっただけではなくて、結果を出すことで自信になるような気がしますけどね。 そして、自他ともに「実力がある」と認識できるんじゃないかな。
終わり
以上、ワールドトリガー28巻のありがたいお話でした。こんな拙い説明よりも原作読むことをお勧めします。
28巻は麓郎がへこんだところで終わってしまったので、次巻以降で巻き返せるのか、もしかしたらドロップアウトしてしまうかもしれませんが…、頑張ってほしいところですね。
僕の好きな言葉に「始めるのに遅すぎるということはない」というものがあります。イギリスの政治家の言葉らしいですが、ワインバーグの本で読んだと思ったんですが、見つからないんですよね。はて…?
まぁ、ぐだぐだ悩んでる暇があったら始めたほうが良いよということです。足踏みはやめて進みましょう(自戒を込めて)。
Goでテーブル情報を自動生成してgoquで使う
前回(というか、今朝)、goquというGoのSQLのクエリビルダーを紹介しましたが、カラム名をハードコーディングするので、カラムに変更があると、どこに書かれてるのか探すのが大変ですね。grep、目grepで対応して、見えないバグが仕込まれるかもしれません。
そして、人(僕)はよくtypoする。
なので、information_schemaから、カラム名やテーブル名を取得してコード生成してしまえば良いのではと考えました。
Before, After
実際どんなイメージになるかというと。
Before:
ex = goqu.Or(
goqu.And(
goqu.C("first_name").Eq(firstName),
goqu.C("age").Lt(20),
),
goqu.And{
goqu.C("first_name").Eq(firstName),
goqu.C("age").Gt(30),
},
)
query, args := goqu.Dialect("mysql").From("user").Where(ex)
カラム名のage や first_name、テーブル名のuserをハードコードしています。
After:
import ( td "your-project/table-definition" // 適当です。 ) d := td.SchemaName.Table ex = goqu.Or( goqu.And( goqu.C(d.FirstName()).Eq(firstName), goqu.C(d.Age()).Lt(20), ), goqu.And{ goqu.C(d.FirstName()).Eq(firstName), goqu.C(d.Age()).Gt(30), }, ) query, args := goqu.Dialect("mysql").From(d.Tablename()).Where(ex)
という感じに、メソッド呼び出しで書けるようになります。
(tablenameというカラム名があったら、メソッド名とかぶりますが、そんなカラム名は付けないでしょう。つけたとしても、常識的にtable_nameと命名すれば、TableNameになるから問題ない)
自動生成でやっていること
"your-project/table-definition" に、var SchemaName を定義します。そこに渡しているtypeは、以下のようなものです。
type tableName1 struct {} func (tableName1) Tablename () string { return "table_name" } func (tableName1) ColumnName1 () string { return "column_name1" } func (tableName1) ColumnName2 () string { return "column_name2" } type schemaName struct { TableName1 tableNaem1 TableName2 tableNaem2 } var SchemaName = schemaName{ tableName1{}, tableName2{}, }
手で書くなら、気が狂いそうになりますが、まぁ、自動生成なら許せる。
生成するコード
生成するコード自体は特に難しいことはやっておらず、information_schemaのCOLUMNSテーブルを検索して、typeとfuncを作りまくるという単純なコードになります。
package main import ( "fmt" "log" "os" "pmall-api/internal/config" "pmall-api/internal/infrastructure/persistence/db" "strings" "github.com/iancoleman/strcase" ) func main() { if len(os.Args) != 4 { log.Fatal("pass dsn, package name and database names with comma") } dsn := os.Args[1] packageName := os.Args[2] databases := make([]string, 0) for _, name := range strings.Split(os.Args[3], ",") { databases = append(databases, "'"+name+"'") } database, err := db.NewConnection(dsn) if err != nil { log.Fatal(err) } result, err := database.Query( fmt.Sprintf(` SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA in (%s) ORDER BY TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME `, strings.Join(databases, ",")), ) if err != nil { log.Fatal(err) } fmt.Printf("package %s\n\n", packageName) currentSchema := "" currentTable := "" tables := make([]string, 0) if !result.Next() { log.Fatal("no rows") } for { var tableSchema string var tableName string var columnName string err := result.Scan(&tableSchema, &tableName, &columnName) if err != nil { log.Fatal(err) } if currentSchema == "" { currentSchema = tableSchema } fullName := tableSchema + strcase.ToCamel(tableName) if currentTable != tableName { tables = append(tables, tableName) fmt.Printf("type %s struct {}\n\n", fullName) fmt.Printf("func (%s) Tablename() string { return \"%s\" } \n\n", fullName, tableName) currentTable = tableName } fmt.Printf("func (%s) %s() string { return \"%s\"}\n\n", fullName, strcase.ToCamel(columnName), columnName, ) hasNext := result.Next() if !hasNext || currentSchema != tableSchema { fmt.Printf("type %sDB struct {\n", tableSchema) structs := make([]string, 0) for _, table := range tables { fullName := tableSchema + strcase.ToCamel(table) fmt.Printf(" %s %s\n", strcase.ToCamel(table), fullName) structs = append(structs, fullName+"{}") } fmt.Print("}\n\n") fmt.Printf("var %s = &%sDB{\n %s,\n}", strcase.ToCamel(currentSchema), currentSchema, strings.Join(structs, ",\n ")) tables = make([]string, 0) } if !hasNext { break } } }
終わり
これで、typoや、カラム変更も怖くないですね。
めでたし。
GoのSQL Query Builder goquの紹介
GoでORMを使うのではなくて、SQLだけを作りたいという要望があって調べたところ、goquというものがみつかりましたので、試してみました。
書いてから気づきましたが、日本語の紹介記事は他にも結構ありました。
- Goのクエリビルダー goqu を使ってみる | RE:ENGINES
- SQLビルダーgoquの使い方 | フューチャー技術ブログ
- Goのクエリビルダーgoqu入門
- goquを駆使してgoでSQL構築も構造体マッピングもRDBテストもやる - エムスリーテックブログ
基本の使い方
Dialectを指定
MySQLやPostgreSQLなどをDialectで指定できます。MySQLの場合は、以下のようにします。
import ( "github.com/doug-martin/goqu/v9" _ "github.com/doug-martin/goqu/v9/dialect/mysql" )
上記のような importを行い、Dialectで指定します。
goqu.Dialect("mysql")
SELECTの使い方
Tableを指定する
続けて、Fromでテーブルを指定します。
goqu.Dialect("mysql").From("user")
SQLにする
最後に、ToSQL() で、SQLを受け取れます。
sql, _, err := goqu.Dialect("mysql").From("user").ToSQL() fmt.Println(sql)
SQL:
SELECT * FROM `user`
ToSQL() でSQLにするのではなく構造体にマッピングする
ScanStructs や ScanStruct がありますので、下記のように構造体にマッピングすることも出来ます。
type User struct{ FirstName string `db:"first_name"` LastName string `db:"last_name"` Age int `db:"-"` // a field that shouldn't be selected }
users := make([]Users, 0) sql, _, err := goqu.Dialect("mysql").From("user").ScanStructs(&users)
Selectで関数を使う
goqu.L を使うことで、任意の文字を渡すことができるので、関数を渡すことも出来ます。
sql, _, err := goqu.Dialect("mysql").Select(goqu.L("AES_ENCRYPT(num, \"hoge\")").As("crypted")).From("user").ToSQL() fmt.Println(sql)
SQL:
SELECT AES_ENCRYPT(num, "hoge") AS `crypted` FROM `user`
Whereを指定する
Whereで条件を指定することも普通にできます。
sql, _, err := goqu.Dialect("mysql").From("user").Where( goqu.Or( goqu.C("age").Gt(10), goqu.C("age").Lt(20), ), ).ToSQL() fmt.Println(sql)
SQL:
SELECT * FROM `user` WHERE ((`age` > 10) OR (`age` < 20))
このWhereの書き方は、下記のようにも書けます。
goqu.Or(
goqu.Ex{
"age": goqu.Op{"gt": 10},
},
goqu.Ex{
"age": goqu.Op{"lt": 20},
},
),
この書き方なら、カラムがかぶらなければ複数条件をまとめて書けます。
goqu.Or(
goqu.Ex{
"age": goqu.Op{"gt": 10},
"name": goqu.Op{"eq": "name"},
},
goqu.Ex{
"age": goqu.Op{"lt": 20},
},
),
まぁ、下記のようにも書けますし、このスタイルのほうが書きやすいし、読みやすいかなと思います。
goqu.Or(
goqu.And(
goqu.C("age").Gt(10),
goqu.C("name").Eq("name"),
),
goqu.C("age").Lt(20),
),
Placeholderを使う
今までのものは、すべて、SQLの中に変数でわたし値が埋め込み済みでしたが、Prepared(true)を使うことで、Placeholderを使うことも出来ます。
sql, args, err := goqu.Dialect("mysql").From("user").Where( goqu.Or( goqu.C("age").Gt(10), goqu.C("age").Lt(20), ), ).Prepared(true).ToSQL() fmt.Println(sql) // SELECT * FROM `user` WHERE ((`age` > ?) OR (`age` < ?)) fmt.Printf("%#v\n", args) // []interface {}{10, 20}
もっと複雑なSQL
かなり複雑なSQLも表現可能です(複雑っぽくしたかっただけなので、出来上がるSQLには特に意味がないです)。
ds := goqu.From("user_item").Select(goqu.COUNT("*")).GroupBy("user_id").As("ui") sql, _, _ = goqu.Dialect("mysql"). Select(goqu.COUNT("*")). From("user"). Join( goqu.T("user_hobby"), goqu.On(goqu.I("user.id").Eq(goqu.I("user_hobby.user_id"))), ). Join( ds, goqu.On(goqu.I("user.id").Eq(goqu.I("ui.user_id"))), ). Where( goqu.Or( goqu.And( goqu.C("age").Gt(10), goqu.C("name").Eq("name"), ), goqu.C("age").Lt(20), ), ).GroupBy("user.id").Having(goqu.SUM("income").Gt(1000)).ToSQL()
SQL:
SELECT COUNT(*) FROM `user` NNER JOIN `user_hobby` ON (`user`.`id` = `user_hobby`.`user_id`) INNER JOIN (SELECT COUNT(*) FROM "user_item" GROUP BY "user_id") AS `ui` ON (`user`.`id` = `ui`.`user_id`) WHERE (((`age` > 10) AND (`name` = 'name')) OR (`age` < 20)) GROUP BY `user`.`id` HAVING (SUM(`income`) > 1000)
その他
詳しくは、ドキュメントを見てもらえれば良いのですが、希望するものはだいたいできるんじゃないのかなぁという印象を持ちました。SELECT FOR UPDATE なんかも使えるようです。
Updateの使い方
WhereはSELECTのときと同じなので省略します。Updateでテーブル名を渡して、Setでデータをセットします。
sql, _, _ = goqu.Dialect("mysql").Update("user").Set(goqu.Record{"name": "name1"}).ToSQL() fmt.Println(sql)
SQL:
UPDATE `user` SET `name`='name1'
DELETの使い方
こちらも、WhereはSELECTのときと同じなので省略します。Deleteにテーブル名を渡します。
sql, _, _ = goqu.Dialect("mysql").Delete("user").ToSQL() fmt.Println(sql)
SQL:
DELETE `user` FROM `user`
DELETEの後ろにテーブル名入れて使った覚えがあまりないんですが、正しいSQLでした。
INSERTの使い方
INSERTは、goqu.Recordでレコードのセットを作って、Rowsに渡します。
users := []goqu.Record{
{"first_name": "Bob", "last_name": "Yukon", "created": time.Now()},
{"first_name": "Sally", "last_name": "Yukon", "created": time.Now()},
{"first_name": "Jimmy", "last_name": "Yukon", "created": time.Now()},
}
sql, _, _ = goqu.Dialect("mysql").Insert("user").Rows(users).ToSQL()
fmt.Println(sql)
INSERT INTO `user` (`created`, `first_name`, `last_name`) VALUES ('2024-08-19 00:29:07', 'Bob', 'Yukon'), ('2024-08-19 00:29:07', 'Sally', 'Yukon'), ('2024-08-19 00:29:07', 'Jimmy', 'Yukon')
Cols と Vals でわけて指定したり、Rows に構造体を渡すこともできるようです。
終わり
ということで、一通りgoquの使い方を説明しました。
途中でも紹介しましたが、下記に、それぞれのオペレーションについてのドキュメントが詳しく載っています。
デメリットとしては、テーブル名や、カラム名をハードコーディングしないといけないところですが、そこに関しては、コードを自動生成することでなんとかできそうですね。
次回に続きます。