Skip to content

feat(mobile): add LLM smart shuffle#271

Draft
xiongzikun0106 wants to merge 8 commits into
bbplayer-app:devfrom
xiongzikun0106:emotionRandomPlay
Draft

feat(mobile): add LLM smart shuffle#271
xiongzikun0106 wants to merge 8 commits into
bbplayer-app:devfrom
xiongzikun0106:emotionRandomPlay

Conversation

@xiongzikun0106
Copy link
Copy Markdown

@xiongzikun0106 xiongzikun0106 commented Apr 28, 2026

Summary

  • Add an LLM-powered smart shuffle flow for local playlists, including playlist entry points, modal UI, app-store flags, navigation typing, and persistent track tag storage.
  • Add the smart shuffle tagging service and database migration so tracks can be classified with tags, confidence, and optional reasoning for recommendation/shuffle decisions.
  • Improve development reliability on Windows and Expo prebuild by using cross-env, fixing generated Android ABI filter insertion, and making Lefthook/gitleaks execution work without fragile inline shell parsing.

Feature Details

  • Adds SmartShuffleModal and LlmSmartShuffleSettingsModal so users can start smart shuffle from playlist UI and configure the feature from playback settings.
  • Introduces LlmSmartShuffleService plus TrackTagIndex storage to normalize LLM responses, clamp confidence values, persist tags, and support future smart ordering logic.
  • Extends the local playlist header and playlist screen wiring so the feature is available where users manage/play local playlists.
  • Adds Drizzle migration 0019_smart_shuffle_tags and schema updates for smart shuffle tag indexing.
  • Updates Bilibili playlist sync integration to work with the new smart shuffle metadata path.

Tooling / Android Notes

  • Fixes withAbiFilters so Expo prebuild inserts ndk.abiFilters into defaultConfig safely without corrupting buildConfigField in android/app/build.gradle.
  • Adds cross-env to mobile scripts so expo start and expo run:android work consistently on Windows shells.
  • Updates Lefthook gitleaks handling with a small Node wrapper so pre-commit hooks do not fail from shell parsing differences on Windows.

Verification

  • Confirmed working tree is clean before pushing.
  • Confirmed branch diff against origin/dev contains the intended smart shuffle, Android prebuild, and tooling changes.
  • Previously verified generated Android Gradle project with ./gradlew.bat projects from apps/mobile/android successfully.
  • Pre-commit and commit-msg hooks passed while creating the latest commits; gitleaks is skipped locally because it is not installed.

Summary by CodeRabbit

发布说明

  • 新功能

    • 智能随机播放:用户可通过自然语言提示语自定义播放队列
    • LLM配置界面:支持配置OpenAI兼容API端点和默认收听偏好
    • 智能标签索引:自动为播放列表曲目生成和存储AI标签
  • 改进

    • 增强跨平台构建脚本兼容性
  • Chores

    • 更新代码检查脚本配置

@safedep
Copy link
Copy Markdown

safedep Bot commented Apr 28, 2026

SafeDep Report Summary

Green Malicious Packages Badge Green Vulnerable Packages Badge Green Risky License Badge

Package Details
Package Malware Vulnerability Risky License Report
icon @epic-web/invariant @ 1.0.0
pnpm-lock.yaml
ok icon
ok icon
ok icon
🔗
icon cross-env @ 10.1.0
pnpm-lock.yaml apps/mobile/package.json
ok icon
ok icon
ok icon
🔗

View complete scan results →

This report is generated by SafeDep Github App

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

@xiongzikun0106 is attempting to deploy a commit to the yanyao2333's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4241c16a-d9d2-4978-b2de-41301aa8a318

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

该PR引入了一个LLM驱动的"智能随机播放"功能,用于音乐播放列表。包括数据库表用于存储LLM派生的轨道标签、配置模态框、智能队列生成服务、以及与现有同步管道的集成。

Changes

Cohort / File(s) Summary
数据库架构和迁移
apps/mobile/drizzle/0019_smart_shuffle_tags.sql, apps/mobile/drizzle/meta/_journal.json, apps/mobile/drizzle/migrations.js, apps/mobile/src/lib/db/schema.ts
添加新的track_llm_tags表以存储LLM生成的轨道标签、置信度和元数据。包括外键约束、级联删除和用于查询优化的索引。
LLM服务实现
apps/mobile/src/lib/services/llmSmartShuffleService.ts
实现核心LlmSmartShuffleService,提供轨道索引、偏好解析和智能队列生成功能。支持与OpenAI兼容的API集成。
类型定义
apps/mobile/src/types/core/appStore.ts, apps/mobile/src/types/services/llmSmartShuffle.ts, apps/mobile/src/types/navigation.ts
扩展应用配置类型以包含LLM设置(基础URL、API密钥、模型选择)。定义智能随机播放相关接口和导航模态框映射。
UI模态框组件
apps/mobile/src/components/modals/playlist/SmartShuffleModal.tsx, apps/mobile/src/components/modals/settings/LlmSmartShuffleSettingsModal.tsx, apps/mobile/src/components/ModalRegistry.tsx
添加两个新的模态框:智能随机播放触发器和LLM配置面板。在模态框注册表中注册新组件。
播放列表UI和页面集成
apps/mobile/src/features/playlist/local/components/LocalPlaylistHeader.tsx, apps/mobile/src/app/playlist/local/[id].tsx, apps/mobile/src/app/settings/playback.tsx
在播放列表头部和播放设置页面添加智能随机播放按钮和配置区域。
同步管道集成
apps/mobile/src/lib/facades/syncBilibiliPlaylist.ts
在Bilibili播放列表同步完成后触发LLM轨道索引。添加'indexing'进度阶段。
应用配置和工具
apps/mobile/src/hooks/stores/useAppStore.ts, apps/mobile/package.json
扩展应用存储以包含LLM配置默认值。添加cross-env以进行环境变量管理。
构建工具和脚本
apps/mobile/expo-plugins/withAbiFilters.js, lefthook.yml, scripts/lefthook-gitleaks.cjs
改进Gradle配置插件的NDK过滤逻辑。重构Lefthook钩子配置以分离lint/format命令。添加脚本化的gitleaks检查。

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant PlaylistUI as Playlist UI
    participant SmartShuffleService as SmartShuffle Service
    participant LLMApi as LLM API
    participant Database as Database
    participant QueueManager as Queue Manager
    
    User->>PlaylistUI: Click Smart Shuffle Button
    PlaylistUI->>SmartShuffleService: createSmartQueue(tracks, prompt)
    SmartShuffleService->>Database: Fetch stored LLM tags for tracks
    Database-->>SmartShuffleService: Track tags (confidence, categories)
    
    alt Tags missing locally
        SmartShuffleService->>SmartShuffleService: Generate tags from track metadata
    end
    
    SmartShuffleService->>SmartShuffleService: Score tracks based on preference
    SmartShuffleService->>SmartShuffleService: Apply time bias & exploration level
    SmartShuffleService->>SmartShuffleService: Stable shuffle & sort by score
    SmartShuffleService-->>PlaylistUI: Reordered track list
    
    PlaylistUI->>QueueManager: Enqueue smart queue
    QueueManager-->>User: Start playback with smart order
