master を dev へ取り込み AGENTS.md を同期する - #30
Conversation
リポジトリガイドラインを追加する
* TTSをAzure SpeechからOpenAI gpt-4o-mini-ttsの女性声へ全面移行 (#2) * TTSの既定ボイスを日英で分けて早口寄りに調整する (#3) * TTSの既定ボイスを日英で分けて早口寄りに調整する 日英とも nova だった既定ボイスを、日本語は shimmer、英語は coral に変更する。 どちらも多言語を読めるが、各言語で最も明瞭に聞こえるボイスを選んだ。 あわせて instructions を明るく張りのある調子へ書き換え、読み上げ速度を 通常より一段速い早口気味に指示する。gpt-4o-mini-tts は speed パラメータが 効かないため、速度指定は instructions で行う必要がある。 日英で別ボイスになったことで実態と食い違ったコメントも更新する。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns9SudVEY9UzSUiaHoDy5s * 読み上げ速度の指示をさらに急ぎ目へ強める 「一段速い早口気味」では十分に速くならなかったため、ラッシュ時の自動放送を 引き合いに出して速さを指示し、句読点・文の切れ目での間と語尾の伸ばしを 明示的に抑える。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns9SudVEY9UzSUiaHoDy5s --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * AIチャットのモデルにGemini(Vertex AI)を追加する (#4) * AIチャットのモデルにGemini(Vertex AI)を追加する AGENT_MODEL に "google:<model>" を追加し、対話本体を Gemini でも動かせるように する。dev は google:gemini-3.7-flash、本番は openai のまま据え置く。 Vertex AI は API キーではなく ADC(サービスアカウント)認証が前提だが Workers に ADC は無いため、鍵 JSON を GOOGLE_VERTEX_SA_KEY で受け取り、JWT 署名と トークン交換は @ai-sdk/google-vertex の edge 版に行わせる(google-auth-library は バンドルに含めない)。project は鍵の project_id を既定とし、location の既定は global。AI Gateway 経由では google-vertex-ai/v1beta1 のモデルパスを baseURL に 組み、直行時と API バージョンを揃える。 思考の抑制は providerOptions を自前で組まず AI SDK 共通の reasoning 設定に委ねる。 ただし 3 系へ 'none'(= thinkingLevel: minimal)を送ると Vertex が 400 "Thinking level is unsupported: THINKING_LEVEL_MINIMAL" を返すため、受理される 最小値の 'low' まで下げる。2.5 系は 'none'(thinkingBudget: 0)で完全に止める。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi * fix: apply CodeRabbit auto-fixes プロバイダ切り替えが「vars のみの変更」で済むのは、そのプロバイダのシークレットを 投入済みの場合に限ることを README に明記する。未投入だと resolveAgentModel が "<SECRET> is not configured" を投げて /agent/chat が全リクエスト失敗するため。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * TTSをOpenAIからGoogle Cloud Text-to-Speechへ全面移行する (#5) * TTSをOpenAIからGoogle Cloud Text-to-Speechへ全面移行する 合成エンジンを OpenAI(gpt-4o-mini-tts) から Cloud Text-to-Speech へ差し替える。 既定ボイスは Firebase Functions 時代の Google TTS 実装と同じ ja-JP-Standard-B / en-US-Standard-G に揃え、Android の端末内蔵 TTS と同水準の音質にする。 Standard / Wavenet / Neural2 以外(Studio・Chirp3-HD・Gemini-TTS)は単価が 桁違いのためクライアントから名指しできないようにする。 認証は API キーではなくサービスアカウント(GOOGLE_TTS_SA_KEY)で、既存の getGoogleAccessToken を再利用する。用途ごとに別の鍵を使うようになるため、 トークンキャッシュのキーを scope 単体からサービスアカウント × scope に変える。 Cloud TTS は AI Gateway の対応プロバイダではないので Google へ直行する。 Standard 系には読み方のプロンプト指示が無いため TTS_INSTRUCTIONS_* は廃止し、 速さは TTS_SPEED(speakingRate)、高さは TTS_PITCH に置き換える。リクエストの model / instructions* は受け取っても無視するため、旧アプリからの呼び出しは そのまま通る。エンジン差し替えで同じ入力でも音声が変わるので、キャッシュキーの 版を 13 から 14 へ上げて旧キャッシュとは分離する。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi * 鍵ファイルが空のときに明示的なエラーにする <NAME>_FILE で指定した鍵 JSON が空でも、これまでは値なしとして黙って捨てられ、 「投入対象のシークレットがありません」としか出ないため原因を追いにくかった。 失敗した `gcloud iam service-accounts keys create` は出力先ファイルを空のまま 残すため、この事故は実際に起こる。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi * fix: apply CodeRabbit auto-fixes ボイス名の検証を形式一致から実在ボイスの allowlist へ変更する。 形式だけを見ていたため ja-US-Standard-A や ja-JP-Standard-Z のような実在しない 名前が検証を通過し、Cloud TTS が 400 "Voice ... does not exist" を返して /tts 全体が失敗していた(未知の名前は既定値へ倒す、というこのモジュールの前提が 成り立っていなかった)。allowlist は voices.list で実在を確認した ja-JP 11 件・ en-US 29 件。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * 本番の AGENT_MODEL を Gemini(Vertex AI)へ切り替える (#6) * staging の駅検索を sapi-bff-stg から stationapi-stg へ切り替える (#7) * staging の駅検索を sapi-bff-stg から stationapi-stg へ切り替える BFF 廃止に伴い、staging の Service Binding を stationapi-stg へ移す。 GraphQL のクエリとレスポンス構造は sapi-bff と同一(stationsByName / stationGroupStations とも引数・フィールド名が変わらない)ため、 クエリ側の処理には手を入れていない。 本番の route 移管はまだ済んでいないので、production は SAPI_BFF → sapi-bff のまま残し、STATION_API を優先して 未設定なら SAPI_BFF へ落ちる順序にした。 なお stationapi は GraphQL をサブドメイン直下(POST /)で受けるため、 STATION_API 経由の POST 先は /graphql ではなくルート直下にしている。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PM76omHQmBawZ3EfBsuU7z * STATION_API 経由のテストで POST であることも検証する CodeRabbit の指摘(PR #7)。テスト名は「サブドメイン直下へ POST する」 なのに URL しか見ておらず、method はスイート全体でも未検証だった。 リクエスト本文の variables は既存テストがカバー済みのため触っていない。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PM76omHQmBawZ3EfBsuU7z --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * 本番の駅検索を sapi-bff から stationapi へ切り替える (#9) * 本番の駅検索を sapi-bff から stationapi へ切り替える BFF 廃止に伴い、production の Service Binding を SAPI_BFF → sapi-bff から STATION_API → stationapi へ移す。staging(#7)と揃い、両環境とも stationapi を直に叩く構成になったため、SAPI_BFF への分岐は不要になった。 postGraphQL から SAPI_BFF 分岐を落とし、Service Binding 不使用時の フォールバック環境変数も SAPI_BFF_GRAPHQL_URL から STATION_API_GRAPHQL_URL へ改名する(wrangler.jsonc では未設定のため リネームによる実害はない)。 分岐が消えたことで「STATION_API があれば優先し、サブドメイン直下へ POST する」テストは意味を失うので削除するが、stationapi が GraphQL を POST / でのみ受けるという前提の検証は残す必要があるため、POST 先 URL と method のアサーションは既存の Service Binding テストへ畳み込んでいる。 なお Service Binding は対象 Worker が存在しないとデプロイに失敗するため、 stationapi(production)のデプロイ完了後にこの変更をデプロイし、 BFF の削除はその後に行うこと。 Closes #8 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sXt1cqwoy71GakDQMGC3G * fix: apply CodeRabbit auto-fixes STATION_API と STATION_API_GRAPHQL_URL の優先順位を回帰テストで固定する。 SAPI_BFF 分岐の削除に伴い「STATION_API があれば優先」テストを落としたが、 残る 2 分岐(Service Binding / URL フォールバック)の順序を確かめる テストが無くなっていた。既存テストは片方だけを設定するため、postGraphQL の 分岐順を入れ替えても全て通ってしまう。 グローバル fetch はスパイに実装を持たせ、分岐順が壊れた場合でも実 ネットワークへ出ないようにする(jest.config.js に restoreMocks が無いため finally で明示的に戻す)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sXt1cqwoy71GakDQMGC3G --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* TTSをAzure SpeechからOpenAI gpt-4o-mini-ttsの女性声へ全面移行 (#2) * TTSの既定ボイスを日英で分けて早口寄りに調整する (#3) * TTSの既定ボイスを日英で分けて早口寄りに調整する 日英とも nova だった既定ボイスを、日本語は shimmer、英語は coral に変更する。 どちらも多言語を読めるが、各言語で最も明瞭に聞こえるボイスを選んだ。 あわせて instructions を明るく張りのある調子へ書き換え、読み上げ速度を 通常より一段速い早口気味に指示する。gpt-4o-mini-tts は speed パラメータが 効かないため、速度指定は instructions で行う必要がある。 日英で別ボイスになったことで実態と食い違ったコメントも更新する。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns9SudVEY9UzSUiaHoDy5s * 読み上げ速度の指示をさらに急ぎ目へ強める 「一段速い早口気味」では十分に速くならなかったため、ラッシュ時の自動放送を 引き合いに出して速さを指示し、句読点・文の切れ目での間と語尾の伸ばしを 明示的に抑える。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns9SudVEY9UzSUiaHoDy5s --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * AIチャットのモデルにGemini(Vertex AI)を追加する (#4) * AIチャットのモデルにGemini(Vertex AI)を追加する AGENT_MODEL に "google:<model>" を追加し、対話本体を Gemini でも動かせるように する。dev は google:gemini-3.7-flash、本番は openai のまま据え置く。 Vertex AI は API キーではなく ADC(サービスアカウント)認証が前提だが Workers に ADC は無いため、鍵 JSON を GOOGLE_VERTEX_SA_KEY で受け取り、JWT 署名と トークン交換は @ai-sdk/google-vertex の edge 版に行わせる(google-auth-library は バンドルに含めない)。project は鍵の project_id を既定とし、location の既定は global。AI Gateway 経由では google-vertex-ai/v1beta1 のモデルパスを baseURL に 組み、直行時と API バージョンを揃える。 思考の抑制は providerOptions を自前で組まず AI SDK 共通の reasoning 設定に委ねる。 ただし 3 系へ 'none'(= thinkingLevel: minimal)を送ると Vertex が 400 "Thinking level is unsupported: THINKING_LEVEL_MINIMAL" を返すため、受理される 最小値の 'low' まで下げる。2.5 系は 'none'(thinkingBudget: 0)で完全に止める。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi * fix: apply CodeRabbit auto-fixes プロバイダ切り替えが「vars のみの変更」で済むのは、そのプロバイダのシークレットを 投入済みの場合に限ることを README に明記する。未投入だと resolveAgentModel が "<SECRET> is not configured" を投げて /agent/chat が全リクエスト失敗するため。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * TTSをOpenAIからGoogle Cloud Text-to-Speechへ全面移行する (#5) * TTSをOpenAIからGoogle Cloud Text-to-Speechへ全面移行する 合成エンジンを OpenAI(gpt-4o-mini-tts) から Cloud Text-to-Speech へ差し替える。 既定ボイスは Firebase Functions 時代の Google TTS 実装と同じ ja-JP-Standard-B / en-US-Standard-G に揃え、Android の端末内蔵 TTS と同水準の音質にする。 Standard / Wavenet / Neural2 以外(Studio・Chirp3-HD・Gemini-TTS)は単価が 桁違いのためクライアントから名指しできないようにする。 認証は API キーではなくサービスアカウント(GOOGLE_TTS_SA_KEY)で、既存の getGoogleAccessToken を再利用する。用途ごとに別の鍵を使うようになるため、 トークンキャッシュのキーを scope 単体からサービスアカウント × scope に変える。 Cloud TTS は AI Gateway の対応プロバイダではないので Google へ直行する。 Standard 系には読み方のプロンプト指示が無いため TTS_INSTRUCTIONS_* は廃止し、 速さは TTS_SPEED(speakingRate)、高さは TTS_PITCH に置き換える。リクエストの model / instructions* は受け取っても無視するため、旧アプリからの呼び出しは そのまま通る。エンジン差し替えで同じ入力でも音声が変わるので、キャッシュキーの 版を 13 から 14 へ上げて旧キャッシュとは分離する。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi * 鍵ファイルが空のときに明示的なエラーにする <NAME>_FILE で指定した鍵 JSON が空でも、これまでは値なしとして黙って捨てられ、 「投入対象のシークレットがありません」としか出ないため原因を追いにくかった。 失敗した `gcloud iam service-accounts keys create` は出力先ファイルを空のまま 残すため、この事故は実際に起こる。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi * fix: apply CodeRabbit auto-fixes ボイス名の検証を形式一致から実在ボイスの allowlist へ変更する。 形式だけを見ていたため ja-US-Standard-A や ja-JP-Standard-Z のような実在しない 名前が検証を通過し、Cloud TTS が 400 "Voice ... does not exist" を返して /tts 全体が失敗していた(未知の名前は既定値へ倒す、というこのモジュールの前提が 成り立っていなかった)。allowlist は voices.list で実在を確認した ja-JP 11 件・ en-US 29 件。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * 本番の AGENT_MODEL を Gemini(Vertex AI)へ切り替える (#6) * staging の駅検索を sapi-bff-stg から stationapi-stg へ切り替える (#7) * staging の駅検索を sapi-bff-stg から stationapi-stg へ切り替える BFF 廃止に伴い、staging の Service Binding を stationapi-stg へ移す。 GraphQL のクエリとレスポンス構造は sapi-bff と同一(stationsByName / stationGroupStations とも引数・フィールド名が変わらない)ため、 クエリ側の処理には手を入れていない。 本番の route 移管はまだ済んでいないので、production は SAPI_BFF → sapi-bff のまま残し、STATION_API を優先して 未設定なら SAPI_BFF へ落ちる順序にした。 なお stationapi は GraphQL をサブドメイン直下(POST /)で受けるため、 STATION_API 経由の POST 先は /graphql ではなくルート直下にしている。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PM76omHQmBawZ3EfBsuU7z * STATION_API 経由のテストで POST であることも検証する CodeRabbit の指摘(PR #7)。テスト名は「サブドメイン直下へ POST する」 なのに URL しか見ておらず、method はスイート全体でも未検証だった。 リクエスト本文の variables は既存テストがカバー済みのため触っていない。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PM76omHQmBawZ3EfBsuU7z --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * 本番の駅検索を sapi-bff から stationapi へ切り替える (#9) * 本番の駅検索を sapi-bff から stationapi へ切り替える BFF 廃止に伴い、production の Service Binding を SAPI_BFF → sapi-bff から STATION_API → stationapi へ移す。staging(#7)と揃い、両環境とも stationapi を直に叩く構成になったため、SAPI_BFF への分岐は不要になった。 postGraphQL から SAPI_BFF 分岐を落とし、Service Binding 不使用時の フォールバック環境変数も SAPI_BFF_GRAPHQL_URL から STATION_API_GRAPHQL_URL へ改名する(wrangler.jsonc では未設定のため リネームによる実害はない)。 分岐が消えたことで「STATION_API があれば優先し、サブドメイン直下へ POST する」テストは意味を失うので削除するが、stationapi が GraphQL を POST / でのみ受けるという前提の検証は残す必要があるため、POST 先 URL と method のアサーションは既存の Service Binding テストへ畳み込んでいる。 なお Service Binding は対象 Worker が存在しないとデプロイに失敗するため、 stationapi(production)のデプロイ完了後にこの変更をデプロイし、 BFF の削除はその後に行うこと。 Closes #8 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sXt1cqwoy71GakDQMGC3G * fix: apply CodeRabbit auto-fixes STATION_API と STATION_API_GRAPHQL_URL の優先順位を回帰テストで固定する。 SAPI_BFF 分岐の削除に伴い「STATION_API があれば優先」テストを落としたが、 残る 2 分岐(Service Binding / URL フォールバック)の順序を確かめる テストが無くなっていた。既存テストは片方だけを設定するため、postGraphQL の 分岐順を入れ替えても全て通ってしまう。 グローバル fetch はスパイに実装を持たせ、分岐順が壊れた場合でも実 ネットワークへ出ないようにする(jest.config.js に restoreMocks が無いため finally で明示的に戻す)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sXt1cqwoy71GakDQMGC3G --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * フィードバックを原因リポジトリへ振り分け、トリアージの起票品質を改善する (#14) * フィードバックを原因リポジトリへ振り分け、トリアージの起票品質を改善する 原因が特定できたフィードバックを該当する公開リポジトリにも起票できるようにし、 あわせて Issue #11 で報告された Spam 誤判定とタイトル破損を修正する。 ## 公開リポジトリへの振り分け - トリアージに原因コンポーネントの判定(component / componentConfidence)を追加 - component が特定でき信頼度 0.7 以上なら、TrainLCD/MobileApp・StationAPI・ Functions・Website のいずれかにスタブ Issue を起票する - 公開リポジトリなのでフィードバックの内容は一切載せず、管理 Issue 番号と チケットID だけを持たせる。管理 Issue 側にもコメントで相互リンクを残す - クラッシュ・スパム・スパム疑い・質問・称賛・トリアージ失敗は振り分けない - 追跡のため、管理 Issue の本文にもチケットID を追加 ## Spam 誤判定の修正(不具合A) - looksLikeSpam の「停車駅/方面」「駅名・路線名の併記」への単独加点を撤回し、 放送定型句との共起を必須にした。本アプリのドメイン語彙そのものであり、 正確な報告ほどスパム判定される構造だったため - 「違います」「反映されない」「ほしい」など、報告で頻出する言い回しを追加 - モデルの非スパム判定をヒューリスティックが無条件に上書きするのをやめ、 モデルの confidence が高い場合は分類を維持して ❓ Unknown Type で人手確認に回す - 感謝・称賛を 💩 Spam に分類していたのをやめ、praise カテゴリ(💚 Praise)を新設 ## タイトル破損の修正(不具合B) - AI_TRIAGE_MODEL を @cf/google/gemma-4-26b-a4b-it に変更(日本語生成品質) - gemma-4 は response を返さず choices[0].message.content のみのため、 両形式を受ける pickModelResponse を追加。従来の取り出し方だと全件失敗する - 推論トレースで JSON が途中で切れる(finish_reason: length)ため max_tokens を 2048 に - category / triageLevel / component を JSON Schema の required に追加。 optional だったため、モデルが省略した非スパムが軒並み question に落ちていた - 未取得・文字化け・生成ループ・助詞連続などの破損タイトルを検知し、 要約失敗マーカー付きで起票する。破損タイトルを要約へ伝播させない - 破損の理由とモデル名をログに残し、破損率を計測できるようにした Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013z8ZwcVqMewc7fZx2gAzxF * fix: CodeRabbit 指摘対応(助詞連続の誤検知・README の条件・推論例外の握り) - particle_run が「のでは」「ものには」など正常な日本語を破損と誤検知していた。 誤検知するとトリアージ結果ごと破棄されるため、同一助詞の 3 連続に限定する - 公開リポジトリへの起票条件を README と実装で一致させる(reportType・ トリアージ成否・スパム疑い・対象カテゴリ・信頼度のすべてを明記) - env.AI.run の例外を試行ループ内で捕捉する。JSON Mode を満たせない場合や AI 側の一時障害で throw すると queue の再試行を使い切ってフィードバックが 消えるため、生成失敗として扱い「要約失敗」で起票して原文を残す なお AI_TRIAGE_MODEL の JSON Mode 非対応の指摘は、実機検証で反証済みのため モデルは変更しない(詳細は PR のレビュー返信を参照)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013z8ZwcVqMewc7fZx2gAzxF * fix: 信頼度を 0..1 に制限する モデルが "90"(パーセント表記のつもり)や負値を返した場合、componentConfidence が そのまま閾値判定を通過し、公開リポジトリへ内容を出すべきでないフィードバックが 起票されうる状態だった。 - TRIAGE_JSON_SCHEMA の confidence / componentConfidence に minimum: 0 と maximum: 1 を追加 - coerceReport でも範囲を検証し、範囲外は値を信用せず既定値へ倒す (componentConfidence は 0 = 公開起票しない、confidence は 0.5 = 従来の既定) 制約追加後も gemma-4 が JSON Mode を満たすことは実機で確認済み。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013z8ZwcVqMewc7fZx2gAzxF * fix: 信頼度は数値のみ受け付ける Number() 任せの変換だと componentConfidence: true や [1] が 1 に化け、 component と対象カテゴリが揃うと公開リポジトリへの起票条件を通過してしまう。 数値、または数値だけの文字列に限定し、それ以外は既定値へ倒す。 - boolean・配列・オブジェクト・null・空文字は既定値(componentConfidence は 0) - 範囲検証のテストで category を省いていたため question 扱いで弾かれ、 信頼度の検証に到達していなかった。category: 'bug' を明示して実際に 信頼度のガードを検証するようにした(ガードを外すと落ちることを確認済み) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013z8ZwcVqMewc7fZx2gAzxF * few-shot に functions / website の例を追加し、唯一例の抽出漏れを防ぐ 原因コンポーネントの判定を入れたが、few-shot に functions / website の例が 1 件も無く、この 2 リポジトリへは実質振り分けられない状態だった。両方の例を 追加する。 few-shot は毎回 FEW_SHOT_LIMIT 件をランダム抽出するため、カテゴリや コンポーネント唯一の例(praise / functions / website)は抽出から漏れると モデルがその値を出さなくなる。weight を付けて残りやすくし、あわせて FEW_SHOT_LIMIT を 12 から 16 に引き上げる。README に weight / disabled と FEW_SHOT_LIMIT の関係を追記した。 KV の実データ(dev / production)にも同じ方針で再投入済み。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4r9HW8s5VHfRoFEJN8bAU --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * アプリ指定の読み上げ速度を受け付け既定速度を等速へ変更 (#16) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* TTSをAzure SpeechからOpenAI gpt-4o-mini-ttsの女性声へ全面移行 (#2) * TTSの既定ボイスを日英で分けて早口寄りに調整する (#3) * TTSの既定ボイスを日英で分けて早口寄りに調整する 日英とも nova だった既定ボイスを、日本語は shimmer、英語は coral に変更する。 どちらも多言語を読めるが、各言語で最も明瞭に聞こえるボイスを選んだ。 あわせて instructions を明るく張りのある調子へ書き換え、読み上げ速度を 通常より一段速い早口気味に指示する。gpt-4o-mini-tts は speed パラメータが 効かないため、速度指定は instructions で行う必要がある。 日英で別ボイスになったことで実態と食い違ったコメントも更新する。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns9SudVEY9UzSUiaHoDy5s * 読み上げ速度の指示をさらに急ぎ目へ強める 「一段速い早口気味」では十分に速くならなかったため、ラッシュ時の自動放送を 引き合いに出して速さを指示し、句読点・文の切れ目での間と語尾の伸ばしを 明示的に抑える。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns9SudVEY9UzSUiaHoDy5s --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * AIチャットのモデルにGemini(Vertex AI)を追加する (#4) * AIチャットのモデルにGemini(Vertex AI)を追加する AGENT_MODEL に "google:<model>" を追加し、対話本体を Gemini でも動かせるように する。dev は google:gemini-3.7-flash、本番は openai のまま据え置く。 Vertex AI は API キーではなく ADC(サービスアカウント)認証が前提だが Workers に ADC は無いため、鍵 JSON を GOOGLE_VERTEX_SA_KEY で受け取り、JWT 署名と トークン交換は @ai-sdk/google-vertex の edge 版に行わせる(google-auth-library は バンドルに含めない)。project は鍵の project_id を既定とし、location の既定は global。AI Gateway 経由では google-vertex-ai/v1beta1 のモデルパスを baseURL に 組み、直行時と API バージョンを揃える。 思考の抑制は providerOptions を自前で組まず AI SDK 共通の reasoning 設定に委ねる。 ただし 3 系へ 'none'(= thinkingLevel: minimal)を送ると Vertex が 400 "Thinking level is unsupported: THINKING_LEVEL_MINIMAL" を返すため、受理される 最小値の 'low' まで下げる。2.5 系は 'none'(thinkingBudget: 0)で完全に止める。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi * fix: apply CodeRabbit auto-fixes プロバイダ切り替えが「vars のみの変更」で済むのは、そのプロバイダのシークレットを 投入済みの場合に限ることを README に明記する。未投入だと resolveAgentModel が "<SECRET> is not configured" を投げて /agent/chat が全リクエスト失敗するため。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * TTSをOpenAIからGoogle Cloud Text-to-Speechへ全面移行する (#5) * TTSをOpenAIからGoogle Cloud Text-to-Speechへ全面移行する 合成エンジンを OpenAI(gpt-4o-mini-tts) から Cloud Text-to-Speech へ差し替える。 既定ボイスは Firebase Functions 時代の Google TTS 実装と同じ ja-JP-Standard-B / en-US-Standard-G に揃え、Android の端末内蔵 TTS と同水準の音質にする。 Standard / Wavenet / Neural2 以外(Studio・Chirp3-HD・Gemini-TTS)は単価が 桁違いのためクライアントから名指しできないようにする。 認証は API キーではなくサービスアカウント(GOOGLE_TTS_SA_KEY)で、既存の getGoogleAccessToken を再利用する。用途ごとに別の鍵を使うようになるため、 トークンキャッシュのキーを scope 単体からサービスアカウント × scope に変える。 Cloud TTS は AI Gateway の対応プロバイダではないので Google へ直行する。 Standard 系には読み方のプロンプト指示が無いため TTS_INSTRUCTIONS_* は廃止し、 速さは TTS_SPEED(speakingRate)、高さは TTS_PITCH に置き換える。リクエストの model / instructions* は受け取っても無視するため、旧アプリからの呼び出しは そのまま通る。エンジン差し替えで同じ入力でも音声が変わるので、キャッシュキーの 版を 13 から 14 へ上げて旧キャッシュとは分離する。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi * 鍵ファイルが空のときに明示的なエラーにする <NAME>_FILE で指定した鍵 JSON が空でも、これまでは値なしとして黙って捨てられ、 「投入対象のシークレットがありません」としか出ないため原因を追いにくかった。 失敗した `gcloud iam service-accounts keys create` は出力先ファイルを空のまま 残すため、この事故は実際に起こる。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi * fix: apply CodeRabbit auto-fixes ボイス名の検証を形式一致から実在ボイスの allowlist へ変更する。 形式だけを見ていたため ja-US-Standard-A や ja-JP-Standard-Z のような実在しない 名前が検証を通過し、Cloud TTS が 400 "Voice ... does not exist" を返して /tts 全体が失敗していた(未知の名前は既定値へ倒す、というこのモジュールの前提が 成り立っていなかった)。allowlist は voices.list で実在を確認した ja-JP 11 件・ en-US 29 件。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * 本番の AGENT_MODEL を Gemini(Vertex AI)へ切り替える (#6) * staging の駅検索を sapi-bff-stg から stationapi-stg へ切り替える (#7) * staging の駅検索を sapi-bff-stg から stationapi-stg へ切り替える BFF 廃止に伴い、staging の Service Binding を stationapi-stg へ移す。 GraphQL のクエリとレスポンス構造は sapi-bff と同一(stationsByName / stationGroupStations とも引数・フィールド名が変わらない)ため、 クエリ側の処理には手を入れていない。 本番の route 移管はまだ済んでいないので、production は SAPI_BFF → sapi-bff のまま残し、STATION_API を優先して 未設定なら SAPI_BFF へ落ちる順序にした。 なお stationapi は GraphQL をサブドメイン直下(POST /)で受けるため、 STATION_API 経由の POST 先は /graphql ではなくルート直下にしている。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PM76omHQmBawZ3EfBsuU7z * STATION_API 経由のテストで POST であることも検証する CodeRabbit の指摘(PR #7)。テスト名は「サブドメイン直下へ POST する」 なのに URL しか見ておらず、method はスイート全体でも未検証だった。 リクエスト本文の variables は既存テストがカバー済みのため触っていない。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PM76omHQmBawZ3EfBsuU7z --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * 本番の駅検索を sapi-bff から stationapi へ切り替える (#9) * 本番の駅検索を sapi-bff から stationapi へ切り替える BFF 廃止に伴い、production の Service Binding を SAPI_BFF → sapi-bff から STATION_API → stationapi へ移す。staging(#7)と揃い、両環境とも stationapi を直に叩く構成になったため、SAPI_BFF への分岐は不要になった。 postGraphQL から SAPI_BFF 分岐を落とし、Service Binding 不使用時の フォールバック環境変数も SAPI_BFF_GRAPHQL_URL から STATION_API_GRAPHQL_URL へ改名する(wrangler.jsonc では未設定のため リネームによる実害はない)。 分岐が消えたことで「STATION_API があれば優先し、サブドメイン直下へ POST する」テストは意味を失うので削除するが、stationapi が GraphQL を POST / でのみ受けるという前提の検証は残す必要があるため、POST 先 URL と method のアサーションは既存の Service Binding テストへ畳み込んでいる。 なお Service Binding は対象 Worker が存在しないとデプロイに失敗するため、 stationapi(production)のデプロイ完了後にこの変更をデプロイし、 BFF の削除はその後に行うこと。 Closes #8 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sXt1cqwoy71GakDQMGC3G * fix: apply CodeRabbit auto-fixes STATION_API と STATION_API_GRAPHQL_URL の優先順位を回帰テストで固定する。 SAPI_BFF 分岐の削除に伴い「STATION_API があれば優先」テストを落としたが、 残る 2 分岐(Service Binding / URL フォールバック)の順序を確かめる テストが無くなっていた。既存テストは片方だけを設定するため、postGraphQL の 分岐順を入れ替えても全て通ってしまう。 グローバル fetch はスパイに実装を持たせ、分岐順が壊れた場合でも実 ネットワークへ出ないようにする(jest.config.js に restoreMocks が無いため finally で明示的に戻す)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sXt1cqwoy71GakDQMGC3G --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * フィードバックを原因リポジトリへ振り分け、トリアージの起票品質を改善する (#14) * フィードバックを原因リポジトリへ振り分け、トリアージの起票品質を改善する 原因が特定できたフィードバックを該当する公開リポジトリにも起票できるようにし、 あわせて Issue #11 で報告された Spam 誤判定とタイトル破損を修正する。 ## 公開リポジトリへの振り分け - トリアージに原因コンポーネントの判定(component / componentConfidence)を追加 - component が特定でき信頼度 0.7 以上なら、TrainLCD/MobileApp・StationAPI・ Functions・Website のいずれかにスタブ Issue を起票する - 公開リポジトリなのでフィードバックの内容は一切載せず、管理 Issue 番号と チケットID だけを持たせる。管理 Issue 側にもコメントで相互リンクを残す - クラッシュ・スパム・スパム疑い・質問・称賛・トリアージ失敗は振り分けない - 追跡のため、管理 Issue の本文にもチケットID を追加 ## Spam 誤判定の修正(不具合A) - looksLikeSpam の「停車駅/方面」「駅名・路線名の併記」への単独加点を撤回し、 放送定型句との共起を必須にした。本アプリのドメイン語彙そのものであり、 正確な報告ほどスパム判定される構造だったため - 「違います」「反映されない」「ほしい」など、報告で頻出する言い回しを追加 - モデルの非スパム判定をヒューリスティックが無条件に上書きするのをやめ、 モデルの confidence が高い場合は分類を維持して ❓ Unknown Type で人手確認に回す - 感謝・称賛を 💩 Spam に分類していたのをやめ、praise カテゴリ(💚 Praise)を新設 ## タイトル破損の修正(不具合B) - AI_TRIAGE_MODEL を @cf/google/gemma-4-26b-a4b-it に変更(日本語生成品質) - gemma-4 は response を返さず choices[0].message.content のみのため、 両形式を受ける pickModelResponse を追加。従来の取り出し方だと全件失敗する - 推論トレースで JSON が途中で切れる(finish_reason: length)ため max_tokens を 2048 に - category / triageLevel / component を JSON Schema の required に追加。 optional だったため、モデルが省略した非スパムが軒並み question に落ちていた - 未取得・文字化け・生成ループ・助詞連続などの破損タイトルを検知し、 要約失敗マーカー付きで起票する。破損タイトルを要約へ伝播させない - 破損の理由とモデル名をログに残し、破損率を計測できるようにした Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013z8ZwcVqMewc7fZx2gAzxF * fix: CodeRabbit 指摘対応(助詞連続の誤検知・README の条件・推論例外の握り) - particle_run が「のでは」「ものには」など正常な日本語を破損と誤検知していた。 誤検知するとトリアージ結果ごと破棄されるため、同一助詞の 3 連続に限定する - 公開リポジトリへの起票条件を README と実装で一致させる(reportType・ トリアージ成否・スパム疑い・対象カテゴリ・信頼度のすべてを明記) - env.AI.run の例外を試行ループ内で捕捉する。JSON Mode を満たせない場合や AI 側の一時障害で throw すると queue の再試行を使い切ってフィードバックが 消えるため、生成失敗として扱い「要約失敗」で起票して原文を残す なお AI_TRIAGE_MODEL の JSON Mode 非対応の指摘は、実機検証で反証済みのため モデルは変更しない(詳細は PR のレビュー返信を参照)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013z8ZwcVqMewc7fZx2gAzxF * fix: 信頼度を 0..1 に制限する モデルが "90"(パーセント表記のつもり)や負値を返した場合、componentConfidence が そのまま閾値判定を通過し、公開リポジトリへ内容を出すべきでないフィードバックが 起票されうる状態だった。 - TRIAGE_JSON_SCHEMA の confidence / componentConfidence に minimum: 0 と maximum: 1 を追加 - coerceReport でも範囲を検証し、範囲外は値を信用せず既定値へ倒す (componentConfidence は 0 = 公開起票しない、confidence は 0.5 = 従来の既定) 制約追加後も gemma-4 が JSON Mode を満たすことは実機で確認済み。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013z8ZwcVqMewc7fZx2gAzxF * fix: 信頼度は数値のみ受け付ける Number() 任せの変換だと componentConfidence: true や [1] が 1 に化け、 component と対象カテゴリが揃うと公開リポジトリへの起票条件を通過してしまう。 数値、または数値だけの文字列に限定し、それ以外は既定値へ倒す。 - boolean・配列・オブジェクト・null・空文字は既定値(componentConfidence は 0) - 範囲検証のテストで category を省いていたため question 扱いで弾かれ、 信頼度の検証に到達していなかった。category: 'bug' を明示して実際に 信頼度のガードを検証するようにした(ガードを外すと落ちることを確認済み) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013z8ZwcVqMewc7fZx2gAzxF * few-shot に functions / website の例を追加し、唯一例の抽出漏れを防ぐ 原因コンポーネントの判定を入れたが、few-shot に functions / website の例が 1 件も無く、この 2 リポジトリへは実質振り分けられない状態だった。両方の例を 追加する。 few-shot は毎回 FEW_SHOT_LIMIT 件をランダム抽出するため、カテゴリや コンポーネント唯一の例(praise / functions / website)は抽出から漏れると モデルがその値を出さなくなる。weight を付けて残りやすくし、あわせて FEW_SHOT_LIMIT を 12 から 16 に引き上げる。README に weight / disabled と FEW_SHOT_LIMIT の関係を追記した。 KV の実データ(dev / production)にも同じ方針で再投入済み。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4r9HW8s5VHfRoFEJN8bAU --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * アプリ指定の読み上げ速度を受け付け既定速度を等速へ変更 (#16) * フィードバックのキューに dead letter queue を設定する (#19) * フィードバックのキューに dead letter queue を設定する consumer は max_retries のみで、使い切ったメッセージの退避先が無かった。 processFeedbackMessage() は queue に retry させるためエラーを再送出するが、 権限やリポジトリ設定のような恒久的な原因だと retry しても回復しないため、 3 回失敗した時点でメッセージごと破棄され、フィードバックが失われていた。 dev / production 両方の consumer に dead_letter_queue を追加する。DLQ 側に consumer は付けない。同じハンドラを回しても同じ理由で落ちるだけで、復旧は 原因を直してから手動で流し直すのが正しい。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc * DLQ の保持期限と再実行手順をドキュメントに追加する CodeRabbit のレビュー指摘への対応。 - DLQ にも保持期限があり、期限切れでメッセージが消えることを明記した。 DLQ を置いただけでは「失わない」保証にならず、再実行には期限がある。 - 再実行用の一時 consumer には別の DLQ を付けるよう明記した。再実行は同じ ハンドラを回すため、DLQ が無いと max_retries 超過でそのまま削除される。 - Setup のリソース作成コマンドが dev 名のみだったので、production 側の 名前について補足した。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc * DLQ の保持期限更新手順を dev 側にも適用する形にする CodeRabbit のレビュー指摘への対応。保持期限はキュー単位の設定で、 feedback-triage-dev-dlq が prod 側の設定を継承することはないため、 コマンド例を両方のキューを回す形に変えた。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc * DLQ 再実行の CLI 手順を README に追加する CodeRabbit のレビュー指摘(outside diff)への対応。復旧手順が散文だけで コマンドが無かったため、dev / production 双方のキュー名・スクリプト名で 実行できる形にした。一時 quarantine DLQ の作成、consumer add での接続、 排出後の consumer remove までを含む。 保持期限の確認については、wrangler 4.103 の `queues info` が retention を 表示しないため、CLI では読み出せない旨を明記した(指摘では `queues info` で確認する手順が求められていたが、実際には出力されない)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc * DLQ 手順の保持期限・バッチサイズ・quarantine の後始末を補う CodeRabbit のレビュー指摘への対応。 - 保持期限を RETENTION 変数にまとめ、free tier では 86400 が上限で 1209600 は弾かれることを明記した。 - quarantine DLQ の作成にも --message-retention-period-secs を渡すように した。指定しないとアカウント既定に落ちる。 - 再実行用 consumer に --batch-size 5 を付けた。既定の 10 は 1 メッセージ あたり 5〜17 秒の推論が入る本ワーカーには 1 起動あたりの負荷が大きい。 - quarantine DLQ は incident 後も残るため、再利用条件と queues delete に よる後始末を追記した。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc * 再実行手順のブロック内で RETENTION を定義する CodeRabbit のレビュー指摘への対応。RETENTION を保持期限の設定ブロック側 だけで定義していたため、再実行手順のブロックだけをコピーして実行すると 空になり、quarantine DLQ に保持期限が渡らなかった。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * DLQ の紐付けを確認する手順を README に追加する (#22) DLQ への移送は Cloudflare 側が内部で行うため、正しく設定されていても `wrangler queues list` 上の DLQ は producer / consumer とも 0 のままになる。 紐付けは移送元キューの consumer 設定に載るので、確認は `wrangler queues consumer list <移送元キュー>` で行う必要がある。 設定は deploy して初めて反映される点も併記した。dead_letter_queue が "-" のままなら、その環境は今も retry 枯渇でメッセージを捨てている。 Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc Co-authored-by: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
) * TTSをAzure SpeechからOpenAI gpt-4o-mini-ttsの女性声へ全面移行 (#2) * TTSの既定ボイスを日英で分けて早口寄りに調整する (#3) * TTSの既定ボイスを日英で分けて早口寄りに調整する 日英とも nova だった既定ボイスを、日本語は shimmer、英語は coral に変更する。 どちらも多言語を読めるが、各言語で最も明瞭に聞こえるボイスを選んだ。 あわせて instructions を明るく張りのある調子へ書き換え、読み上げ速度を 通常より一段速い早口気味に指示する。gpt-4o-mini-tts は speed パラメータが 効かないため、速度指定は instructions で行う必要がある。 日英で別ボイスになったことで実態と食い違ったコメントも更新する。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns9SudVEY9UzSUiaHoDy5s * 読み上げ速度の指示をさらに急ぎ目へ強める 「一段速い早口気味」では十分に速くならなかったため、ラッシュ時の自動放送を 引き合いに出して速さを指示し、句読点・文の切れ目での間と語尾の伸ばしを 明示的に抑える。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns9SudVEY9UzSUiaHoDy5s --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * AIチャットのモデルにGemini(Vertex AI)を追加する (#4) * AIチャットのモデルにGemini(Vertex AI)を追加する AGENT_MODEL に "google:<model>" を追加し、対話本体を Gemini でも動かせるように する。dev は google:gemini-3.7-flash、本番は openai のまま据え置く。 Vertex AI は API キーではなく ADC(サービスアカウント)認証が前提だが Workers に ADC は無いため、鍵 JSON を GOOGLE_VERTEX_SA_KEY で受け取り、JWT 署名と トークン交換は @ai-sdk/google-vertex の edge 版に行わせる(google-auth-library は バンドルに含めない)。project は鍵の project_id を既定とし、location の既定は global。AI Gateway 経由では google-vertex-ai/v1beta1 のモデルパスを baseURL に 組み、直行時と API バージョンを揃える。 思考の抑制は providerOptions を自前で組まず AI SDK 共通の reasoning 設定に委ねる。 ただし 3 系へ 'none'(= thinkingLevel: minimal)を送ると Vertex が 400 "Thinking level is unsupported: THINKING_LEVEL_MINIMAL" を返すため、受理される 最小値の 'low' まで下げる。2.5 系は 'none'(thinkingBudget: 0)で完全に止める。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi * fix: apply CodeRabbit auto-fixes プロバイダ切り替えが「vars のみの変更」で済むのは、そのプロバイダのシークレットを 投入済みの場合に限ることを README に明記する。未投入だと resolveAgentModel が "<SECRET> is not configured" を投げて /agent/chat が全リクエスト失敗するため。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * TTSをOpenAIからGoogle Cloud Text-to-Speechへ全面移行する (#5) * TTSをOpenAIからGoogle Cloud Text-to-Speechへ全面移行する 合成エンジンを OpenAI(gpt-4o-mini-tts) から Cloud Text-to-Speech へ差し替える。 既定ボイスは Firebase Functions 時代の Google TTS 実装と同じ ja-JP-Standard-B / en-US-Standard-G に揃え、Android の端末内蔵 TTS と同水準の音質にする。 Standard / Wavenet / Neural2 以外(Studio・Chirp3-HD・Gemini-TTS)は単価が 桁違いのためクライアントから名指しできないようにする。 認証は API キーではなくサービスアカウント(GOOGLE_TTS_SA_KEY)で、既存の getGoogleAccessToken を再利用する。用途ごとに別の鍵を使うようになるため、 トークンキャッシュのキーを scope 単体からサービスアカウント × scope に変える。 Cloud TTS は AI Gateway の対応プロバイダではないので Google へ直行する。 Standard 系には読み方のプロンプト指示が無いため TTS_INSTRUCTIONS_* は廃止し、 速さは TTS_SPEED(speakingRate)、高さは TTS_PITCH に置き換える。リクエストの model / instructions* は受け取っても無視するため、旧アプリからの呼び出しは そのまま通る。エンジン差し替えで同じ入力でも音声が変わるので、キャッシュキーの 版を 13 から 14 へ上げて旧キャッシュとは分離する。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi * 鍵ファイルが空のときに明示的なエラーにする <NAME>_FILE で指定した鍵 JSON が空でも、これまでは値なしとして黙って捨てられ、 「投入対象のシークレットがありません」としか出ないため原因を追いにくかった。 失敗した `gcloud iam service-accounts keys create` は出力先ファイルを空のまま 残すため、この事故は実際に起こる。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi * fix: apply CodeRabbit auto-fixes ボイス名の検証を形式一致から実在ボイスの allowlist へ変更する。 形式だけを見ていたため ja-US-Standard-A や ja-JP-Standard-Z のような実在しない 名前が検証を通過し、Cloud TTS が 400 "Voice ... does not exist" を返して /tts 全体が失敗していた(未知の名前は既定値へ倒す、というこのモジュールの前提が 成り立っていなかった)。allowlist は voices.list で実在を確認した ja-JP 11 件・ en-US 29 件。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4jE3NdeZJ5Ny7Zjue9tzi --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * 本番の AGENT_MODEL を Gemini(Vertex AI)へ切り替える (#6) * staging の駅検索を sapi-bff-stg から stationapi-stg へ切り替える (#7) * staging の駅検索を sapi-bff-stg から stationapi-stg へ切り替える BFF 廃止に伴い、staging の Service Binding を stationapi-stg へ移す。 GraphQL のクエリとレスポンス構造は sapi-bff と同一(stationsByName / stationGroupStations とも引数・フィールド名が変わらない)ため、 クエリ側の処理には手を入れていない。 本番の route 移管はまだ済んでいないので、production は SAPI_BFF → sapi-bff のまま残し、STATION_API を優先して 未設定なら SAPI_BFF へ落ちる順序にした。 なお stationapi は GraphQL をサブドメイン直下(POST /)で受けるため、 STATION_API 経由の POST 先は /graphql ではなくルート直下にしている。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PM76omHQmBawZ3EfBsuU7z * STATION_API 経由のテストで POST であることも検証する CodeRabbit の指摘(PR #7)。テスト名は「サブドメイン直下へ POST する」 なのに URL しか見ておらず、method はスイート全体でも未検証だった。 リクエスト本文の variables は既存テストがカバー済みのため触っていない。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PM76omHQmBawZ3EfBsuU7z --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * 本番の駅検索を sapi-bff から stationapi へ切り替える (#9) * 本番の駅検索を sapi-bff から stationapi へ切り替える BFF 廃止に伴い、production の Service Binding を SAPI_BFF → sapi-bff から STATION_API → stationapi へ移す。staging(#7)と揃い、両環境とも stationapi を直に叩く構成になったため、SAPI_BFF への分岐は不要になった。 postGraphQL から SAPI_BFF 分岐を落とし、Service Binding 不使用時の フォールバック環境変数も SAPI_BFF_GRAPHQL_URL から STATION_API_GRAPHQL_URL へ改名する(wrangler.jsonc では未設定のため リネームによる実害はない)。 分岐が消えたことで「STATION_API があれば優先し、サブドメイン直下へ POST する」テストは意味を失うので削除するが、stationapi が GraphQL を POST / でのみ受けるという前提の検証は残す必要があるため、POST 先 URL と method のアサーションは既存の Service Binding テストへ畳み込んでいる。 なお Service Binding は対象 Worker が存在しないとデプロイに失敗するため、 stationapi(production)のデプロイ完了後にこの変更をデプロイし、 BFF の削除はその後に行うこと。 Closes #8 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sXt1cqwoy71GakDQMGC3G * fix: apply CodeRabbit auto-fixes STATION_API と STATION_API_GRAPHQL_URL の優先順位を回帰テストで固定する。 SAPI_BFF 分岐の削除に伴い「STATION_API があれば優先」テストを落としたが、 残る 2 分岐(Service Binding / URL フォールバック)の順序を確かめる テストが無くなっていた。既存テストは片方だけを設定するため、postGraphQL の 分岐順を入れ替えても全て通ってしまう。 グローバル fetch はスパイに実装を持たせ、分岐順が壊れた場合でも実 ネットワークへ出ないようにする(jest.config.js に restoreMocks が無いため finally で明示的に戻す)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sXt1cqwoy71GakDQMGC3G --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * フィードバックを原因リポジトリへ振り分け、トリアージの起票品質を改善する (#14) * フィードバックを原因リポジトリへ振り分け、トリアージの起票品質を改善する 原因が特定できたフィードバックを該当する公開リポジトリにも起票できるようにし、 あわせて Issue #11 で報告された Spam 誤判定とタイトル破損を修正する。 ## 公開リポジトリへの振り分け - トリアージに原因コンポーネントの判定(component / componentConfidence)を追加 - component が特定でき信頼度 0.7 以上なら、TrainLCD/MobileApp・StationAPI・ Functions・Website のいずれかにスタブ Issue を起票する - 公開リポジトリなのでフィードバックの内容は一切載せず、管理 Issue 番号と チケットID だけを持たせる。管理 Issue 側にもコメントで相互リンクを残す - クラッシュ・スパム・スパム疑い・質問・称賛・トリアージ失敗は振り分けない - 追跡のため、管理 Issue の本文にもチケットID を追加 ## Spam 誤判定の修正(不具合A) - looksLikeSpam の「停車駅/方面」「駅名・路線名の併記」への単独加点を撤回し、 放送定型句との共起を必須にした。本アプリのドメイン語彙そのものであり、 正確な報告ほどスパム判定される構造だったため - 「違います」「反映されない」「ほしい」など、報告で頻出する言い回しを追加 - モデルの非スパム判定をヒューリスティックが無条件に上書きするのをやめ、 モデルの confidence が高い場合は分類を維持して ❓ Unknown Type で人手確認に回す - 感謝・称賛を 💩 Spam に分類していたのをやめ、praise カテゴリ(💚 Praise)を新設 ## タイトル破損の修正(不具合B) - AI_TRIAGE_MODEL を @cf/google/gemma-4-26b-a4b-it に変更(日本語生成品質) - gemma-4 は response を返さず choices[0].message.content のみのため、 両形式を受ける pickModelResponse を追加。従来の取り出し方だと全件失敗する - 推論トレースで JSON が途中で切れる(finish_reason: length)ため max_tokens を 2048 に - category / triageLevel / component を JSON Schema の required に追加。 optional だったため、モデルが省略した非スパムが軒並み question に落ちていた - 未取得・文字化け・生成ループ・助詞連続などの破損タイトルを検知し、 要約失敗マーカー付きで起票する。破損タイトルを要約へ伝播させない - 破損の理由とモデル名をログに残し、破損率を計測できるようにした Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013z8ZwcVqMewc7fZx2gAzxF * fix: CodeRabbit 指摘対応(助詞連続の誤検知・README の条件・推論例外の握り) - particle_run が「のでは」「ものには」など正常な日本語を破損と誤検知していた。 誤検知するとトリアージ結果ごと破棄されるため、同一助詞の 3 連続に限定する - 公開リポジトリへの起票条件を README と実装で一致させる(reportType・ トリアージ成否・スパム疑い・対象カテゴリ・信頼度のすべてを明記) - env.AI.run の例外を試行ループ内で捕捉する。JSON Mode を満たせない場合や AI 側の一時障害で throw すると queue の再試行を使い切ってフィードバックが 消えるため、生成失敗として扱い「要約失敗」で起票して原文を残す なお AI_TRIAGE_MODEL の JSON Mode 非対応の指摘は、実機検証で反証済みのため モデルは変更しない(詳細は PR のレビュー返信を参照)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013z8ZwcVqMewc7fZx2gAzxF * fix: 信頼度を 0..1 に制限する モデルが "90"(パーセント表記のつもり)や負値を返した場合、componentConfidence が そのまま閾値判定を通過し、公開リポジトリへ内容を出すべきでないフィードバックが 起票されうる状態だった。 - TRIAGE_JSON_SCHEMA の confidence / componentConfidence に minimum: 0 と maximum: 1 を追加 - coerceReport でも範囲を検証し、範囲外は値を信用せず既定値へ倒す (componentConfidence は 0 = 公開起票しない、confidence は 0.5 = 従来の既定) 制約追加後も gemma-4 が JSON Mode を満たすことは実機で確認済み。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013z8ZwcVqMewc7fZx2gAzxF * fix: 信頼度は数値のみ受け付ける Number() 任せの変換だと componentConfidence: true や [1] が 1 に化け、 component と対象カテゴリが揃うと公開リポジトリへの起票条件を通過してしまう。 数値、または数値だけの文字列に限定し、それ以外は既定値へ倒す。 - boolean・配列・オブジェクト・null・空文字は既定値(componentConfidence は 0) - 範囲検証のテストで category を省いていたため question 扱いで弾かれ、 信頼度の検証に到達していなかった。category: 'bug' を明示して実際に 信頼度のガードを検証するようにした(ガードを外すと落ちることを確認済み) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013z8ZwcVqMewc7fZx2gAzxF * few-shot に functions / website の例を追加し、唯一例の抽出漏れを防ぐ 原因コンポーネントの判定を入れたが、few-shot に functions / website の例が 1 件も無く、この 2 リポジトリへは実質振り分けられない状態だった。両方の例を 追加する。 few-shot は毎回 FEW_SHOT_LIMIT 件をランダム抽出するため、カテゴリや コンポーネント唯一の例(praise / functions / website)は抽出から漏れると モデルがその値を出さなくなる。weight を付けて残りやすくし、あわせて FEW_SHOT_LIMIT を 12 から 16 に引き上げる。README に weight / disabled と FEW_SHOT_LIMIT の関係を追記した。 KV の実データ(dev / production)にも同じ方針で再投入済み。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4r9HW8s5VHfRoFEJN8bAU --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * アプリ指定の読み上げ速度を受け付け既定速度を等速へ変更 (#16) * フィードバックのキューに dead letter queue を設定する (#19) * フィードバックのキューに dead letter queue を設定する consumer は max_retries のみで、使い切ったメッセージの退避先が無かった。 processFeedbackMessage() は queue に retry させるためエラーを再送出するが、 権限やリポジトリ設定のような恒久的な原因だと retry しても回復しないため、 3 回失敗した時点でメッセージごと破棄され、フィードバックが失われていた。 dev / production 両方の consumer に dead_letter_queue を追加する。DLQ 側に consumer は付けない。同じハンドラを回しても同じ理由で落ちるだけで、復旧は 原因を直してから手動で流し直すのが正しい。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc * DLQ の保持期限と再実行手順をドキュメントに追加する CodeRabbit のレビュー指摘への対応。 - DLQ にも保持期限があり、期限切れでメッセージが消えることを明記した。 DLQ を置いただけでは「失わない」保証にならず、再実行には期限がある。 - 再実行用の一時 consumer には別の DLQ を付けるよう明記した。再実行は同じ ハンドラを回すため、DLQ が無いと max_retries 超過でそのまま削除される。 - Setup のリソース作成コマンドが dev 名のみだったので、production 側の 名前について補足した。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc * DLQ の保持期限更新手順を dev 側にも適用する形にする CodeRabbit のレビュー指摘への対応。保持期限はキュー単位の設定で、 feedback-triage-dev-dlq が prod 側の設定を継承することはないため、 コマンド例を両方のキューを回す形に変えた。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc * DLQ 再実行の CLI 手順を README に追加する CodeRabbit のレビュー指摘(outside diff)への対応。復旧手順が散文だけで コマンドが無かったため、dev / production 双方のキュー名・スクリプト名で 実行できる形にした。一時 quarantine DLQ の作成、consumer add での接続、 排出後の consumer remove までを含む。 保持期限の確認については、wrangler 4.103 の `queues info` が retention を 表示しないため、CLI では読み出せない旨を明記した(指摘では `queues info` で確認する手順が求められていたが、実際には出力されない)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc * DLQ 手順の保持期限・バッチサイズ・quarantine の後始末を補う CodeRabbit のレビュー指摘への対応。 - 保持期限を RETENTION 変数にまとめ、free tier では 86400 が上限で 1209600 は弾かれることを明記した。 - quarantine DLQ の作成にも --message-retention-period-secs を渡すように した。指定しないとアカウント既定に落ちる。 - 再実行用 consumer に --batch-size 5 を付けた。既定の 10 は 1 メッセージ あたり 5〜17 秒の推論が入る本ワーカーには 1 起動あたりの負荷が大きい。 - quarantine DLQ は incident 後も残るため、再利用条件と queues delete に よる後始末を追記した。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc * 再実行手順のブロック内で RETENTION を定義する CodeRabbit のレビュー指摘への対応。RETENTION を保持期限の設定ブロック側 だけで定義していたため、再実行手順のブロックだけをコピーして実行すると 空になり、quarantine DLQ に保持期限が渡らなかった。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * DLQ の紐付けを確認する手順を README に追加する (#22) DLQ への移送は Cloudflare 側が内部で行うため、正しく設定されていても `wrangler queues list` 上の DLQ は producer / consumer とも 0 のままになる。 紐付けは移送元キューの consumer 設定に載るので、確認は `wrangler queues consumer list <移送元キュー>` で行う必要がある。 設定は deploy して初めて反映される点も併記した。dead_letter_queue が "-" のままなら、その環境は今も retry 枯渇でメッセージを捨てている。 Claude-Session: https://claude.ai/code/session_018n3eJcHFb6E2BDzPCcwKZc Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * フィードバック再試行での Issue 重複起票を防ぐ (#23) * AI エージェントのモデルを Gemini 3.8 Flash へ更新する (#24) * 英語TTSで「Keisei」が「かいせい」と読まれる誤読を合成前の表記置換で修正する (#25) * GitHub Actions で dev・master からのデプロイを自動化する (#26) * GitHub Actions で dev・master からのデプロイを自動化する 検証 (lint / typecheck / test / wrangler の dry-run) を composite action へ 切り出し、CI とデプロイ 2 環境で同じ手順を踏ませる。デプロイ先はブランチを 式で判定せず、ファイルとトリガで固定する。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EWZaSGKrw2MDZy5SYhuGiY * デプロイ workflow の変更でも ci.yml が起動するようにする deploy_dev.yml / deploy_production.yml はどちらも pull_request では起動せず、 ci.yml の paths にも載っていなかったため、この 2 ファイルだけを変更した PR が どの workflow も通らないままマージできてしまう状態だった。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EWZaSGKrw2MDZy5SYhuGiY --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * 英語TTSで「Seibu」が「さいぶ」と読まれる誤読を合成前の表記置換で修正する (#27) Claude-Session: https://claude.ai/code/session_01WS1LSBnYTMfm7CyrzqKxuW Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
リリース PR の squash merge により master 側にのみ存在していた AGENTS.md を dev へ取り込み、両ブランチの内容差分を解消する。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0114YsLLd1ZAHM1u6NTrRMbX
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Essentials Run ID: 📒 Files selected for processing (1)
Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour. 📝 WalkthroughWalkthrough
Changes開発ガイド
Priority: ⬇️ Low Estimated code review effort: 1 (Trivial) | ~3 minutes Change: Other Merge Risk: ⚪ Minimal · up to This change only synchronizes repository guidance and does not affect source code or runtime behavior. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
うさぎがガイドをひらきます Comment |
背景
devとmasterの内容差分を確認したところ、リリース対象の未反映コミットは無く、差分はAGENTS.mdがdevに存在しない 一点のみでした。これは
feature/repository-guidelinesがmasterへ直接マージされ(0d1cb96)、devへ取り込まれていないためです。この状態ではdev→masterのリリース PR がAGENTS.mdの削除を含んでしまうため、先にmasterをdevへ取り込んで同期します。変更内容
masterをdevへマージし、AGENTS.mdをdevに取り込むAGENTS.mdのみ確認したこと
git diff --stat origin/master HEAD→ 差分なし(masterと内容一致)git diff --stat origin/dev HEAD→AGENTS.mdの追加 33 行のみソースコードの変更を含まないドキュメントのみの同期のため、ビルド・テストの挙動には影響しません。
影響範囲
binding / secret / queue / KV / R2 / Cron の変更はありません。
🤖 Generated with Claude Code
https://claude.ai/code/session_0114YsLLd1ZAHM1u6NTrRMbX
Summary by CodeRabbit