Commit Graph

326 Commits

Author SHA1 Message Date
hailin c9367ee22a fix: PDF attachments sent as document blocks instead of image blocks
PDF files were incorrectly wrapped as type:'image' content blocks,
causing Claude API to reject them as "Invalid image data".

- conversation-context.service: check mediaType for application/pdf,
  use type:'document' block (Anthropic native PDF support) instead
- claude-agent-sdk-engine: detect both 'image' and 'document' blocks
  when deciding to build multimodal SDK prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 05:27:41 -08:00
hailin 3025910095 ui: transparent compact AppBar (64dp → 44dp)
- AppBar background transparent, merges with scaffold for seamless look
- toolbarHeight reduced from 64dp to 44dp (~20dp screen space saved)
- scrolledUnderElevation: 0 prevents Material 3 shadow on scroll
- Icons 24→20px with VisualDensity.compact for tighter action buttons
- Title fontSize 16 w600, less visual weight
- Both dark and light themes updated consistently

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 05:20:23 -08:00
hailin ed39518a71 feat: floating pill input bar + auto-scroll on history load
Input area redesign (ChatGPT/Claude App style):
- Replace fixed bottom bar with floating pill overlay using Stack+Positioned
- Semi-transparent background (surface 92% opacity) with rounded corners (28px)
- Drop shadow for depth separation from content
- Remove inner TextField border (InputBorder.none) for cleaner look
- ListView bottom padding increased to 80px to leave room under the pill
- Input pill floats 12px from edges, 8px from bottom

History scroll fix:
- Add jump parameter to _scrollToBottom() for instant positioning
- When loading conversation history (empty→many messages), use jumpTo
  instead of animateTo to avoid incomplete scroll on large message lists
- Double-frame jumpTo ensures layout settles before final scroll position

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 05:15:18 -08:00
hailin 1f1bf18a75 fix: remove clipboard paste menu item, fix timeline line overlap, dim input placeholder
- Remove redundant "从剪贴板粘贴" option from attachment menu (long-press to paste natively)
- Remove super_clipboard dependency (no longer needed)
- Fix timeline vertical line overlapping icon nodes by using dynamic dotRadius
- Dim input field placeholder color to AppColors.textMuted

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 05:05:27 -08:00
hailin cfc0a97da7 fix: correct super_clipboard getFile API call signature
getFile requires two positional args: format and callback.
Wrapped in Completer for async/await usage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 04:45:19 -08:00
hailin 5f28605e13 feat: add clipboard paste, multi-image select, and file picker
- Add super_clipboard and file_picker dependencies
- Clipboard paste: reads PNG/JPEG image data from system clipboard
- Multi-image: pickMultiImage with remaining count limit
- File picker: supports images (jpg/png/gif/webp) and PDF files
- Updated attachment preview to show file icon for non-image types
- Bottom sheet now shows 4 options: gallery, camera, clipboard, file

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 04:32:16 -08:00
hailin 9b467924a0 fix: add attachments JSONB column to conversation_messages schema
Update migration files to include the attachments column for
multimodal image storage. Also add ALTER TABLE migration for
existing deployments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 04:18:35 -08:00
hailin 2c657e2b4c fix: use NestJS native useBodyParser instead of direct express import
The direct `import * as express from 'express'` caused a
MODULE_NOT_FOUND error in the Docker production image since express
is only available as a transitive dependency via @nestjs/platform-express.
Use NestExpressApplication.useBodyParser() which is the official NestJS API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 04:01:54 -08:00
hailin b9c3bfdf91 feat: add multimodal image support to Claude Agent SDK engine
- SDK engine now constructs AsyncIterable<SDKUserMessage> with image
  content blocks when attachments are present in conversationHistory,
  using the SDK's native multimodal prompt format
- CLI engine logs a warning when images are detected, since the `-p`
  flag only accepts text (upstream Claude CLI limitation)
