feat(desktop): Aivis API による音声読み上げ通知 (#286)#287
Conversation
- Settings > Notifications に Aivis 設定セクションを追加
- API Key / Model UUID / On-Off / 完了・許可要求別フォーマットを保存
- プレースホルダ ({{branch}} 等) をクリックで挿入できる UI
- エージェント完了・許可要求時に通知音後 Aivis API で音声合成して再生
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAivis APIを統合したテキスト音声合成機能を追加します。通知音再生後に、ワークスペース・ブランチ名などを音声で報告するシステムを実装し、TRPC手続き、データベーススキーマ、UIコンポーネント、テンプレート変数レンダリングを含みます。 Changes
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 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".
- P1: AivisSettings の useEffect が refetch で編集中の値を上書きするのを防止 (hydratedRef で初回のみハイドレートする) - P2: Aivis 音声がリングトーンと並列再生される問題を修正 (playNotificationSound に onComplete を追加し、通知音終了後に TTS を鳴らす)
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
apps/desktop/src/main/lib/notifications/aivis-tts.ts (1)
110-129:playSoundFileのonCompleteが呼ばれない場合の一時ファイルリーク
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.tsのreadAivisSettings()および 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 コミット
apiKey/modelUuidのonBlurでは値が未変更でも毎回save.mutateが走ります。動作上問題はないものの、refetch が連鎖するので「値が変わった時のみ mutate」にすると無駄が減ります。- 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
📒 Files selected for processing (13)
apps/desktop/src/lib/trpc/routers/settings/index.tsapps/desktop/src/main/lib/notification-sound.tsapps/desktop/src/main/lib/notifications/aivis-tts.tsapps/desktop/src/main/lib/notifications/notification-manager.tsapps/desktop/src/main/windows/main.tsapps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/AivisSettings/AivisSettings.tsxapps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/AivisSettings/index.tsapps/desktop/src/renderer/routes/_authenticated/settings/ringtones/page.tsxapps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.tspackages/local-db/drizzle/0059_add_aivis_settings.sqlpackages/local-db/drizzle/meta/0059_snapshot.jsonpackages/local-db/drizzle/meta/_journal.jsonpackages/local-db/src/schema/schema.ts
| 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 }; | ||
| }), |
There was a problem hiding this comment.
testAivisPlayback の入力バリデーション強化と多重実行ガード
2 点指摘します。
apiKey/modelUuidにmin(1)が無いため空文字列が通り、playAivisTts内で throw される形になっています。Zod レベルで弾いたほうが TRPC エラーとして素直に扱えます。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.
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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 | headRepository: 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 tsxRepository: 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 -20Repository: 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 2Repository: MocA-Love/superset
Length of output: 89
🏁 Script executed:
# Find files related to settings and Aivis
fd "settings" apps/desktop packages -type f | head -20Repository: MocA-Love/superset
Length of output: 232
🏁 Script executed:
# Search for setAivisSettings without file type specification
rg "setAivisSettings" apps/desktop packages -B 2 -A 15Repository: MocA-Love/superset
Length of output: 4358
🏁 Script executed:
# Search for Aivis-related database schema definitions
rg "aivisApiKey|aivisEnabled" packages/db -B 3 -A 3Repository: 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 -100Repository: MocA-Love/superset
Length of output: 3172
🏁 Script executed:
# Find where localDb is initialized
rg "localDb.*=" apps/desktop packages -B 2 -A 5 | head -40Repository: 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 20Repository: 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.
| 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); | ||
| } |
There was a problem hiding this comment.
Aivis API 呼び出しにタイムアウトが無い
fetch に signal / 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.
| function cleanup(path: string): void { | ||
| execFile("rm", ["-f", path], () => { | ||
| /* ignore */ | ||
| }); | ||
| } |
There was a problem hiding this comment.
🧩 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 | headRepository: MocA-Love/superset
Length of output: 406
🏁 Script executed:
cat -n apps/desktop/src/main/lib/notifications/aivis-tts.tsRepository: MocA-Love/superset
Length of output: 5032
🏁 Script executed:
cat -n apps/desktop/electron-builder.tsRepository: MocA-Love/superset
Length of output: 6098
🏁 Script executed:
cat -n apps/desktop/package.json | head -50Repository: 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.
| const branch = worktree?.branch ?? ""; | ||
| const worktreeName = branch || ""; | ||
| return { | ||
| branch, | ||
| workspace: workspace?.name || branch || "", | ||
| worktree: worktreeName, |
There was a problem hiding this comment.
{{worktree}} が {{branch}} と同じ値になっている
worktreeName に worktree?.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.
| 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 として再生成。
Closes #286
概要
Settings > Notifications に Aivis API による音声読み上げ機能を追加。エージェント動作完了・許可要求時に通知音の後、Aivis の音声合成で「ワークスペース、{{branch}}、です」のようにアナウンスする。さらに、同じ Aivis アカウント上のユーザー辞書管理と日別使用量ダッシュボードも同画面内に統合。
音声通知 (ベース機能)
settingsにaivisEnabled/aivisApiKey/aivisModelUuid/aivisFormat/aivisFormatPermissionを追加apps/desktop/src/main/lib/notifications/aivis-tts.tsでPOST /v1/tts/synthesize(Bearer, mp3) を叩き、通知音終了後に再生 (onComplete チェーン)getAivisSettings/setAivisSettings/testAivisPlayback{{branch}}/{{workspace}}/{{worktree}}/{{tab}}/{{pane}}/{{event}}。未定義は空文字。ワークスペース、{{workspace}}、です/ 許可要求{{branch}}で対応が必要ですユーザー辞書 (追加)
settings.aivis_user_dictionary_uuidを追加 (migration 0061)main/lib/aivis/client.ts— Bearer 認証 + エラー型付きの共通 fetch ラッパー (AivisApiKeyMissingError/AivisApiError)dictionary.list/get/create/update/delete/import/export、usage.daily/me、validateKeyuser_dictionary_uuidを TTS リクエストに付与 (辞書選択中のみ)surface / 読み (カタカナ検証) / アクセント型 / 優先度 0-10 / 品詞日別使用量ダッシュボード (追加)
aivis.usage.daily(startDate, endDate)— Aivis/v1/payment/usage-summaries(時間単位) を日別集計 + API キー別に内訳を添付動作確認