Commit Graph

163 Commits

Author SHA1 Message Date
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
hailin 7ac753ada4 fix: add ANTHROPIC_BASE_URL to agent-service for proxy access
The agent-service was missing the ANTHROPIC_BASE_URL environment variable,
causing the Claude Agent SDK to call api.anthropic.com directly instead of
going through the proxy at 67.223.119.33, resulting in 403 Forbidden errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:49:27 -08:00
hailin 79fae0629e chore: upgrade claude-agent-sdk to ^0.2.52
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:12:03 -08:00
hailin 2a150dcff5 fix: prevent error event from overriding completed status in controller
Add finished guard so that once a task reaches completed/error terminal
state, subsequent events don't flip the status back.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:49:21 -08:00
hailin 6876ec569b fix: remove ANTHROPIC_API_KEY from agent-service to use subscription mode
Default to OAuth subscription billing via ~/.claude/.credentials.json
instead of consuming API key credits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:43:09 -08:00
hailin e20936ee2a feat: collapsible thinking node in chat timeline
Thinking content auto-expands while streaming, auto-collapses when done.
User can toggle with "Thinking ∨" button, matching Claude Code VSCode UX.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:34:58 -08:00
hailin 8e4bd573f4 fix: deduplicate text events from SDK stream_event and assistant message
SDK sends text both via stream_event deltas (token-level) and assistant
message (complete block). Track hasStreamedText flag per session to skip
duplicate text extraction from assistant messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:31:48 -08:00
hailin 65e68a0487 feat: streaming TTS — synthesize per-sentence as agent tokens arrive
Replace batch TTS (wait for full response) with streaming approach:
- _agent_generate → _agent_stream async generator (yield text chunks)
- _process_speech accumulates tokens, splits on sentence boundaries
- Each sentence is TTS'd and sent immediately while more tokens arrive
- First audio plays within ~1s of agent response vs waiting for full text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:14:22 -08:00
hailin aa2a49afd4 fix: extract text from assistant message + fix event data parsing
Root causes found:
1. SDK engine only emitted 'completed' without 'text' events because
   mapSdkMessage skipped text blocks in 'assistant' messages (assumed
   stream_event deltas would provide them, but SDK didn't send deltas)
2. Voice pipeline read evt_data.data.content but engine events are flat
   (evt_data.content) — so even if text arrived, it was never extracted

Fixes:
- Extract text/thinking blocks from assistant messages in SDK engine
- Fix voice pipeline to read content directly from evt_data, not nested

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:01:25 -08:00
hailin a7b42e6b98 feat: add detailed logging to agent engine and task controller
Log every SDK message type, event emission, and stream lifecycle
to diagnose why text events are missing in voice-agent flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:56:09 -08:00
hailin 0dbe711ed3 feat: add detailed logging to voice pipeline (STT/Agent/TTS timing)
Log timestamps, content, and event details at each pipeline stage
to help diagnose voice-agent integration issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:47:21 -08:00
hailin 1d5c834dfe feat: add event buffering to agent WS gateway for late subscribers
Buffer stream events when no WS clients are subscribed yet, then replay
them when a client subscribes. This eliminates the race condition where
events are lost between task creation and WS subscription.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:41:38 -08:00
hailin 370e32599f fix: subscribe to agent WS before creating task to avoid race condition
The engine stream could emit text events before the voice pipeline
subscribed, causing all text to be lost.  Now we connect and subscribe
first, then POST the task.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:35:57 -08:00
hailin 82d12a5ff5 feat: mount voice model cache volumes to avoid re-downloading on restart
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:28:28 -08:00