- Both SDK and API engines now fully support multimodal image input

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 03:38:59 -08:00
hailin e4c2505048 feat: add multimodal image input with streaming markdown optimization
Two major features in this commit:

1. Streaming Markdown Rendering Optimization
   - Replace deprecated flutter_markdown with gpt_markdown (active, AI-optimized)
   - Real-time markdown rendering during streaming (was showing raw syntax)
   - Solid block cursor (█) instead of AnimationController blink
   - 80ms token throttle buffer reducing rebuilds from per-token to ~12.5/sec
   - RepaintBoundary isolation for markdown widget repaints
   - StreamTextWidget simplified from StatefulWidget to StatelessWidget

2. Multimodal Image Input (camera + gallery + display)
   - Flutter: image_picker for gallery/camera, base64 encoding, attachment
     preview strip with delete, thumbnails in sent messages
   - Data layer: List<String>? → List<Map<String, dynamic>>? for structured
     attachment payloads through datasource/repository/usecase
   - ChatAttachment model with base64Data, mediaType, fileName
   - ChatMessage entity + ChatMessageModel both support attachments field
   - Backend DTO, Entity (JSONB), Controller, ConversationContextService
     all extended to receive, store, and reconstruct Anthropic image
     content blocks in loadContext()
   - Claude API engine skips duplicate user message when history already
     ends with multimodal content blocks
   - NestJS body parser limit raised to 10MB for base64 image payloads
   - Android CAMERA permission added to manifest
   - Image.memory uses cacheWidth/cacheHeight for memory efficiency
   - Max 5 images per message enforced in UI

Data flow:
  ImagePicker → base64Encode → ChatAttachment → POST body →
  DB (JSONB) → loadContext → Anthropic image content blocks → Claude API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 03:24:17 -08:00
hailin 89f0f6134d fix: resolve bottom overflow issues in chat page timeline rendering
Three root causes fixed:

1. TimelineEventNode: Replaced IntrinsicHeight (which forces intrinsic
   height calculation on unbounded content) with CustomPaint-based
   _TimelineLinePainter that draws vertical lines based on actual
   rendered widget size. Also added maxLines/ellipsis to label text
   and mainAxisSize.min on inner Column.

2. ApprovalActionCard: Changed countdown + action buttons layout from
   Row with Spacer (which requires infinite width) to Wrap with
   spacing, preventing horizontal overflow on narrow screens.

3. AnimatedCrossFade in _CollapsibleCodeBlock and _CollapsibleThinking:
   Wrapped with ClipRect and added sizeCurve: Curves.easeInOut to
   prevent the outgoing child from extending beyond parent bounds
   during the cross-fade transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 01:38:37 -08:00
hailin 50dbb641a3 fix: comprehensive hardening of agent task cancel/inject/approve flows
6 rounds of systematic audit identified and fixed 14 bugs across
backend controller and Flutter client:

## Backend (agent.controller.ts)

Security & Tenant Isolation:
- Add @TenantId + ForbiddenException check to cancelTask, injectMessage,
  approveCommand — all 4 write endpoints now enforce tenant isolation
- Add tenantId check on session reuse in executeTask to prevent
  cross-tenant session hijacking

Architecture & Correctness:
- Extract shared runTaskStream() from inline fire-and-forget block,
  used by both executeTask and injectMessage to reduce duplication
- Use session.engineType (not getActiveEngine()) in cancelTask,
  injectMessage, approveCommand — fixes wrong-engine-cancel when
  global engine config is switched after task creation
- Add concurrent task prevention: executeTask checks for existing
  RUNNING task on same session and cancels it before starting new one
- Add runningTasks Map to track task promises, awaitTaskCleanup()
  helper with 3s timeout for inject to wait for partial text save
- captureSdkSessionId() captures SDK session ID into metadata
  without DB save (callers persist), preventing fire-and-forget race

Cancel/Reject Improvements:
- cancelTask: idempotent (returns early if already CANCELLED/COMPLETED),
  session stays 'active' (was 'cancelled'), emits cancelled WS event
