Skip to content

feat(desktop): Aivis API による音声読み上げ通知 (#286)#287

Merged
MocA-Love merged 5 commits intomainfrom
feat/aivis-notification-286
Apr 17, 2026
Merged

feat(desktop): Aivis API による音声読み上げ通知 (#286)#287
MocA-Love merged 5 commits intomainfrom
feat/aivis-notification-286

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

@MocA-Love MocA-Love commented Apr 17, 2026

Closes #286

概要

Settings > Notifications に Aivis API による音声読み上げ機能を追加。エージェント動作完了・許可要求時に通知音の後、Aivis の音声合成で「ワークスペース、{{branch}}、です」のようにアナウンスする。さらに、同じ Aivis アカウント上のユーザー辞書管理と日別使用量ダッシュボードも同画面内に統合。

音声通知 (ベース機能)

  • DB: settingsaivisEnabled / aivisApiKey / aivisModelUuid / aivisFormat / aivisFormatPermission を追加
  • Main: apps/desktop/src/main/lib/notifications/aivis-tts.tsPOST /v1/tts/synthesize (Bearer, mp3) を叩き、通知音終了後に再生 (onComplete チェーン)
  • TRPC settings: getAivisSettings / setAivisSettings / testAivisPlayback
  • UI (AivisSettings): On/Off、API Key、Model UUID、完了/許可要求の 2 種フォーマット、プレースホルダ挿入チップ、テスト再生
  • プレースホルダ: {{branch}} / {{workspace}} / {{worktree}} / {{tab}} / {{pane}} / {{event}}。未定義は空文字。
  • デフォルト: 完了 ワークスペース、{{workspace}}、です / 許可要求 {{branch}}で対応が必要です

ユーザー辞書 (追加)

  • DB: settings.aivis_user_dictionary_uuid を追加 (migration 0061)
  • Main: main/lib/aivis/client.ts — Bearer 認証 + エラー型付きの共通 fetch ラッパー (AivisApiKeyMissingError / AivisApiError)
  • TRPC aivis ルーター: dictionary.list/get/create/update/delete/import/exportusage.daily/mevalidateKey
  • 合成: user_dictionary_uuid を TTS リクエストに付与 (辞書選択中のみ)
  • UI (AivisDictionary)
    • 辞書一覧カード (アクティブマーカー、説明、単語数、更新日)
    • インライン編集ダイアログ: surface / 読み (カタカナ検証) / アクセント型 / 優先度 0-10 / 品詞
    • 新規作成ダイアログ、削除確認、適用切替
    • AivisSpeech 互換 JSON の import / export
  • UI (AivisSettings): Model UUID 直下に辞書セレクタを追加

日別使用量ダッシュボード (追加)

  • TRPC: aivis.usage.daily(startDate, endDate) — Aivis /v1/payment/usage-summaries (時間単位) を日別集計 + API キー別に内訳を添付
  • UI (AivisUsage)
    • 7 日 / 30 日の期間タブ
    • 統計カード: Requests / Characters / Credits (残高も併記)
    • 日別バーグラフ (Credits/Requests/Chars トグル、ホバーツールチップ)
    • 日別テーブル (直近 10 日表示)
    • API キー別内訳 (複数キー保有時のみ)
    • 5 分キャッシュ

動作確認

  • Aivis API キーを設定するとバッジ/残高が出る
  • テスト再生で音声が鳴る (両フォーマット)
  • エージェント完了 → 通知音 → 音声の順で鳴る
  • 辞書の作成 / 編集 / 削除 / import / export
  • 辞書選択中は TTS に読み方が反映される
  • 使用量ダッシュボードが期間切替で更新される

- Settings > Notifications に Aivis 設定セクションを追加
- API Key / Model UUID / On-Off / 完了・許可要求別フォーマットを保存
- プレースホルダ ({{branch}} 等) をクリックで挿入できる UI
- エージェント完了・許可要求時に通知音後 Aivis API で音声合成して再生
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Warning

Rate limit exceeded

@MocA-Love has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 38 minutes and 52 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 38 minutes and 52 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f82394f4-9981-4270-a870-d57284bb60fa

📥 Commits

Reviewing files that changed from the base of the PR and between b517b98 and d840db7.

📒 Files selected for processing (4)
  • apps/desktop/plans/20260418-aivis-dictionary-and-usage.md
  • packages/local-db/drizzle/0060_add_aivis_settings.sql
  • packages/local-db/drizzle/meta/0060_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
📝 Walkthrough

Walkthrough

Aivis APIを統合したテキスト音声合成機能を追加します。通知音再生後に、ワークスペース・ブランチ名などを音声で報告するシステムを実装し、TRPC手続き、データベーススキーマ、UIコンポーネント、テンプレート変数レンダリングを含みます。

Changes

Cohort / File(s) Summary
Database Schema
packages/local-db/drizzle/0059_add_aivis_settings.sql, packages/local-db/drizzle/meta/0059_snapshot.json, packages/local-db/drizzle/meta/_journal.json, packages/local-db/src/schema/schema.ts
Aivis設定用に5つの列(aivis_enabledaivis_api_keyaivis_model_uuidaivis_formataivis_format_permission)をsettingsテーブルに追加。Drizzleスキーマスナップショットとマイグレーションメタデータを更新。
TRPC Settings Router
apps/desktop/src/lib/trpc/routers/settings/index.ts
3つのTRPC手続きを追加:getAivisSettings(設定値取得)、setAivisSettings(設定値更新)、testAivisPlayback(再生テスト、動的インポートと実行)。
Aivis TTS Core Module
apps/desktop/src/main/lib/notifications/aivis-tts.ts
Aivis TTS統合実装。テンプレートレンダリング(renderAivisTemplate)、音声合成(playAivisTts)、通知再生(playAivisNotification)機能を提供。プレースホルダ変数(branch、workspace、worktree、tab、pane、event)対応。
Notification System Integration
apps/desktop/src/main/lib/notification-sound.ts, apps/desktop/src/main/lib/notifications/notification-manager.ts
通知音再生に完了コールバック対応を追加。NotificationManagerにplayAivis依存を追加し、音声再生後にAivis再生を順序立てて実行。
Main Window Initialization
apps/desktop/src/main/windows/main.ts
ワークスペース情報取得ロジックをgetWorkspaceRecordsに分離。Aivis変数構築関数buildAivisVarsを追加し、通知イベントからブランチ・ワークスペース・ワーキングツリー・タブ・ペーン情報を抽出。
Settings UI Component
apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/AivisSettings/AivisSettings.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/AivisSettings/index.ts
Aivis音声報告設定UIコンポーネント。プレースホルダ挿入ボタン、テンプレート編集テキストエリア、テスト再生機能を実装。キャッシュ無効化対応。
Settings Page & Configuration
apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/page.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
Aivisコンポーネントをリングトーンページに統合。設定項目IDRINGTONES_AIVISを追加し、検索・発見可能性を構成。

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant NotifMgr as NotificationManager
    participant Sound as Notification Sound
    participant Aivis as Aivis TTS
    participant AivisAPI as Aivis API
    participant Speaker as Speaker

    User->>NotifMgr: Lifecycle Event<br/>(task complete)
    activate NotifMgr
    NotifMgr->>Sound: playSound(onComplete)
    activate Sound
    Sound->>Speaker: Play ringtone
    activate Speaker
    Speaker-->>Sound: Complete
    deactivate Speaker
    Sound->>NotifMgr: onComplete()
    deactivate Sound
    
    NotifMgr->>Aivis: playAivisNotification(event, vars)
    activate Aivis
    Note over Aivis: Load settings, select format
    Note over Aivis: renderAivisTemplate<br/>(format, vars)
    Aivis->>AivisAPI: POST /synthesize<br/>(text, modelUuid)
    activate AivisAPI
    AivisAPI-->>Aivis: MP3 Audio
    deactivate AivisAPI
    Aivis->>Speaker: playAudioFile(mp3)
    activate Speaker
    Speaker-->>Aivis: Complete
    deactivate Speaker
    Note over Aivis: Delete temp file
    deactivate Aivis
    deactivate NotifMgr
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Aivis の声よ、ひびけ!
ブランチ名をね、読み上げよ
通知のあとで、音声で知る
ワークスペースが、どれなのやら
テンプレート変数で自由自在 ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed タイトルは PR の主な変更内容(Aivis API による音声読み上げ通知機能の追加)を明確に要約している。
Description check ✅ Passed PR の説明は、関連 Issue、概要、変更点、プレースホルダ仕様、デフォルトフォーマット、動作確認チェックリストを含み、テンプレートの要件をほぼ満たしている。
Linked Issues check ✅ Passed 変更内容は Issue #286 の要件をすべて実装している:Aivis 設定項目の追加、通知音後の音声合成読み上げ、API Key / Model UUID / On-Off トグル / フォーマット設定、プレースホルダ対応。
Out of Scope Changes check ✅ Passed すべての変更は Issue #286 の Aivis API 音声読み上げ機能実装に関連しており、スコープ外の変更は認められない。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/aivis-notification-286

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 471d7f92d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/main/lib/notifications/notification-manager.ts Outdated
- P1: AivisSettings の useEffect が refetch で編集中の値を上書きするのを防止
  (hydratedRef で初回のみハイドレートする)
- P2: Aivis 音声がリングトーンと並列再生される問題を修正
  (playNotificationSound に onComplete を追加し、通知音終了後に TTS を鳴らす)
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
apps/desktop/src/main/lib/notifications/aivis-tts.ts (1)

110-129: playSoundFileonComplete が呼ばれない場合の一時ファイルリーク

playSoundFile のコールバック契約に依存しており、再生側が onComplete を呼ばずに終了したり、コールバック前にプロセスがクラッシュすると %TMP%/superset-aivis-*.mp3 が残り続けます。起動時にアプリ自身が古い superset-aivis-*.mp3 をスイープする、もしくは一定時間後の fallback unlink を仕掛けると堅牢になります。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/notifications/aivis-tts.ts` around lines 110 - 129,
playAivisTts currently relies solely on playSoundFile's onComplete to call
cleanup(path), which can leak temp files if the callback never fires; add a
robust fallback by (1) implementing a startup sweep that deletes existing
superset-aivis-*.mp3 temp files and (2) adding a timed fallback unlink when
creating each temp file in playAivisTts: after writing to uniqueTmpPath(),
schedule a timeout that calls cleanup(path) (and cancels the timeout when
playSoundFile's onComplete runs); reference synthesize, writeFile,
uniqueTmpPath, playSoundFile, onComplete, and cleanup when locating where to add
the startup sweep and per-file timeout cancellation logic.
apps/desktop/src/lib/trpc/routers/settings/index.ts (1)

1049-1052: デフォルトフォーマット文字列の二重管理

同じ既定値 "ワークスペース、{{workspace}}、です""{{branch}}で対応が必要です"aivis-tts.tsreadAivisSettings() および UI 側のプレースホルダにも登場しています。共通定数として @superset/local-db もしくは shared/ から export し、各参照箇所で同じ値を使うようにするとズレを防げます。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/settings/index.ts` around lines 1049 -
1052, 同じ既定値が重複管理されているので、"ワークスペース、{{workspace}}、です" と "{{branch}}で対応が必要です"
を共通定数(例: DEFAULT_AIVIS_FORMAT, DEFAULT_AIVIS_FORMAT_PERMISSION)として
`@superset/local-db` か shared/
にエクスポートし、readAivisSettings()(apps/desktop/src/lib/trpc/routers/settings/index.ts
内の format/formatPermission 設定)と aivis-tts.ts および UI
側のプレースホルダ参照をすべてその定数をインポートして使うように置き換えてください(既存のハードコーディングを削除し、単一ソースで値を管理)。
apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/AivisSettings/AivisSettings.tsx (1)

142-164: API Key のハンドリングと onBlur コミット

  1. apiKey / modelUuidonBlur では値が未変更でも毎回 save.mutate が走ります。動作上問題はないものの、refetch が連鎖するので「値が変わった時のみ mutate」にすると無駄が減ります。
  2. API Key は type="password" で入力 UI 上は伏せられていますが、ローカル DB には平文保存されます(根本コメントは setAivisSettings 側に記載)。
♻️ onBlur 最適化の例
-				onBlur={() => commit({ apiKey })}
+				onBlur={() => {
+					if (data?.apiKey !== apiKey) commit({ apiKey });
+				}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/AivisSettings/AivisSettings.tsx`
around lines 142 - 164, OnBlur handlers for apiKey and modelUuid are triggering
save.mutate every time even when the value hasn't changed; update the onBlur
logic in the Input components (the handlers that call commit({ apiKey }) and
commit({ modelUuid })) to first compare the current local value
(apiKey/modelUuid, set via setApiKey/setModelUuid) against the persisted value
you already have (e.g., the incoming prop or state holding
savedAivisSettings.apiKey / savedAivisSettings.modelUuid or similar) and only
call commit/save.mutate when the value is different (and optionally non-empty);
keep using the same identifiers (apiKey, modelUuid, setApiKey, setModelUuid,
commit, save.mutate) so reviewers can locate and apply the conditional check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/lib/trpc/routers/settings/index.ts`:
- Around line 1096-1114: testAivisPlayback currently allows empty
apiKey/modelUuid and permits concurrent calls; update the input schema in the
testAivisPlayback publicProcedure to require non-empty strings (add .min(1) to
apiKey and modelUuid) and add a server-side concurrency guard inside the
mutation (before calling playAivisTts) to prevent parallel executions—e.g., use
a short-lived in-memory lock or Map keyed by user/session/apiKey to allow only
one active request per key and debounce/TTL to release the lock on completion or
error; ensure the lock check returns a clear error when a request is already in
progress and always releases the lock after playAivisTts finishes or throws.

In `@apps/desktop/src/main/lib/notifications/aivis-tts.ts`:
- Around line 67-90: The fetch call to AIVIS_ENDPOINT (using apiKey, modelUuid,
text) lacks a timeout: create an AbortController, pass controller.signal into
the fetch options, start a timer (e.g., 20s) that calls controller.abort() on
timeout, and clear the timer after receiving a response or on error; ensure the
error path handles AbortError appropriately so the function that returns
Buffer.from(arrayBuffer) won’t hang and resources (timers/FDs) are cleaned up.
- Around line 41-60: readAivisSettings currently returns the Aivis API key in
plaintext; update readAivisSettings and the corresponding setAivisSettings TRPC
handler to use Electron safeStorage: when saving, encrypt the API key with
safeStorage.encryptString(value).toString("base64") (follow extension-context
pattern) and when reading, if row?.aivisApiKey is present call
safeStorage.decryptString(Buffer.from(row.aivisApiKey, "base64")) to obtain the
plaintext key; additionally implement a migration in the set/read path that
detects unencrypted keys (e.g., not valid base64/encrypted format) and re-saves
them encrypted using the same methods so existing user data is migrated
transparently while preserving other fields returned by readAivisSettings.
- Around line 99-103: The cleanup() helper currently calls execFile("rm", ...)
which fails on Windows and leaks temp MP3s — replace that with
fs.promises.unlink(path) (or fs.unlinkSync if synchronous behavior is required),
await or handle the returned Promise and swallow only ENOENT while logging other
errors; keep the function name cleanup and ensure callers still work with its
signature. Also add a timeout to synthesize(): create an AbortController, start
a setTimeout that calls controller.abort() after a sensible timeout (e.g. 30s),
pass controller.signal into the fetch call in synthesize(), clear the timer on
success, and handle AbortError/rejections to avoid hanging promises and memory
leaks.

In `@apps/desktop/src/main/windows/main.ts`:
- Around line 84-89: worktreeName is set to worktree?.branch so {{worktree}}
always equals {{branch}}; change it to a distinct identifier (e.g., the basename
of worktree.path) and fall back to branch/empty when missing. Update the return
block that builds branch/workspace/worktree: set worktreeName = worktree ?
path.basename(worktree.path || worktree.name || "") : branch || ""; ensure you
import Node's path module (or use an existing basename helper) and keep the same
fallback semantics for workspace and branch.

---

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/settings/index.ts`:
- Around line 1049-1052: 同じ既定値が重複管理されているので、"ワークスペース、{{workspace}}、です" と
"{{branch}}で対応が必要です" を共通定数(例: DEFAULT_AIVIS_FORMAT,
DEFAULT_AIVIS_FORMAT_PERMISSION)として `@superset/local-db` か shared/
にエクスポートし、readAivisSettings()(apps/desktop/src/lib/trpc/routers/settings/index.ts
内の format/formatPermission 設定)と aivis-tts.ts および UI
側のプレースホルダ参照をすべてその定数をインポートして使うように置き換えてください(既存のハードコーディングを削除し、単一ソースで値を管理)。

In `@apps/desktop/src/main/lib/notifications/aivis-tts.ts`:
- Around line 110-129: playAivisTts currently relies solely on playSoundFile's
onComplete to call cleanup(path), which can leak temp files if the callback
never fires; add a robust fallback by (1) implementing a startup sweep that
deletes existing superset-aivis-*.mp3 temp files and (2) adding a timed fallback
unlink when creating each temp file in playAivisTts: after writing to
uniqueTmpPath(), schedule a timeout that calls cleanup(path) (and cancels the
timeout when playSoundFile's onComplete runs); reference synthesize, writeFile,
uniqueTmpPath, playSoundFile, onComplete, and cleanup when locating where to add
the startup sweep and per-file timeout cancellation logic.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/AivisSettings/AivisSettings.tsx`:
- Around line 142-164: OnBlur handlers for apiKey and modelUuid are triggering
save.mutate every time even when the value hasn't changed; update the onBlur
logic in the Input components (the handlers that call commit({ apiKey }) and
commit({ modelUuid })) to first compare the current local value
(apiKey/modelUuid, set via setApiKey/setModelUuid) against the persisted value
you already have (e.g., the incoming prop or state holding
savedAivisSettings.apiKey / savedAivisSettings.modelUuid or similar) and only
call commit/save.mutate when the value is different (and optionally non-empty);
keep using the same identifiers (apiKey, modelUuid, setApiKey, setModelUuid,
commit, save.mutate) so reviewers can locate and apply the conditional check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 492c18c6-9646-4e61-b79d-ecf318854c48

📥 Commits

Reviewing files that changed from the base of the PR and between a92a8ee and b517b98.

📒 Files selected for processing (13)
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/main/lib/notification-sound.ts
  • apps/desktop/src/main/lib/notifications/aivis-tts.ts
  • apps/desktop/src/main/lib/notifications/notification-manager.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/AivisSettings/AivisSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/AivisSettings/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
  • packages/local-db/drizzle/0059_add_aivis_settings.sql
  • packages/local-db/drizzle/meta/0059_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/src/schema/schema.ts

Comment on lines +1096 to +1114
testAivisPlayback: publicProcedure
.input(
z.object({
apiKey: z.string(),
modelUuid: z.string(),
text: z.string().min(1).max(3000),
}),
)
.mutation(async ({ input }) => {
const { playAivisTts } = await import(
"main/lib/notifications/aivis-tts"
);
await playAivisTts({
apiKey: input.apiKey,
modelUuid: input.modelUuid,
text: input.text,
});
return { success: true };
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

testAivisPlayback の入力バリデーション強化と多重実行ガード

2 点指摘します。

  1. apiKey / modelUuidmin(1) が無いため空文字列が通り、playAivisTts 内で throw される形になっています。Zod レベルで弾いたほうが TRPC エラーとして素直に扱えます。
  2. publicProcedure として外部 HTTP(課金あり)を叩くエンドポイントです。UI のボタンは testPlay.isPending でガードされていますが、別経路やボタン連打時に同時リクエストが走り得ます。メインプロセス側で「同時 1 件まで / 短時間デバウンス」等のガードを入れるとより安全です。
🛡️ 入力バリデーションの修正例
 		testAivisPlayback: publicProcedure
 			.input(
 				z.object({
-					apiKey: z.string(),
-					modelUuid: z.string(),
+					apiKey: z.string().min(1),
+					modelUuid: z.string().min(1),
 					text: z.string().min(1).max(3000),
 				}),
 			)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/settings/index.ts` around lines 1096 -
1114, testAivisPlayback currently allows empty apiKey/modelUuid and permits
concurrent calls; update the input schema in the testAivisPlayback
publicProcedure to require non-empty strings (add .min(1) to apiKey and
modelUuid) and add a server-side concurrency guard inside the mutation (before
calling playAivisTts) to prevent parallel executions—e.g., use a short-lived
in-memory lock or Map keyed by user/session/apiKey to allow only one active
request per key and debounce/TTL to release the lock on completion or error;
ensure the lock check returns a clear error when a request is already in
progress and always releases the lock after playAivisTts finishes or throws.

Comment on lines +41 to +60
function readAivisSettings() {
try {
const row = localDb.select().from(settings).get();
return {
enabled: row?.aivisEnabled ?? false,
apiKey: row?.aivisApiKey ?? "",
modelUuid: row?.aivisModelUuid ?? "",
format: row?.aivisFormat ?? "ワークスペース、{{workspace}}、です",
formatPermission:
row?.aivisFormatPermission ?? "{{branch}}で対応が必要です",
volume:
typeof row?.notificationVolume === "number" &&
Number.isFinite(row.notificationVolume)
? Math.max(0, Math.min(100, row.notificationVolume))
: 100,
};
} catch {
return null;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if safeStorage is already used elsewhere in the app for secrets.
rg -nP '\bsafeStorage\b' -g '!**/node_modules/**' apps/desktop packages | head

Repository: MocA-Love/superset

Length of output: 874


🏁 Script executed:

# Find setAivisSettings procedure in TRPC settings
rg -nA 20 "setAivisSettings" apps/desktop packages --type ts --type tsx

Repository: MocA-Love/superset

Length of output: 89


🏁 Script executed:

# Find the full path to settings/index.ts and check its content
fd -e ts -e tsx "settings" apps/desktop | grep -E "(settings/index|aivis)" | head -20

Repository: MocA-Love/superset

Length of output: 44


🏁 Script executed:

# Check the database schema for aivisApiKey
rg -nP "aivisApiKey|aivisEnabled|aivisFormat" packages/db --type ts --type tsx -B 2 -A 2

Repository: MocA-Love/superset

Length of output: 89


🏁 Script executed:

# Find files related to settings and Aivis
fd "settings" apps/desktop packages -type f | head -20

Repository: MocA-Love/superset

Length of output: 232


🏁 Script executed:

# Search for setAivisSettings without file type specification
rg "setAivisSettings" apps/desktop packages -B 2 -A 15

Repository: MocA-Love/superset

Length of output: 4358


🏁 Script executed:

# Search for Aivis-related database schema definitions
rg "aivisApiKey|aivisEnabled" packages/db -B 3 -A 3

Repository: MocA-Love/superset

Length of output: 44


🏁 Script executed:

# Read the full setAivisSettings mutation implementation
cat -n apps/desktop/src/lib/trpc/routers/settings/index.ts | sed -n '1,150p'

Repository: MocA-Love/superset

Length of output: 5249


🏁 Script executed:

# Read the aivis-tts.ts file to understand localDb
cat -n apps/desktop/src/main/lib/notifications/aivis-tts.ts | head -100

Repository: MocA-Love/superset

Length of output: 3172


🏁 Script executed:

# Find where localDb is initialized
rg "localDb.*=" apps/desktop packages -B 2 -A 5 | head -40

Repository: MocA-Love/superset

Length of output: 1844


🏁 Script executed:

# Read the full setAivisSettings mutation implementation
cat -n apps/desktop/src/lib/trpc/routers/settings/index.ts | sed -n '200,280p'

Repository: MocA-Love/superset

Length of output: 3017


🏁 Script executed:

# Also check getAivisSettings to understand the full flow
rg "getAivisSettings" apps/desktop/src/lib/trpc/routers/settings/index.ts -B 2 -A 20

Repository: MocA-Love/superset

Length of output: 692


🏁 Script executed:

# Check if there's any encryption being applied in the settings router
rg "safeStorage|encrypt|decrypt" apps/desktop/src/lib/trpc/routers/settings/

Repository: MocA-Love/superset

Length of output: 44


Aivis API キーの平文保存を修正してください(Electron の safeStorage を使用)

readAivisSettings() および setAivisSettings TRPC 手続きで、Aivis API キーが暗号化されずに SQLite に平文保存されています。Electron の safeStorage API を使用して暗号化(macOS: Keychain、Windows: DPAPI、Linux: libsecret など)してください。既存のコードベース内(extension-context.ts)で確立されている safeStorage.encryptString(v).toString("base64") および safeStorage.decryptString(buf) のパターンに従ってください。既存ユーザーデータのマイグレーション戦略も合わせて設計が必要です。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/notifications/aivis-tts.ts` around lines 41 - 60,
readAivisSettings currently returns the Aivis API key in plaintext; update
readAivisSettings and the corresponding setAivisSettings TRPC handler to use
Electron safeStorage: when saving, encrypt the API key with
safeStorage.encryptString(value).toString("base64") (follow extension-context
pattern) and when reading, if row?.aivisApiKey is present call
safeStorage.decryptString(Buffer.from(row.aivisApiKey, "base64")) to obtain the
plaintext key; additionally implement a migration in the set/read path that
detects unencrypted keys (e.g., not valid base64/encrypted format) and re-saves
them encrypted using the same methods so existing user data is migrated
transparently while preserving other fields returned by readAivisSettings.

Comment on lines +67 to +90
const res = await fetch(AIVIS_ENDPOINT, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "audio/mpeg",
},
body: JSON.stringify({
model_uuid: modelUuid,
text,
output_format: "mp3",
}),
});

if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(
`Aivis API error: ${res.status} ${res.statusText} ${body.slice(0, 200)}`,
);
}

const arrayBuffer = await res.arrayBuffer();
return Buffer.from(arrayBuffer);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Aivis API 呼び出しにタイムアウトが無い

fetchsignal / AbortController が設定されていないため、Aivis 側のレスポンスが返ってこない場合にメインプロセス側のリクエストが無期限に滞留します。通知時に連続発火すると FD / メモリが積み重なる可能性があるので、明示的なタイムアウトを設定することを推奨します。

🛡️ タイムアウト追加例(例: 20 秒)
 async function synthesize(
 	apiKey: string,
 	modelUuid: string,
 	text: string,
 ): Promise<Buffer> {
-	const res = await fetch(AIVIS_ENDPOINT, {
-		method: "POST",
-		headers: {
-			Authorization: `Bearer ${apiKey}`,
-			"Content-Type": "application/json",
-			Accept: "audio/mpeg",
-		},
-		body: JSON.stringify({
-			model_uuid: modelUuid,
-			text,
-			output_format: "mp3",
-		}),
-	});
+	const controller = new AbortController();
+	const timer = setTimeout(() => controller.abort(), 20_000);
+	let res: Response;
+	try {
+		res = await fetch(AIVIS_ENDPOINT, {
+			method: "POST",
+			headers: {
+				Authorization: `Bearer ${apiKey}`,
+				"Content-Type": "application/json",
+				Accept: "audio/mpeg",
+			},
+			body: JSON.stringify({
+				model_uuid: modelUuid,
+				text,
+				output_format: "mp3",
+			}),
+			signal: controller.signal,
+		});
+	} finally {
+		clearTimeout(timer);
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/notifications/aivis-tts.ts` around lines 67 - 90,
The fetch call to AIVIS_ENDPOINT (using apiKey, modelUuid, text) lacks a
timeout: create an AbortController, pass controller.signal into the fetch
options, start a timer (e.g., 20s) that calls controller.abort() on timeout, and
clear the timer after receiving a response or on error; ensure the error path
handles AbortError appropriately so the function that returns
Buffer.from(arrayBuffer) won’t hang and resources (timers/FDs) are cleaned up.

Comment on lines +99 to +103
function cleanup(path: string): void {
execFile("rm", ["-f", path], () => {
/* ignore */
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify repo supports Windows builds (package.json platform targets, electron-builder configs, etc.)
fd -a '(package\.json|electron-builder\.(yml|yaml|json|js|ts))' apps/desktop | head -n 20
rg -nP '"win"|"target".*?"nsis"|"platform".*?"win"' -g '!**/node_modules/**' apps/desktop | head

Repository: MocA-Love/superset

Length of output: 406


🏁 Script executed:

cat -n apps/desktop/src/main/lib/notifications/aivis-tts.ts

Repository: MocA-Love/superset

Length of output: 5032


🏁 Script executed:

cat -n apps/desktop/electron-builder.ts

Repository: MocA-Love/superset

Length of output: 6098


🏁 Script executed:

cat -n apps/desktop/package.json | head -50

Repository: MocA-Love/superset

Length of output: 2722


execFile("rm") は Windows で動作しない — 一時ファイルがリークする

アプリケーションは Windows ビルド対応(electron-builder.ts で NSIS ターゲット設定)ですが、cleanup() 関数(99-103 行目)は POSIX 専用の execFile("rm", ["-f", path]) を使用しています。Windows には rm コマンドが存在しないため、エラーが静かに無視され、%TEMP%\superset-aivis-*.mp3 ファイルが削除されずに蓄積します。

fs.promises.unlink() に置き換えてください。

修正案
-import { execFile } from "node:child_process";
-import { writeFile } from "node:fs/promises";
+import { unlink, writeFile } from "node:fs/promises";
-function cleanup(path: string): void {
-	execFile("rm", ["-f", path], () => {
-		/* ignore */
-	});
-}
+function cleanup(path: string): void {
+	unlink(path).catch(() => {
+		/* ignore: best-effort cleanup */
+	});
+}

付属問題:synthesize() に fetch タイムアウトがない

67-79 行目の fetch は AbortSignal を指定していません。Aivis API がハング状態に入った場合、タイムアウト機構がなく、リクエストが永遠に待機してメモリリークの原因になります。AbortController でタイムアウトを設けてください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/notifications/aivis-tts.ts` around lines 99 - 103,
The cleanup() helper currently calls execFile("rm", ...) which fails on Windows
and leaks temp MP3s — replace that with fs.promises.unlink(path) (or
fs.unlinkSync if synchronous behavior is required), await or handle the returned
Promise and swallow only ENOENT while logging other errors; keep the function
name cleanup and ensure callers still work with its signature. Also add a
timeout to synthesize(): create an AbortController, start a setTimeout that
calls controller.abort() after a sensible timeout (e.g. 30s), pass
controller.signal into the fetch call in synthesize(), clear the timer on
success, and handle AbortError/rejections to avoid hanging promises and memory
leaks.

Comment on lines +84 to +89
const branch = worktree?.branch ?? "";
const worktreeName = branch || "";
return {
branch,
workspace: workspace?.name || branch || "",
worktree: worktreeName,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

{{worktree}}{{branch}} と同じ値になっている

worktreeNameworktree?.branch をそのまま入れているため、{{worktree}}{{branch}} が常に同一になり、プレースホルダとして実質使い分けができません。ワークツリーを識別したい場合は worktree.path の basename などを使うのが自然です。

♻️ 修正案
-	const branch = worktree?.branch ?? "";
-	const worktreeName = branch || "";
+	const branch = worktree?.branch ?? "";
+	const worktreePath = worktree?.path ?? "";
+	// path の末尾要素をワークツリー名として使う(無ければ branch にフォールバック)
+	const worktreeName = worktreePath
+		? worktreePath.split(/[\\/]/).filter(Boolean).pop() ?? ""
+		: branch;
 	return {
 		branch,
 		workspace: workspace?.name || branch || "",
 		worktree: worktreeName,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const branch = worktree?.branch ?? "";
const worktreeName = branch || "";
return {
branch,
workspace: workspace?.name || branch || "",
worktree: worktreeName,
const branch = worktree?.branch ?? "";
const worktreePath = worktree?.path ?? "";
// path の末尾要素をワークツリー名として使う(無ければ branch にフォールバック)
const worktreeName = worktreePath
? worktreePath.split(/[\\/]/).filter(Boolean).pop() ?? ""
: branch;
return {
branch,
workspace: workspace?.name || branch || "",
worktree: worktreeName,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/windows/main.ts` around lines 84 - 89, worktreeName is
set to worktree?.branch so {{worktree}} always equals {{branch}}; change it to a
distinct identifier (e.g., the basename of worktree.path) and fall back to
branch/empty when missing. Update the return block that builds
branch/workspace/worktree: set worktreeName = worktree ?
path.basename(worktree.path || worktree.name || "") : branch || ""; ensure you
import Node's path module (or use an existing basename helper) and keep the same
fallback semantics for workspace and branch.

main の 0059_todo_remote_control_enabled とマイグレーション番号が衝突していたため、
aivis migration を drizzle-kit で 0060 として再生成。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aivis APIによる音声読み上げ機能を追加

1 participant