Loading
sequenceDiagram
    participant SyncProcess as Sync Process
    participant Database as Database
    participant SmartShuffleService as SmartShuffle Service
    participant LLMApi as LLM API
    participant TrackLlmTagsTable as track_llm_tags Table
    
    SyncProcess->>Database: Sync playlist tracks
    Database-->>SyncProcess: Transaction committed
    
    SyncProcess->>SmartShuffleService: indexPlaylistTracks(playlistId, sourceInfo)
    SmartShuffleService->>Database: Fetch all playlist tracks
    Database-->>SmartShuffleService: Track list
    
    SmartShuffleService->>SmartShuffleService: Batch tracks into LLM requests
    SmartShuffleService->>LLMApi: Extract tags (language, genre, mood, etc.)
    LLMApi-->>SmartShuffleService: Tag arrays with confidence
    
    SmartShuffleService->>SmartShuffleService: Normalize tags & build indices
    SmartShuffleService->>TrackLlmTagsTable: Upsert tag records with metadata
    TrackLlmTagsTable-->>SmartShuffleService: Insert/Update complete
    
    SmartShuffleService-->>SyncProcess: Indexing complete
    SyncProcess->>SyncProcess: Emit progress update
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • feat: sync progress modal #179: 扩展播放列表同步进度流程,添加"indexing"阶段并从syncBilibiliPlaylist发出,与本PR对FavoriteSyncProgress的类型更改和进度跟踪直接互补。
  • feat: some improvements #243: 两个PR都涉及移动应用数据库架构和迁移管道的修改,包括Drizzle迁移和数据库模式定义。

Suggested labels

enhancement

Poem

🐰 智能洗牌的魔法在,
LLM标签闪闪发亮,
偏好转化成播放队列,
音乐随意且聪慧地流淌,
Bilibili播放列表焕新章!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了PR的核心功能:为移动应用添加LLM智能随机播放功能。描述清晰、简洁,涵盖了变更的主要意图。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@roitium roitium marked this pull request as draft April 28, 2026 12:25
@roitium
Copy link
Copy Markdown
Collaborator

roitium commented Apr 28, 2026

@CodeRabbit pause

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Apr 28, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm @react-native/debugger-frontend is 96.0% likely obfuscated

Confidence: 0.96

Location: Package overview

From: packages/image-theme-colors/example/package-lock.jsonnpm/react-native@0.81.5npm/expo@54.0.22npm/@react-native/debugger-frontend@0.81.5

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@react-native/debugger-frontend@0.81.5. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
bbplayer-docs Ready Ready Preview, Comment Apr 28, 2026 0:30am

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: 9

🧹 Nitpick comments (4)
apps/mobile/drizzle/0019_smart_shuffle_tags.sql (1)

4-4: 建议在库层补充 confidence 范围约束。

当前仅靠服务层 clamp,建议增加 CHECK (confidence >= 0 AND confidence <= 1),避免旁路写入导致脏数据。

🧱 建议修改
-	`confidence` real DEFAULT 0 NOT NULL,
+	`confidence` real DEFAULT 0 NOT NULL CHECK (`confidence` >= 0 AND `confidence` <= 1),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/drizzle/0019_smart_shuffle_tags.sql` at line 4, Add a DB-level
check constraint to enforce confidence is within [0,1] for the confidence
column; update the column definition in the migration (the `confidence` real
DEFAULT 0 NOT NULL` line) to include a CHECK (confidence >= 0 AND confidence <=
1) or add a named CONSTRAINT (e.g., CONSTRAINT check_confidence_range CHECK
(confidence >= 0 AND confidence <= 1)) so out-of-band writes cannot insert
values outside 0..1.
apps/mobile/src/components/ModalRegistry.tsx (1)