- approveCommand reject: session stays 'active' (was 'cancelled'),
  now emits cancelled WS event so Flutter stream listeners clean up
- approveCommand approved: collect text events and save assistant
  response to conversation history on completion (was missing)

Minor:
- task.result! non-null assertion → task.result ?? 'Unknown error'
- Add findRunningBySessionId() to TaskRepository

## Flutter

API Contract Fix:
- approveCommand: route changed from /api/v1/ops/approvals/:id/approve
  to /api/v1/agent/tasks/:id/approve with {approved: true} body
- rejectCommand: route changed from /api/v1/ops/approvals/:id/reject
  to /api/v1/agent/tasks/:id/approve with {approved: false} body

Resource Management:
- ChatNotifier.dispose() now disconnects WebSocket to prevent
  connection leak when navigating away from chat

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 22:20:46 -08:00
hailin d5f663f7af feat: inject-message support for mid-stream task interruption
Backend (agent-engine.port.ts):
- Add `cancelled` event type: emitted when a task is cancelled (user-initiated
  or injection), so Flutter can close the old stream cleanly
- Add `task_info` event type: emitted after inject to pass the new taskId to
  the client, enabling cancel/re-inject on the replacement task

Flutter (features/chat/):
- ChatState: track current `taskId` alongside `sessionId`; clear on completion
  or error
- Handle `TaskInfoEvent`: update taskId in state when server issues a new task
- Handle `CancelledEvent`: treat as stream termination (agentStatus → idle)
- MessageType.interrupted: new UI node (warning style) for mid-stream cancels
- _inject(): send text as an inject request while streaming; backend cancels
  the current task and starts a new one with the injected message
- Input area: during streaming, hint changes to "追加指令...", Enter key calls
  _inject() instead of _send(), and both inject-send + stop buttons are shown
- isAwaitingApproval kept separate from isStreaming so approval flow is not
  blocked by inject mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 21:33:50 -08:00
hailin ce4e7840ec fix: route AgentSkillService to per-tenant schema to match SDK engine
Previously AgentSkillService wrote skills to public.agent_skills (TypeORM
entity with tenantId column filter), while ClaudeAgentSdkEngine read from
it0_t_{tenantId}.skills (per-tenant schema). The two tables were never
connected, so any skill added via the CRUD API was invisible to the agent.

This fix:
- Rewrites AgentSkillService to use DataSource + raw SQL against the
  per-tenant schema it0_t_{tenantId}.skills
- Maps API fields: script→content, enabled→is_active
- Removes AgentSkillRepository and AgentSkill entity from module (no longer needed)
- CRUD API response shape is unchanged (fields mapped back to script/enabled)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 21:21:36 -08:00
hailin f5d9b1f04f feat: add app upgrade system with self-hosted APK update support
- Add core/updater module: version checker, download manager (resumable + SHA-256),
  APK installer, app market detector, self-hosted updater with progress dialogs
- Add Android native MethodChannels for APK installation and market detection
- Add FileProvider config and REQUEST_INSTALL_PACKAGES permission
- Wire UpdateService singleton into main.dart initialization
- Add auto-check on home entry with cooldown + app resume detection
- Add manual "检查更新" button and dynamic version display in settings
- Fix chat page: bottom overflow, bash spinner persistence, collapsible results
- Merge standing orders into tasks page as second tab

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:35:01 -08:00
hailin 3278696f4c feat: inject tenant skills into agent system prompt
Load active skills from the tenant's schema `skills` table and append
them to the system prompt before passing to the Claude Agent SDK. This
closes the gap where skills existed in the DB but were never surfaced
to the agent during task execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:42:15 -08:00
hailin 3ed20cdf08 refactor: clean up agent SSH setup after fixing host-local routing
- Remove iproute2/NET_ADMIN (no longer needed)
- Remove ip route hack from entrypoint.sh
- rwa-colocation-2 server record updated to use Docker gateway IP
  since 14.215.128.96 is a host-local NIC on the IT0 server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:11:44 -08:00
hailin 836d4d2a03 fix: add iproute2 to container for ip route command
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:06:35 -08:00
hailin ae7d9251ec fix: add route for host-local IP (14.215.128.96) in agent container
14.215.128.96 is bound to a host NIC (enp5s0) and unreachable from
Docker bridge via default NAT. Add NET_ADMIN + ip route via gateway.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:05:30 -08:00
hailin 0dea3f82bc fix: mount correct SSH key (rwadurian_ed25519) in agent-service
The IT0 server has its own id_ed25519 which differs from the local
key that's authorized on RWADurian servers. Use a dedicated key file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 13:05:01 -08:00
hailin f0ad6e09e6 fix: move entrypoint.sh to project root (deploy/ is in .dockerignore)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:14:31 -08:00
hailin bad7f4802d fix: use root entrypoint to copy SSH key then drop to appuser
The bind-mounted SSH key is owned by host uid (1000/node) but the
service runs as appuser (uid 1001). Use su-exec in entrypoint.sh
to copy the key as root, fix ownership, then drop privileges.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:13:55 -08:00
hailin 329916e1f6 fix: correct SSH key permissions in agent-service container
Mount host key to /tmp/host-ssh-key (read-only), then copy to
appuser's .ssh directory with correct ownership at container start.
Fixes "Permission denied" due to uid mismatch on bind mount.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:00:02 -08:00
hailin 795e8a11c5 feat: enable SSH access from agent-service container
- Add openssh-client to Dockerfile.service (alpine)
- Create .ssh directory with correct permissions for appuser
- Mount host SSH key into agent-service container (read-only)

This allows the Agent SDK to SSH into managed servers using the Bash tool.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:55:54 -08:00
hailin 36d36acad4 fix: set tenantId when creating credentials in inventory-service
The createCredential method was missing the tenantId assignment,
causing a NOT NULL constraint violation on the credentials table.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 10:52:14 -08:00
hailin 51b348e609 feat: complete tenant member management (CRUD + delete tenant)
Backend: add 5 missing endpoints to TenantController:
- DELETE /tenants/:id (deprovision schema + cleanup)
- GET /tenants/:id/members (query tenant schema users)
- PATCH /tenants/:id/members/:memberId (change role)
- DELETE /tenants/:id/members/:memberId (remove member)
- PUT /tenants/:id (alias for frontend compatibility)

Frontend: add member actions to tenant detail page:
- Role column changed to dropdown selector
- Added remove member button with confirmation
- Added updateMember and removeMember mutations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 10:00:09 -08:00
hailin bc7e32061a fix: improve voice call reconnection robustness
Server side (session_router.py):
- /reconnect now accepts sessions in "active" state (not just "disconnected")
- When client reconnects to an active session, the old WebSocket/pipeline is
  automatically replaced when the new WebSocket connects
- Only truly terminal states (e.g. "ended") return 409

Flutter side (agent_call_page.dart):
- Distinguish terminal errors (404 session gone, 409 ended) from transient
  errors (network timeout, server unreachable) in reconnect loop
- Terminal errors break immediately instead of wasting retry attempts
- Extract _connectWebSocket() helper for cleaner reconnect flow
- Add DioException handling for proper HTTP status code inspection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 07:33:34 -08:00
hailin 57fabb4653 fix: set interleaved=true for PcmPlayer streaming playback
FlutterSoundPlayer.feedUint8FromStream() requires interleaved mode.
With interleaved=false, every feed() call threw:
  "Cannot feed with UInt8 with non interleaved mode"

- feedUint8FromStream (Uint8List) → requires interleaved: true
- feedFromStream (Float32List) → requires interleaved: false
Since we feed raw PCM bytes (Uint8List), interleaved must be true.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 06:59:06 -08:00
hailin e706a4cdc7 fix: enable simultaneous playback + recording in voice call
Root cause: PcmPlayer called openPlayer() without audio session config,
so Android defaulted to earpiece-only mode. When the mic was actively
recording, playback was silently suppressed — the agent's TTS audio was
sent successfully over WebSocket but never reached the speaker.