68-73: 新增懒加载导入建议改为 @/* 别名。

这两处新增 lazy(import(...)) 仍使用相对路径,建议与移动端导入规范统一,避免后续规则冲突。

♻️ 建议修改
 const LlmSmartShuffleSettingsModal = lazy(
-	() => import('./modals/settings/LlmSmartShuffleSettingsModal'),
+	() => import('@/components/modals/settings/LlmSmartShuffleSettingsModal'),
 )
 const SmartShuffleModal = lazy(
-	() => import('./modals/playlist/SmartShuffleModal'),
+	() => import('@/components/modals/playlist/SmartShuffleModal'),
 )

As per coding guidelines, apps/mobile/src/**/*.{ts,tsx}: Use import aliases @/* for all imports in the mobile app instead of relative paths.

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

In `@apps/mobile/src/components/ModalRegistry.tsx` around lines 68 - 73, Replace
the relative import paths in the lazy calls for LlmSmartShuffleSettingsModal and
SmartShuffleModal so they use the app alias import style (e.g., '@/...') instead
of './modals/...'; update the two lazy(() => import(...)) expressions for the
symbols LlmSmartShuffleSettingsModal and SmartShuffleModal to import via the
alias path that matches the mobile app convention.
apps/mobile/src/features/playlist/local/components/LocalPlaylistHeader.tsx (1)

33-33: 建议将智能随机按钮限定在本地歌单,并将回调设为可选。

现在是无条件渲染 + 必填回调,会让非本地场景也被迫接入该行为。建议按 playlist.type === 'local' 控制展示,避免组件 API 过度耦合。

♻️ 建议修改
 interface PlaylistHeaderProps {
   onClickPlayAll: () => void
-  onClickSmartShuffle: () => void
+  onClickSmartShuffle?: () => void
   onClickSync: () => void
   onClickCopyToLocalPlaylist: () => void
 }

 ...
-  <IconButton
-    mode='contained'
-    icon='shuffle-variant'
-    size={20}
-    onPress={onClickSmartShuffle}
-    testID='playlist-smart-shuffle'
-  />
+  {playlist.type === 'local' && onClickSmartShuffle && (
+    <IconButton
+      mode='contained'
+      icon='shuffle-variant'
+      size={20}
+      onPress={onClickSmartShuffle}
+      testID='playlist-smart-shuffle'
+    />
+  )}

Also applies to: 92-93, 358-364

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

In `@apps/mobile/src/features/playlist/local/components/LocalPlaylistHeader.tsx`
at line 33, The LocalPlaylistHeader prop onClickSmartShuffle is currently
required and the smart-shuffle UI is always rendered; make the prop optional
(onClickSmartShuffle?: () => void) and only render the smart-shuffle button when
playlist.type === 'local' and onClickSmartShuffle is provided; update the
LocalPlaylistHeader props/interface and all render sites that reference
onClickSmartShuffle (including the other instances noted around the
smart-shuffle button rendering) to handle it being undefined to avoid forcing
non-local playlists to implement the callback.
apps/mobile/src/lib/db/schema.ts (1)

201-201: 建议在数据库层约束 confidence 取值范围(0~1)。

仅依赖服务层 clamp 容易被绕过;在 schema 加 check 可以更好保证数据一致性。

🛡️ 建议修改
  (table) => [
    index('track_llm_tags_source_idx').on(table.sourceType, table.sourceId),
    index('track_llm_tags_indexed_at_idx').on(table.indexedAt),
+   check(
+     'track_llm_tags_confidence_range_check',
+     sql`confidence >= 0 AND confidence <= 1`,
+   ),
  ],

Also applies to: 214-217

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

In `@apps/mobile/src/lib/db/schema.ts` at line 201, Add a DB-level CHECK
constraint to the confidence column(s) so values are always between 0 and 1:
locate the confidence column definition(s)
(real('confidence').notNull().default(0)) in schema.ts and modify the
column/schema to include a CHECK constraint enforcing confidence >= 0 AND
confidence <= 1; update the corresponding table definitions referenced around
the other occurrences (lines ~214-217) and add a migration so the new constraint
is applied to existing databases.
🤖 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/mobile/expo-plugins/withAbiFilters.js`:
- Around line 12-15: The current global regex used when building
contentsWithoutManagedBlock removes any ndk { abiFilters ... } block across the
file (affecting productFlavors/buildTypes); change the logic to only remove the
plugin-managed block by matching explicit markers the plugin inserts (e.g.,
BEGIN/END managed comments) or a unique sentinel the plugin writes, and update
the replacement in the code that computes contentsWithoutManagedBlock (and any
helper that writes the block) to search for and remove only that marked block so
other ndk blocks remain untouched.

In `@apps/mobile/src/components/modals/playlist/SmartShuffleModal.tsx`:
- Around line 47-49: The filter on tracksResult.value can throw when item.source
=== 'bilibili' but item.bilibiliMetadata is null/undefined; update the predicate
to use optional chaining and a safe default (e.g.
item.bilibiliMetadata?.videoIsValid ?? false) so the expression becomes: when
source is 'bilibili' check item.bilibiliMetadata?.videoIsValid (or false if
missing), otherwise keep true; adjust in the code that computes tracks
(references: tracks, tracksResult, item.bilibiliMetadata.videoIsValid).

In `@apps/mobile/src/components/modals/settings/LlmSmartShuffleSettingsModal.tsx`:
- Around line 78-100: The three TextInput components in
LlmSmartShuffleSettingsModal (the fields bound to llmBaseUrl, llmApiKey, and
llmModel) need keyboard behavior fixed: add autoCapitalize='none' and
autoCorrect={false} props to each TextInput (the one using llmBaseUrl, the one
using llmApiKey, and the one using llmModel) so URLs, API keys, and model names
are not auto-capitalized or autocorrected by the mobile keyboard.
- Around line 28-36: handleSave is persisting llmApiKey into MMKV without
encryption and the TextInput fields for llmBaseUrl and llmModel lack keyboard
protections; either enable MMKV encryption during app store initialization (call
storage.setEncryptionKey(...) from useAppStore/init code) or remove llmApiKey
from persisted state in setSettings, and add autoCorrect={false} and
autoCapitalize="none" to the TextInput components used for llmBaseUrl and
llmModel to prevent keyboard autocorrections; ensure secureTextEntry remains for
UI masking but do not rely on it for storage protection.

In `@apps/mobile/src/lib/facades/syncBilibiliPlaylist.ts`:
- Around line 303-308: The transaction callback passed to this.db.transaction is
missing the transaction handle and therefore
playlistService/trackService/artistService are using the root DB instead of the
transaction; change the async callback signature to accept the tx parameter
(e.g., async (tx) => { ... }) and replace withDB(this.db) calls with withDB(tx)
for playlistService, trackService and artistService inside that callback so all
writes run within the same transaction context (ensure the playlistId assignment
remains inside the transaction-returning block).

In `@apps/mobile/src/lib/services/llmSmartShuffleService.ts`:
- Around line 260-298: The LLM-returned trackId should not be trusted as the DB
primary key: before inserting into schema.trackLlmTags (the block building
values from indexes mapped from response.tracks) filter or remap entries using
the local context map (contextById) so you only insert tags for known contexts;
e.g., only keep indexes where contextById.has(index.trackId) or better yet, map
each response item to the context entry and use context.track.id (or
request-order mapping) as the authoritative trackId when constructing the insert
payload to avoid accidental overwrites.
- Around line 81-89: parseJsonObject and callJson currently throw Errors which
violates the service-layer convention to use neverthrow Results; change both
functions to return neverthrow Result<T, Error> or ResultAsync<T, Error> instead
of throwing: on JSON parse success return ok(value), on failure return err(new
Error(...)) (for parseJsonObject include the fallback match logic but return err
if still invalid); ensure callers like indexTrackBatch and parsePreference
consume/chain these Results (using .map/.andThen/.mapErr or ResultAsync
equivalents) so error branches are composed rather than throwing. Use the
existing function names parseJsonObject and callJson so callers can swap to the
Result-returning APIs without changing call sites beyond handling the Result.
- Around line 190-202: The fetch call in llmSmartShuffleService.ts must be
protected with an AbortController timeout and the service must stop throwing
errors directly; change the fetch block to use an AbortController with a
configurable timeout and call controller.abort() on timeout, and refactor error
paths to use neverthrow's ResultAsync/okAsync/errAsync instead of throwing;
update the helper callJson function to return ResultAsync (wrap fetch + JSON
parsing errors into errAsync) and replace all direct throw Error usages in this
module (and call sites referenced by callJson) with returning errAsync(...) or
mapping to okAsync(...) so the public API of functions like the
LlmSmartShuffleService method returns ResultAsync<ResultType, ErrorType>.

In `@lefthook.yml`:
- Line 2: The lefthook configuration currently sets parallel: true which causes
race conditions between the format-codes hook (which rewrites and restages
files) and the oxlint/eslint hooks; change the configuration to run hooks
sequentially by removing or setting parallel: false and ensure ordering so that
the format-codes hook runs before oxlint/eslint (apply the same change to the
other hook groups referenced around lines 6-15) to eliminate the timing/race
issue.

---

Nitpick comments:
In `@apps/mobile/drizzle/0019_smart_shuffle_tags.sql`:
- Line 4: Add a DB-level check constraint to enforce confidence is within [0,1]
for the confidence column; update the column definition in the migration (the
`confidence` real DEFAULT 0 NOT NULL` line) to include a CHECK (confidence >= 0
AND confidence <= 1) or add a named CONSTRAINT (e.g., CONSTRAINT
check_confidence_range CHECK (confidence >= 0 AND confidence <= 1)) so
out-of-band writes cannot insert values outside 0..1.

In `@apps/mobile/src/components/ModalRegistry.tsx`:
- Around line 68-73: Replace the relative import paths in the lazy calls for
LlmSmartShuffleSettingsModal and SmartShuffleModal so they use the app alias
import style (e.g., '@/...') instead of './modals/...'; update the two lazy(()
=> import(...)) expressions for the symbols LlmSmartShuffleSettingsModal and
SmartShuffleModal to import via the alias path that matches the mobile app
convention.

In `@apps/mobile/src/features/playlist/local/components/LocalPlaylistHeader.tsx`:
- Line 33: The LocalPlaylistHeader prop onClickSmartShuffle is currently
required and the smart-shuffle UI is always rendered; make the prop optional
(onClickSmartShuffle?: () => void) and only render the smart-shuffle button when
playlist.type === 'local' and onClickSmartShuffle is provided; update the
LocalPlaylistHeader props/interface and all render sites that reference
onClickSmartShuffle (including the other instances noted around the
smart-shuffle button rendering) to handle it being undefined to avoid forcing
non-local playlists to implement the callback.

In `@apps/mobile/src/lib/db/schema.ts`:
- Line 201: Add a DB-level CHECK constraint to the confidence column(s) so
values are always between 0 and 1: locate the confidence column definition(s)
(real('confidence').notNull().default(0)) in schema.ts and modify the
column/schema to include a CHECK constraint enforcing confidence >= 0 AND
confidence <= 1; update the corresponding table definitions referenced around
the other occurrences (lines ~214-217) and add a migration so the new constraint
is applied to existing databases.
🪄 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: 36e0bff6-cc4f-4e54-859f-5bad1c83b61b

📥 Commits

Reviewing files that changed from the base of the PR and between 78719ff and a7d73e3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (20)
  • apps/mobile/drizzle/0019_smart_shuffle_tags.sql
  • apps/mobile/drizzle/meta/_journal.json
  • apps/mobile/drizzle/migrations.js
  • apps/mobile/expo-plugins/withAbiFilters.js
  • apps/mobile/package.json
  • apps/mobile/src/app/playlist/local/[id].tsx
  • apps/mobile/src/app/settings/playback.tsx
  • apps/mobile/src/components/ModalRegistry.tsx
  • apps/mobile/src/components/modals/playlist/SmartShuffleModal.tsx
  • apps/mobile/src/components/modals/settings/LlmSmartShuffleSettingsModal.tsx
  • apps/mobile/src/features/playlist/local/components/LocalPlaylistHeader.tsx
  • apps/mobile/src/hooks/stores/useAppStore.ts
  • apps/mobile/src/lib/db/schema.ts
  • apps/mobile/src/lib/facades/syncBilibiliPlaylist.ts
  • apps/mobile/src/lib/services/llmSmartShuffleService.ts
  • apps/mobile/src/types/core/appStore.ts
  • apps/mobile/src/types/navigation.ts
  • apps/mobile/src/types/services/llmSmartShuffle.ts
  • lefthook.yml
  • scripts/lefthook-gitleaks.cjs

Comment on lines +12 to +15
const contentsWithoutManagedBlock = contents.replace(
/\n\s*ndk\s*\{\s*\n\s*abiFilters\s+[^\n]+\n\s*\}/g,
'',
)
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

仅删除“受管块”,当前正则会误删其他 ndk 配置

Line [13] 的全局正则会删除文件中任意 ndk { abiFilters ... } 块,不限于 defaultConfig。这会误伤 productFlavors/buildTypes 的 ABI 配置,导致构建行为被悄然改变。建议改为只清理插件自己插入的受管块。

🔧 建议修复(用受管标记实现精准 upsert)
+const MANAGED_ABI_FILTERS_BLOCK_REGEX =
+	/\n?\s*\/\/ `@managed-by-withAbiFilters`:start[\s\S]*?\/\/ `@managed-by-withAbiFilters`:end\s*\n?/g
+
 const upsertAbiFilters = (contents, abiFiltersString) => {
-	const ndkBlock = `        ndk {
+	const ndkBlock = `        // `@managed-by-withAbiFilters`:start
+        ndk {
             abiFilters ${abiFiltersString}
         }
+        // `@managed-by-withAbiFilters`:end
 `
 
 	const contentsWithoutManagedBlock = contents.replace(
-		/\n\s*ndk\s*\{\s*\n\s*abiFilters\s+[^\n]+\n\s*\}/g,
+		MANAGED_ABI_FILTERS_BLOCK_REGEX,
 		'',
 	)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/expo-plugins/withAbiFilters.js` around lines 12 - 15, The current
global regex used when building contentsWithoutManagedBlock removes any ndk {
abiFilters ... } block across the file (affecting productFlavors/buildTypes);
change the logic to only remove the plugin-managed block by matching explicit
markers the plugin inserts (e.g., BEGIN/END managed comments) or a unique
sentinel the plugin writes, and update the replacement in the code that computes
contentsWithoutManagedBlock (and any helper that writes the block) to search for
and remove only that marked block so other ndk blocks remain untouched.

Comment on lines +47 to +49
const tracks = tracksResult.value.filter((item) =>
item.source === 'bilibili' ? item.bilibiliMetadata.videoIsValid : 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 | 🟠 Major

这里有潜在空引用风险。

item.source === 'bilibili' 且元数据缺失时会直接崩溃。建议加可选链保护。

🐛 建议修复
 const tracks = tracksResult.value.filter((item) =>
-  item.source === 'bilibili' ? item.bilibiliMetadata.videoIsValid : true,
+  item.source === 'bilibili'
+    ? (item.bilibiliMetadata?.videoIsValid ?? true)
+    : true,
 )
📝 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 tracks = tracksResult.value.filter((item) =>
item.source === 'bilibili' ? item.bilibiliMetadata.videoIsValid : true,
)
const tracks = tracksResult.value.filter((item) =>
item.source === 'bilibili'
? (item.bilibiliMetadata?.videoIsValid ?? true)
: true,
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/src/components/modals/playlist/SmartShuffleModal.tsx` around
lines 47 - 49, The filter on tracksResult.value can throw when item.source ===
'bilibili' but item.bilibiliMetadata is null/undefined; update the predicate to
use optional chaining and a safe default (e.g.
item.bilibiliMetadata?.videoIsValid ?? false) so the expression becomes: when
source is 'bilibili' check item.bilibiliMetadata?.videoIsValid (or false if
missing), otherwise keep true; adjust in the code that computes tracks
(references: tracks, tracksResult, item.bilibiliMetadata.videoIsValid).

Comment on lines +28 to +36
const handleSave = () => {
setSettings({
enableLlmTagging,
allowLlmMetadataUpload,
llmBaseUrl: llmBaseUrl.trim() || 'https://api.openai.com/v1',
llmApiKey: llmApiKey.trim(),
llmModel: llmModel.trim() || 'gpt-4o-mini',
llmDefaultPreference: llmDefaultPreference.trim(),
})
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
set -euo pipefail

echo "== useAppStore implementation =="
fd -i 'useAppStore\.ts$' apps/mobile/src/hooks/stores -x sed -n '1,260p' {}

echo
echo "== persistence backend references =="
rg -n -C3 'persist\(|createJSONStorage|AsyncStorage|MMKV|SecureStore|EncryptedStorage|storage:' apps/mobile/src

echo
echo "== llmApiKey usage/persistence sites =="
rg -n -C2 '\bllmApiKey\b' apps/mobile/src

Repository: bbplayer-app/BBPlayer

Length of output: 22258


🏁 Script executed:

#!/bin/bash

echo "== Search for setEncryptionKey calls =="
rg -n 'setEncryptionKey\|setPassword\|encrypt' apps/mobile/src --type ts --type tsx

echo
echo "== Full LlmSmartShuffleSettingsModal.tsx file (to check TextInput props) =="
fd 'LlmSmartShuffleSettingsModal\.tsx$' apps/mobile/src -x cat -n {}

echo
echo "== MMKV initialization with any security setup =="
rg -n -B5 -A10 'createMMKV\(\)' apps/mobile/src/utils/mmkv.ts

Repository: bbplayer-app/BBPlayer

Length of output: 5581


确认 llmApiKey 明文存储,且 URL/模型字段缺少键盘输入保护。

第 29-35 行将 API Key 持久化到 MMKV,但 MMKV 初始化时未启用加密(createMMKV() 无密钥参数,代码中无 setEncryptionKey() 调用)。此外:

  • 第 78-85 行的 llmBaseUrl 和第 94-101 行的 llmModel TextInput 缺少 autoCorrect={false}autoCapitalize="none" 属性,易被键盘自动修正破坏 URL 格式和模型名称。
  • 第 92 行虽有 secureTextEntry 掩盖了 UI 中的输入,但无法解决存储端的明文问题。

建议:为 MMKV 启用加密(在 useAppStore 初始化时调用 storage.setEncryptionKey()),或从持久化状态中排除 llmApiKey;同时在 URL 和模型字段添加 autoCorrect={false}autoCapitalize="none"

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

In `@apps/mobile/src/components/modals/settings/LlmSmartShuffleSettingsModal.tsx`
around lines 28 - 36, handleSave is persisting llmApiKey into MMKV without
encryption and the TextInput fields for llmBaseUrl and llmModel lack keyboard
protections; either enable MMKV encryption during app store initialization (call
storage.setEncryptionKey(...) from useAppStore/init code) or remove llmApiKey
from persisted state in setSettings, and add autoCorrect={false} and
autoCapitalize="none" to the TextInput components used for llmBaseUrl and
llmModel to prevent keyboard autocorrections; ensure secureTextEntry remains for
UI masking but do not rely on it for storage protection.

Comment on lines +78 to +100
<TextInput
label='OpenAI 兼容 API 地址'
value={llmBaseUrl}
onChangeText={setLlmBaseUrl}
mode='outlined'
style={styles.input}
placeholder='https://api.openai.com/v1'
/>
<TextInput
label='API Key'
value={llmApiKey}
onChangeText={setLlmApiKey}
mode='outlined'
style={styles.input}
secureTextEntry
/>
<TextInput
label='模型名称'
value={llmModel}
onChangeText={setLlmModel}
mode='outlined'
style={styles.input}
placeholder='gpt-4o-mini'
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

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and read the relevant lines
if [ -f "apps/mobile/src/components/modals/settings/LlmSmartShuffleSettingsModal.tsx" ]; then
  echo "=== File found. Reading lines 70-110 for context ==="
  sed -n '70,110p' "apps/mobile/src/components/modals/settings/LlmSmartShuffleSettingsModal.tsx"
else
  echo "File not found"
fi

Repository: bbplayer-app/BBPlayer

Length of output: 1082


🏁 Script executed:

# Also check the imports to verify TextInput source
head -30 "apps/mobile/src/components/modals/settings/LlmSmartShuffleSettingsModal.tsx"

Repository: bbplayer-app/BBPlayer

Length of output: 1217


TextInput 字段需要禁用自动更正和自动大写。

这三个字段(llmBaseUrlllmApiKeyllmModel)包含精确字符串,移动端键盘的自动更正和首字母大写会无声地破坏 URL、API 密钥或模型名称。需要添加 autoCapitalize='none'autoCorrect={false} 到这些 TextInput 组件。

建议修改
 					<TextInput
 						label='OpenAI 兼容 API 地址'
 						value={llmBaseUrl}
 						onChangeText={setLlmBaseUrl}
 						mode='outlined'
 						style={styles.input}
 						placeholder='https://api.openai.com/v1'
+						autoCapitalize='none'
+						autoCorrect={false}
 					/>
 					<TextInput
 						label='API Key'
 						value={llmApiKey}
 						onChangeText={setLlmApiKey}
 						mode='outlined'
 						style={styles.input}
 						secureTextEntry
+						autoCapitalize='none'
+						autoCorrect={false}
 					/>
 					<TextInput
 						label='模型名称'
 						value={llmModel}
 						onChangeText={setLlmModel}
 						mode='outlined'
 						style={styles.input}
 						placeholder='gpt-4o-mini'
+						autoCapitalize='none'
+						autoCorrect={false}
 					/>
📝 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
<TextInput
label='OpenAI 兼容 API 地址'
value={llmBaseUrl}
onChangeText={setLlmBaseUrl}
mode='outlined'
style={styles.input}
placeholder='https://api.openai.com/v1'
/>
<TextInput
label='API Key'
value={llmApiKey}
onChangeText={setLlmApiKey}
mode='outlined'
style={styles.input}
secureTextEntry
/>
<TextInput
label='模型名称'
value={llmModel}
onChangeText={setLlmModel}
mode='outlined'
style={styles.input}
placeholder='gpt-4o-mini'
<TextInput
label='OpenAI 兼容 API 地址'
value={llmBaseUrl}
onChangeText={setLlmBaseUrl}
mode='outlined'
style={styles.input}
placeholder='https://api.openai.com/v1'
autoCapitalize='none'
autoCorrect={false}
/>
<TextInput
label='API Key'
value={llmApiKey}
onChangeText={setLlmApiKey}
mode='outlined'
style={styles.input}
secureTextEntry
autoCapitalize='none'
autoCorrect={false}
/>
<TextInput
label='模型名称'
value={llmModel}
onChangeText={setLlmModel}
mode='outlined'
style={styles.input}
placeholder='gpt-4o-mini'
autoCapitalize='none'
autoCorrect={false}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/src/components/modals/settings/LlmSmartShuffleSettingsModal.tsx`
around lines 78 - 100, The three TextInput components in
LlmSmartShuffleSettingsModal (the fields bound to llmBaseUrl, llmApiKey, and
llmModel) need keyboard behavior fixed: add autoCapitalize='none' and
autoCorrect={false} props to each TextInput (the one using llmBaseUrl, the one
using llmApiKey, and the one using llmModel) so URLs, API keys, and model names
are not auto-capitalized or autocorrected by the mobile keyboard.

Comment on lines 303 to 308
return ResultAsync.fromPromise(
this.db.transaction(async () => {
(async () => {
const playlistId = await this.db.transaction(async () => {
const playlistSvc = this.playlistService.withDB(this.db)
const trackSvc = this.trackService.withDB(this.db)
const artistSvc = this.artistService.withDB(this.db)
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:

cat -n apps/mobile/src/lib/facades/syncBilibiliPlaylist.ts | sed -n '295,320p'

Repository: bbplayer-app/BBPlayer

Length of output: 1175


🏁 Script executed:

# Search for transaction usage patterns to understand the API
rg "\.transaction\(" apps/mobile/src --type ts --type tsx -A 3 | head -50

Repository: bbplayer-app/BBPlayer

Length of output: 93


🏁 Script executed:

# Check the withDB method signature
rg "withDB\(" apps/mobile/src/lib/facades/syncBilibiliPlaylist.ts -B 2 -A 2

Repository: bbplayer-app/BBPlayer

Length of output: 1052


🏁 Script executed:

# Search for more transaction patterns to understand full context
rg "\.transaction\(async" apps/mobile/src -B 2 -A 5 | head -80

Repository: bbplayer-app/BBPlayer

Length of output: 6624


🏁 Script executed:

# Check the db.transaction method to understand its signature
fd -e ts -e tsx . apps/mobile/src | xargs grep -l "transaction.*async" | head -5

Repository: bbplayer-app/BBPlayer

Length of output: 305


🏁 Script executed:

# Look for the database or transaction type definitions
rg "transaction\s*\(" apps/mobile/src -B 3 -A 8 | grep -A 10 -B 3 "async.*tx"

Repository: bbplayer-app/BBPlayer

Length of output: 17439


事务句柄未传入,导致操作脱离事务上下文。

第 305 行的回调未接收 tx 参数,第 306-308 行继续使用 withDB(this.db) 而非事务句柄。这使得 playlist / track / artist 的写入不在事务范围内,任一步失败时将留下部分写入的不一致数据。

应改为:

事务参数修复
-							const playlistId = await this.db.transaction(async () => {
-							const playlistSvc = this.playlistService.withDB(this.db)
-							const trackSvc = this.trackService.withDB(this.db)
-							const artistSvc = this.artistService.withDB(this.db)
+							const playlistId = await this.db.transaction(async (tx) => {
+							const playlistSvc = this.playlistService.withDB(tx)
+							const trackSvc = this.trackService.withDB(tx)
+							const artistSvc = this.artistService.withDB(tx)
📝 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
return ResultAsync.fromPromise(
this.db.transaction(async () => {
(async () => {
const playlistId = await this.db.transaction(async () => {
const playlistSvc = this.playlistService.withDB(this.db)
const trackSvc = this.trackService.withDB(this.db)
const artistSvc = this.artistService.withDB(this.db)
return ResultAsync.fromPromise(
(async () => {
const playlistId = await this.db.transaction(async (tx) => {
const playlistSvc = this.playlistService.withDB(tx)
const trackSvc = this.trackService.withDB(tx)
const artistSvc = this.artistService.withDB(tx)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/src/lib/facades/syncBilibiliPlaylist.ts` around lines 303 - 308,
The transaction callback passed to this.db.transaction is missing the
transaction handle and therefore playlistService/trackService/artistService are
using the root DB instead of the transaction; change the async callback
signature to accept the tx parameter (e.g., async (tx) => { ... }) and replace
withDB(this.db) calls with withDB(tx) for playlistService, trackService and
artistService inside that callback so all writes run within the same transaction
context (ensure the playlistId assignment remains inside the
transaction-returning block).

Comment on lines +81 to +89
function parseJsonObject<T>(content: string): T {
try {
return JSON.parse(content) as T
} catch {
const match = content.match(/\{[\s\S]*\}/)
if (!match) throw new Error('LLM 没有返回 JSON 对象')
return JSON.parse(match[0]) as T
}
}
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 the file exists and get its structure
fd -t f "llmSmartShuffleService" apps/mobile/src/lib/services/

Repository: bbplayer-app/BBPlayer

Length of output: 120


🏁 Script executed:

#!/bin/bash
# Read the file to examine the error handling patterns
wc -l apps/mobile/src/lib/services/llmSmartShuffleService.ts

Repository: bbplayer-app/BBPlayer

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Read the specific sections mentioned in the review (lines 81-89 and 204-211)
sed -n '75,95p' apps/mobile/src/lib/services/llmSmartShuffleService.ts
echo "---"
sed -n '200,220p' apps/mobile/src/lib/services/llmSmartShuffleService.ts

Repository: bbplayer-app/BBPlayer

Length of output: 1093


🏁 Script executed:

#!/bin/bash
# Search for all throw statements in the file
rg "throw\s+new\s+Error|throw\s+" apps/mobile/src/lib/services/llmSmartShuffleService.ts -n

Repository: bbplayer-app/BBPlayer

Length of output: 217


🏁 Script executed:

#!/bin/bash
# Check if neverthrow is imported or used in the file
rg "neverthrow|Result|from.*neverthrow" apps/mobile/src/lib/services/llmSmartShuffleService.ts

Repository: bbplayer-app/BBPlayer

Length of output: 273


服务层应使用 neverthrow Result 模式处理错误,不应直接抛异常。

parseJsonObject(第 86 行)和 callJson(第 205、211 行)直接抛出 Error,绕过了该目录约定的 neverthrow 错误处理模式。应改为这些函数返回 ResultResultAsync,由调用方(如 indexTrackBatchparsePreference)组合失败分支。按照编码规范:"在 Facades 和 Services 中使用 neverthrow Result 类型处理所有错误——不应直接抛异常"。

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

In `@apps/mobile/src/lib/services/llmSmartShuffleService.ts` around lines 81 - 89,
parseJsonObject and callJson currently throw Errors which violates the
service-layer convention to use neverthrow Results; change both functions to
return neverthrow Result<T, Error> or ResultAsync<T, Error> instead of throwing:
on JSON parse success return ok(value), on failure return err(new Error(...))
(for parseJsonObject include the fallback match logic but return err if still
invalid); ensure callers like indexTrackBatch and parsePreference consume/chain
these Results (using .map/.andThen/.mapErr or ResultAsync equivalents) so error
branches are composed rather than throwing. Use the existing function names
parseJsonObject and callJson so callers can swap to the Result-returning APIs
without changing call sites beyond handling the Result.

Comment on lines +190 to +202
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({
model: settings.llmModel.trim(),
temperature: 0.1,
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: system },
{ role: 'user', content: user },
],
}),
})
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:

cat -n apps/mobile/src/lib/services/llmSmartShuffleService.ts | sed -n '180,220p'

Repository: bbplayer-app/BBPlayer

Length of output: 1585


🏁 Script executed:

rg "callJson" apps/mobile/src/lib/services/llmSmartShuffleService.ts -A 3 -B 1

Repository: bbplayer-app/BBPlayer

Length of output: 675


🏁 Script executed:

head -n 50 apps/mobile/src/lib/services/llmSmartShuffleService.ts

Repository: bbplayer-app/BBPlayer

Length of output: 1130


🏁 Script executed:

grep -n "neverthrow\|Result" apps/mobile/src/lib/services/llmSmartShuffleService.ts | head -20

Repository: bbplayer-app/BBPlayer

Length of output: 289


🏁 Script executed:

rg "callJson" apps/mobile/src/lib/services/llmSmartShuffleService.ts -B 2 -A 8

Repository: bbplayer-app/BBPlayer

Length of output: 1065


🏁 Script executed:

cat -n apps/mobile/src/lib/services/llmSmartShuffleService.ts | sed -n '220,260p'

Repository: bbplayer-app/BBPlayer

Length of output: 1334


🏁 Script executed:

grep -n "try\|catch\|throw" apps/mobile/src/lib/services/llmSmartShuffleService.ts

Repository: bbplayer-app/BBPlayer

Length of output: 313


🏁 Script executed:

cat -n apps/mobile/src/lib/services/llmSmartShuffleService.ts | sed -n '320,345p'

Repository: bbplayer-app/BBPlayer

Length of output: 946


🏁 Script executed:

cat -n apps/mobile/src/lib/services/llmSmartShuffleService.ts | sed -n '345,380p'

Repository: bbplayer-app/BBPlayer

Length of output: 1380


🏁 Script executed:

ls -la apps/mobile/src/lib/services/

Repository: bbplayer-app/BBPlayer

Length of output: 890


🏁 Script executed:

head -n 100 apps/mobile/src/lib/services/playlistService.ts | grep -A 5 "import.*neverthrow\|Result"

Repository: bbplayer-app/BBPlayer

Length of output: 746


添加请求超时,并改用 neverthrow 进行错误处理。

该方法在请求卡住时会导致同步流程永久挂起,需要添加 AbortController 超时保护。同时,根据服务层编码规范,需要将错误处理改为 neverthrowResult 模式,而非直接抛异�常。

当前代码在行 205、211 和 82、86 中直接 throw Error,违反了服务层不应直接抛异常的要求。建议将 callJson 改为返回 ResultAsync,并将错误处理统一为 Result 模式。

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

In `@apps/mobile/src/lib/services/llmSmartShuffleService.ts` around lines 190 -
202, The fetch call in llmSmartShuffleService.ts must be protected with an
AbortController timeout and the service must stop throwing errors directly;
change the fetch block to use an AbortController with a configurable timeout and
call controller.abort() on timeout, and refactor error paths to use neverthrow's
ResultAsync/okAsync/errAsync instead of throwing; update the helper callJson
function to return ResultAsync (wrap fetch + JSON parsing errors into errAsync)
and replace all direct throw Error usages in this module (and call sites
referenced by callJson) with returning errAsync(...) or mapping to okAsync(...)
so the public API of functions like the LlmSmartShuffleService method returns
ResultAsync<ResultType, ErrorType>.

Comment on lines +260 to +298
const indexes = (response.tracks ?? [])
.map((item) => {
if (!item || typeof item !== 'object') return null
const row = item as Record<string, unknown>
const trackId = Number(row.trackId)
if (!Number.isFinite(trackId)) return null
const index: TrackTagIndex = {
trackId,
tags: normalizeTags(row.tags),
confidence:
typeof row.confidence === 'number'
? Math.max(0, Math.min(1, row.confidence))
: 0,
}
if (typeof row.reason === 'string') index.reason = row.reason
return index
})
.filter((item): item is TrackTagIndex => item !== null)

if (indexes.length === 0) return

const contextById = new Map(contexts.map((item) => [item.track.id, item]))
await db
.insert(schema.trackLlmTags)
.values(
indexes.map((index) => {
const context = contextById.get(index.trackId)
return {
trackId: index.trackId,
tags: index.tags,
confidence: index.confidence,
reason: index.reason,
model: settings.llmModel.trim(),
sourceType: context?.sourceType,
sourceId: context?.sourceId,
sourceSyncedAt: context?.sourceSyncedAt,
indexedAt: new Date(),
}
}),
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

不要信任模型回传的 trackId 作为写库主键。

这里会把 LLM 返回的 trackId 直接拿去 upsert;只要模型串号到任意已有 ID,就可能覆盖别的歌曲标签。写入前至少要过滤 contextById.has(index.trackId),更稳妥的是完全不要让模型决定主键,而是按请求顺序或本地映射回填。

最低限度先把越界的 `trackId` 过滤掉
-		const indexes = (response.tracks ?? [])
+		const contextById = new Map(contexts.map((item) => [item.track.id, item]))
+		const indexes = (response.tracks ?? [])
 			.map((item) => {
 				if (!item || typeof item !== 'object') return null
 				const row = item as Record<string, unknown>
 				const trackId = Number(row.trackId)
 				if (!Number.isFinite(trackId)) return null
@@
 			})
 			.filter((item): item is TrackTagIndex => item !== null)
+			.filter((item) => contextById.has(item.trackId))
 
 		if (indexes.length === 0) return
-
-		const contextById = new Map(contexts.map((item) => [item.track.id, item]))
📝 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 indexes = (response.tracks ?? [])
.map((item) => {
if (!item || typeof item !== 'object') return null
const row = item as Record<string, unknown>
const trackId = Number(row.trackId)
if (!Number.isFinite(trackId)) return null
const index: TrackTagIndex = {
trackId,
tags: normalizeTags(row.tags),
confidence:
typeof row.confidence === 'number'
? Math.max(0, Math.min(1, row.confidence))
: 0,
}
if (typeof row.reason === 'string') index.reason = row.reason
return index
})
.filter((item): item is TrackTagIndex => item !== null)
if (indexes.length === 0) return
const contextById = new Map(contexts.map((item) => [item.track.id, item]))
await db
.insert(schema.trackLlmTags)
.values(
indexes.map((index) => {
const context = contextById.get(index.trackId)
return {
trackId: index.trackId,
tags: index.tags,
confidence: index.confidence,
reason: index.reason,
model: settings.llmModel.trim(),
sourceType: context?.sourceType,
sourceId: context?.sourceId,
sourceSyncedAt: context?.sourceSyncedAt,
indexedAt: new Date(),
}
}),
const contextById = new Map(contexts.map((item) => [item.track.id, item]))
const indexes = (response.tracks ?? [])
.map((item) => {
if (!item || typeof item !== 'object') return null
const row = item as Record<string, unknown>
const trackId = Number(row.trackId)
if (!Number.isFinite(trackId)) return null
const index: TrackTagIndex = {
trackId,
tags: normalizeTags(row.tags),
confidence:
typeof row.confidence === 'number'
? Math.max(0, Math.min(1, row.confidence))
: 0,
}
if (typeof row.reason === 'string') index.reason = row.reason
return index
})
.filter((item): item is TrackTagIndex => item !== null)
.filter((item) => contextById.has(item.trackId))
if (indexes.length === 0) return
await db
.insert(schema.trackLlmTags)
.values(
indexes.map((index) => {
const context = contextById.get(index.trackId)
return {
trackId: index.trackId,
tags: index.tags,
confidence: index.confidence,
reason: index.reason,
model: settings.llmModel.trim(),
sourceType: context?.sourceType,
sourceId: context?.sourceId,
sourceSyncedAt: context?.sourceSyncedAt,
indexedAt: new Date(),
}
}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/src/lib/services/llmSmartShuffleService.ts` around lines 260 -
298, The LLM-returned trackId should not be trusted as the DB primary key:
before inserting into schema.trackLlmTags (the block building values from
indexes mapped from response.tracks) filter or remap entries using the local
context map (contextById) so you only insert tags for known contexts; e.g., only
keep indexes where contextById.has(index.trackId) or better yet, map each
response item to the context entry and use context.track.id (or request-order
mapping) as the authoritative trackId when constructing the insert payload to
avoid accidental overwrites.

Comment thread lefthook.yml
@@ -2,22 +2,17 @@ pre-commit:
parallel: 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 | 🟠 Major

格式化与 lint 并行执行存在竞态风险。

当前 parallel: true 下,format-codes 会改写并重新暂存文件,而 oxlint/eslint 可能已并行读取旧内容,导致 pre-commit 结果偶发失败或不一致。建议改为顺序执行。

🔧 建议修复
 pre-commit:
-  parallel: true
+  parallel: false
   commands:
     gitleaks:
       run: node scripts/lefthook-gitleaks.cjs

Also applies to: 6-15

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

In `@lefthook.yml` at line 2, The lefthook configuration currently sets parallel:
true which causes race conditions between the format-codes hook (which rewrites
and restages files) and the oxlint/eslint hooks; change the configuration to run
hooks sequentially by removing or setting parallel: false and ensure ordering so
that the format-codes hook runs before oxlint/eslint (apply the same change to
the other hook groups referenced around lines 6-15) to eliminate the timing/race
issue.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

✅ Actions performed

Reviews paused.

Generate LLM datasets during favorite sync.
Split large sort payloads to avoid context overflow.

Made-with: Cursor
@socket-security
Copy link
Copy Markdown

socket-security Bot commented Apr 29, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedexpo@​54.0.227710078100100
Addedexpo-image@​3.0.107810091100100
Updated@​types/​react@​19.2.9 ⏵ 19.1.171001007988100
Addedreact@​19.1.01001008497100
Addedtypescript@​5.9.3100100909990
Addedreact-native@​0.81.59110099100100

View full report

Improve local title-based tags for smart shuffle and clean up Android warning
sources that are tracked in the repo.

Made-with: Cursor
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.

2 participants