Changes:

1. PcmPlayer (pcm_player.dart):
   - Added audio_session package for proper audio session management
   - Configure AudioSession with playAndRecord category so mic + speaker
     work simultaneously
   - Set voiceCommunication usage to enable Android hardware AEC (echo
     cancellation) — prevents feedback loops when speaker is active
   - defaultToSpeaker routes output to loudspeaker instead of earpiece
   - Restored setSpeakerOn() method stub (used by UI toggle)

2. AgentCallPage (agent_call_page.dart):
   - Fixed fire-and-forget bug: _pcmPlayer.feed() returns Future but was
     called without await, causing interleaved feedUint8FromStream calls
   - Added _feedChain serializer to guarantee sequential audio feeding

3. Dependencies:
   - Added audio_session package to pubspec.yaml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 06:48:16 -08:00
hailin 75083f23aa debug: add TTS send_bytes logging to pipeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 06:19:18 -08:00
hailin 5be7f9c078 fix: resample OpenAI TTS output from 24kHz to 16kHz WAV
OpenAI TTS returns 24kHz audio which Android MediaPlayer can't play
via FlutterSound's pcm16WAV codec. Request raw PCM and resample to
16kHz before wrapping in WAV header, matching the local TTS format.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 05:38:39 -08:00
hailin 4456550393 feat: lazy-load local TTS/STT models on first request
Local /synthesize and /transcribe endpoints now auto-load Kokoro/Whisper
models on first call instead of returning 503 when not pre-loaded at
startup. This allows switching between Local and OpenAI providers in the
Flutter test page without requiring server restart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 04:38:49 -08:00
hailin 7b71a4f2fc fix: properly close WebSocket with subscription cancel + fire-and-forget
Root cause: IOWebSocketChannel.sink.close() can hang indefinitely
(dart-lang/web_socket_channel#185). Previous fix used unawaited close
but didn't cancel the stream subscription, so the old listener could
still push events to _messageController.

Fix: Extract _closeCurrentConnection() that:
1. Cancels StreamSubscription first (stops duplicate events immediately)
2. Fire-and-forget sink.close(goingAway) (frees underlying socket)

This follows the workaround recommended in the official issue tracker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 03:45:43 -08:00
hailin 45eb6bc453 fix: use unawaited close to prevent WebSocket reconnect hang
The await on sink.close() blocks indefinitely when the server doesn't
respond to the close handshake. Use fire-and-forget with unawaited()
so the new connection can proceed immediately.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 03:41:13 -08:00
hailin 3185438f36 fix: close previous WebSocket before opening new connection
When sending a second message in the same session, the old WebSocket
connection was not closed, causing both connections to subscribe to the
same session room. This resulted in each text event being received twice,
producing garbled/duplicated output text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 03:37:16 -08:00
hailin e02b350043 fix: create /data/claude-tenants dir with appuser ownership in Dockerfile
Without this, the SDK engine fails to create tenant HOME directories
because the Docker volume mount point doesn't exist and appuser lacks
write permissions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 02:52:57 -08:00
hailin cc0f06e2be feat: SDK engine native resume with per-tenant HOME isolation
Replace prompt-prefix workaround with SDK's native resume mechanism.
Each tenant gets isolated HOME directory (/data/claude-tenants/{tenantId})
to prevent cross-tenant session file mixing. SDK session IDs are persisted
in session.metadata for cross-request resume support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 02:27:38 -08:00
hailin 2403ce5636 feat: multi-turn conversation context management with session history UI
Implement DB-based conversation message storage (engine-agnostic) that
works across both Claude API and Agent SDK engines. Add ChatGPT/Claude-style
conversation history drawer in Flutter with date-grouped session list,
session switching, and new chat functionality.

Backend: entity, repository, context service, migration 004, session/message
API endpoints. Flutter: ConversationDrawer, sessionId flow from backend
response via SessionInfoEvent, session list/switch/delete support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 19:04:35 -08:00
hailin 7cda482e49 fix: simplify _dioBinary in voice test page to avoid interceptor conflicts
Remove shared interceptors from the binary Dio instance to prevent
request dedup/retry interceptors from interfering with audio downloads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:57:58 -08:00
hailin c02c2a9a11 feat: add OpenAI TTS/STT provider support in voice pipeline
- Add STT_PROVIDER/TTS_PROVIDER config (local or openai) in settings
- Pipeline uses OpenAI API for STT/TTS when provider is "openai"
- Skip loading local models (Kokoro/faster-whisper) when using OpenAI
- VAD (Silero) always loads for speech detection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 09:27:38 -08:00
hailin f7d39d8544 fix: use theme-aware colors in voice test page for dark mode readability
Replace hardcoded Colors.grey with Theme.of(context).colorScheme for
result containers and status text so they're readable in both light
and dark themes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 09:21:06 -08:00
hailin f8f0d17820 fix: disable SSL verification for OpenAI proxy with self-signed cert
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 08:59:06 -08:00
hailin d43baed3a5 feat: add OpenAI TTS/STT API endpoints for comparison testing
- Add openai package to voice-service requirements
- Add /api/v1/test/tts/synthesize-openai (tts-1/tts-1-hd/gpt-4o-mini-tts)
- Add /api/v1/test/stt/transcribe-openai (gpt-4o-transcribe/whisper-1)
- Add OPENAI_API_KEY and OPENAI_BASE_URL env vars to voice-service
- Flutter test page: SegmentedButton to toggle Local/OpenAI provider
- All endpoints maintain same response format for easy comparison

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 07:20:03 -08:00
hailin ac0b8ee1c6 fix: rewrite voice test page using flutter_sound for both record and play
- Remove record package dependency, use FlutterSoundRecorder instead
- Use permission_handler for microphone permission (already in pubspec)
- Proper temp file path via path_provider
- Cleanup temp files after upload
- Single package (flutter_sound) handles both recording and playback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 05:41:10 -08:00
hailin d4783a3497 fix: use temp directory path for audio recording instead of empty string
The record package requires a valid file path. Empty string caused
ENOENT (No such file or directory) on Android.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 05:39:07 -08:00
hailin 5d4fd96d43 feat: streaming claude-api engine, engineType override, fix voice test page
- Claude API engine now uses streaming API (messages.stream) for real-time
  text delta output instead of waiting for full response
- Agent controller accepts optional engineType body parameter to allow
  callers (e.g. voice pipeline) to select a specific engine
- Fix voice_test_page.dart compilation error: replace audioplayers (not
  installed) with flutter_sound (already in pubspec.yaml)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 05:30:11 -08:00
hailin 6e832c7615 feat: add voice I/O test page in Flutter settings
- TTS: text input → Kokoro synthesis → audio playback
- STT: long-press record → faster-whisper transcription
- Round-trip: record → STT → TTS → playback
- Added /api/v1/test route to Kong gateway for voice-service
- Accessible from Settings → 语音 I/O 测试

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 05:16:10 -08:00
hailin 0bd050c80f feat: add STT test and round-trip test to voice test page
- STT: record from mic or upload audio file → faster-whisper transcription
- Round-trip: record → STT → TTS → playback (full pipeline test)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 05:08:00 -08:00
hailin 0aa20cbc73 feat: add temporary TTS test page at /api/v1/test/tts
Browser-accessible page to test text-to-speech synthesis without
going through the full voice pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 05:06:02 -08:00
hailin 740f8f5f88 fix: sentence splitting bug in voice pipeline TTS streaming
When the first punctuation mark appeared before _MIN_SENTENCE_LEN chars,
the regex search would always find it first and skip it, permanently
blocking all subsequent sentence splits. Fix by advancing search_start
past short matches instead of breaking out of the loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 05:03:05 -08:00