Commit Graph

467 Commits

Author SHA1 Message Date
hailin 87110804f2 chore(flutter): add firebase_messaging and huawei_push plugin registration
- pubspec.lock: resolve firebase_core 3.15.2, firebase_messaging 15.2.10, huawei_push 6.11.0+300
- GeneratedPluginRegistrant.java: register new push notification plugins

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 19:38:47 -07:00
hailin 835b4dd60a chore(voice-agent): remove CROWDED_ROOM and CITY_AMBIENCE from thinking sounds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 10:35:34 -07:00
hailin 38a9f94b45 fix(voice-instance): 3 robustness fixes for OpenClaw voice routing
A. terminateVoiceSession: skip voiceSessionManager.terminateSession for
   instance-mode sessions (no SDK loop was started for them)
B. agent.py: call start_voice_session() when instance_id is set regardless
   of engine_type, so _voice_session_started=True and inject mode is used
C. voice/inject: check instance.status === 'running' before firing to bridge

All changes are additive; iAgent paths are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 10:08:10 -07:00
hailin 54257ac944 feat(voice): route instance voice calls through OpenClaw bridge
When a user places a voice call from an instance chat page (AgentInstanceChatPage),
the call is now routed through the OpenClaw bridge instead of iAgent's
VoiceSessionManager SDK loop. iAgent voice behavior is completely unchanged.

Changes:
- Flutter: ChatPage accepts agentInstanceId, passes to AgentCallPage
- Flutter: AgentCallPage sends instance_id in livekit/token request body
- Flutter: AgentInstanceChatPage passes instance.id as agentInstanceId
- voice-service: livekit_token.py includes instance_id in dispatch metadata
- voice-agent: agent.py reads instance_id from metadata, passes to LLM
- voice-agent: agent_llm.py stores _instance_id, sends instanceId in voice/start
- agent-service voice/start: stores instanceId in session metadata; skips
  VoiceSessionManager for instance mode (iAgent path unchanged)
- agent-service voice/inject: detects instanceId in session metadata and
  fire-and-forgets to OpenClaw bridge; callback via openclaw-app-callback
  emits text WS events that voice-agent collects for TTS playback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 10:00:15 -07:00
hailin 265730acb2 fix(agent): fix openclaw-app-callback Tenant context not initialized
The callback endpoint is public (no JWT), so TenantAwareRepository
calls failed with 'Tenant context not initialized'.

Fix:
1. Include tenantId in callbackData sent to the bridge
2. Wrap all DB operations in TenantContextService.run() using the
   tenantId from callbackData
3. Emit WS events immediately (no tenant context needed) so Flutter
   receives the reply even if DB update fails

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 09:30:58 -07:00
hailin a8c72aca76 fix(app): fix instance chat routing to iAgent instead of OpenClaw
chatRepositoryProvider and sendMessageUseCaseProvider were not overridden
in AgentInstanceChatPage's ProviderScope, causing ChatNotifier.sendMessage
to use the parent scope's ChatRemoteDatasource (iAgent endpoint
POST /api/v1/agent/tasks) instead of AgentInstanceChatDatasource
(OpenClaw endpoint POST /api/v1/agent/instances/:id/tasks).

Fix: override both providers in the child scope so the full call chain
(sendMessage → SendMessage → ChatRepositoryImpl → createTask) routes
to the correct instance-specific endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 09:13:43 -07:00
hailin 73bd32b247 fix(app): hide iAgent FAB on My Agents tab to prevent confusion
The floating iAgent chat button was visible on the /my-agents page,
causing users to accidentally open the iAgent chat instead of their
own agent instance's chat. Hide the FAB when the My Agents tab is
active (currentIndex == 1).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 09:00:09 -07:00
hailin 5d94ed4e8b fix(android): add dontwarn for Google Play Core missing classes
Flutter references Play Core split-install APIs that aren't bundled in
standard APK builds — suppress R8 errors with dontwarn.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 08:33:40 -07:00
hailin 61e7533d64 fix(android): add ProGuard rules to suppress HMS missing class errors
R8 fails on missing optional HMS dependencies (HiAnalytics, libcore,
BouncyCastle). Add dontwarn rules and wire proguard-rules.pro to release
build type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 08:22:11 -07:00
hailin 62f1a88557 fix(flutter): fix push_service.dart compile errors for huawei_push 6.x
- Add hide RemoteMessage,Importance to huawei_push import (conflicts with
  firebase_messaging and flutter_local_notifications)
- Replace removed HuaweiPush/Event API with Push.getTokenStream/onMessageReceivedStream
- Replace non-existent DioClient() with standalone Dio instance;
  init() now accepts optional authToken parameter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 08:18:38 -07:00
hailin e20e8c7ad7 fix(android): migrate OPPO & vivo push SDKs to new API versions
- OPPO HeytapPush V3.7.1: fix ICallBackResultService package (mode→callback),
  update init() signature (remove appKey/appSecret), update all callback
  method signatures, remove deprecated onReceiveMessage/onNotificationTapped
- vivo Push v4.1.3.0: replace OpenIMInterface+OpenClientPushManager with
  OpenClientPushMessageReceiver+PushClient, async getRegId

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 08:09:22 -07:00
hailin 7c7fbab3ef fix(android): add AGP classpath for Huawei AGConnect plugin compatibility
Huawei agcp plugin requires com.android.tools.build:gradle in buildscript
classpath but new Flutter plugin DSL only declares it via settings.gradle.kts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:49:32 -07:00
hailin f1bf0a6abc fix(wecom): remove 6-char hex false-positive intercept
The /^[0-9A-F]{6}$/ check in handleMessage intercepted any message
that happened to be 6 uppercase hex chars (e.g. colour codes, words
like FACADE) and told the user their binding code was expired.

Removed: non-matching resolveBindCode result now falls through to
normal agent routing, matching the original behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:02:07 -07:00
hailin 61b2778ff0 fix(wecom): 4 bugs — watchdog follower, atomic getdel, Redis bindcodes, enter_session dedup
Bug 1 — Watchdog doesn't track followers:
  lastPollAt = Date.now() moved before leader check. All poll()
  invocations update the timestamp, so if a follower's loop dies
  the watchdog fires after WATCHDOG_THRESHOLD_MS and restarts it.

Bug 2 — Non-atomic GetDel for cross-instance recovery:
  Replaced GET + DEL with atomic GETDEL (Redis 6.2+, ioredis v5).
  Two instances can no longer both recover the same callback reply.

Bug 3 — Binding codes stored in per-process memory:
  generateBindingCode() now async; stores in Redis:
    wecom:bindcode:{CODE}        → instanceId  (TTL 15min)
    wecom:bindcode:inst:{instId} → CODE        (reverse lookup)
  resolveBindCode() uses GETDEL atomically, then deletes reverse key.
  Falls back to in-memory Map when Redis is unavailable.
  Old code for same instance is revoked on regenerate.
  handleMessage updated: resolveBindCode() replaces Map.get();
  6-char hex pattern with no match now returns expired-code hint.
  Controller wecomGenerateBindCode now awaits generateBindingCode().

Bug 4 — enter_session events not deduplicated:
  handleEnterSession now receives msgId from the event.
  redisDedup(msgId) called before sending welcome message — prevents
  duplicate welcomes on WeCom retransmission or cursor reset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 06:46:17 -07:00
hailin b3180b0727 fix(wecom): move dedup to Redis (shared across instances)
Replace in-memory dedup Map with Redis SET NX EX:
  - Key: wecom:dedup:{msgId}, TTL=600s (auto-expires, no manual cleanup)
  - SET NX returns 'OK' on first write (process), null on duplicate (skip)
  - Shared across all agent-service instances — no inter-process duplicates
  - Fails open (return true) if Redis is unavailable — avoids silent drops
  - Removed dedup Map and its periodicCleanup loop

WeCom router is now 10/10 robust:
  cursor persistence, token mutex, distributed leader lease (fail-closed),
  exponential backoff, watchdog, send retry, Redis dedup, Redis cross-instance
  callback recovery, health endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 06:19:29 -07:00
hailin e87924563c fix(wecom): health endpoint, fail-closed lease, Redis cross-instance recovery
Fix 1 — Observability (health endpoint):
  WecomRouterService.getStatus() returns { enabled, isLeader, lastPollAt,
  staleSinceMs, consecutiveErrors, pendingCallbacks, queuedUsers }.
  GET /api/v1/agent/channels/wecom/health exposes it.

Fix 2 — Leader lease fail-closed:
  tryClaimLeaderLease() catch now returns false instead of true.
  DB failure → skip poll, preventing multi-master on DB outage.
  isLeader flag tracked for health status.

Fix 3 — Cross-instance callback recovery via Redis:
  routeToAgent() stores wecom:pending:{msgId} → externalUserId in Redis
  with 200s TTL before waiting for the bridge callback.
  resolveCallbackReply() is now async:
    Fast path  — local pendingCallbacks (same instance, 99% case)
    Recovery   — Redis GET → send reply directly to WeChat user
  onModuleDestroy() cleans up Redis keys on graceful shutdown.
  wecom/bridge-callback handler updated to await resolveCallbackReply.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 06:07:58 -07:00
hailin 0d5441f720 fix(wecom): token mutex, leader lease, backoff, watchdog
Four additional robustness fixes:

1. **Token refresh mutex** — tokenRefreshPromise deduplicates concurrent
   refresh calls. All callers share one in-flight HTTP request instead
   of each firing their own, eliminating the race condition.

2. **Distributed leader lease** — service_state table used for a
   TTL-based leader election (LEADER_LEASE_TTL_S=90s). Only one
   agent-service instance polls at a time; others skip until the lease
   expires. Lease auto-released on graceful shutdown.

3. **Exponential backoff** — consecutive poll errors increment a counter;
   next delay = min(10s × 2^(n-1), 5min). Prevents log spam and
   reduces load during sustained WeCom API outages. Counter resets on
   any successful poll.

4. **Watchdog timer** — setInterval every 2min checks lastPollAt.
   If poll loop has been silent for >5min, clears the timer and
   reschedules immediately, recovering from any silent crash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 05:54:42 -07:00
hailin 9e466549c0 fix(wecom): cursor persistence, send retry, enter_session welcome
Three robustness fixes for the WeCom Customer Service router:

1. **Cursor persistence** — sync_msg cursor now stored in
   public.service_state (auto-created via CREATE TABLE IF NOT EXISTS).
   Survives service restarts; no more duplicate message processing.

2. **send_msg retry** — sendChunkWithRetry() retries once after 2s
   on any API error (non-zero errcode or network failure). Lost
   replies due to transient WeChat API errors are now recovered.

3. **enter_session welcome** — WeCom fires an enter_session event
   (origin=0, msgtype=event) when a user opens the chat for the
   first time. Now handled: bound users get a welcome-back message,
   unbound users get step-by-step onboarding instructions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 05:37:24 -07:00
hailin 978c534a7e fix(push): fix TypeScript Map type inference error in OfflinePushService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 04:50:11 -07:00
hailin bc48be1c95 feat(push): log push provider config status on startup
Print ✓/✗ for each platform (FCM/HMS/MI/OPPO/VIVO) so missing credentials
are immediately visible in container logs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 04:48:26 -07:00
hailin 155133a2d6 feat(push): add HMS agconnect-services.json (Huawei Push Kit config)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 04:01:15 -07:00
hailin 3bc35bad64 feat(push): add offline push notification system (FCM + HMS + Mi + OPPO + vivo)
## Backend (notification-service)
- Add `device_push_tokens` table (migration 014) — stores per-user tokens per
  platform (FCM/HMS/MI/OPPO/VIVO) with UNIQUE constraint on (user_id, platform, device_id)
- Add `DevicePushTokenRepository` with upsert/delete/targeting queries
  (by userId, tenantId, plan, tags, segment)
- Add push provider interface with `sendBatch(tokens, message): BatchSendResult`
  returning `invalidTokens[]` for automatic DB cleanup
- Add FCM provider — OAuth2 via RS256 JWT, chunked concurrency (max 20 parallel),
  detects UNREGISTERED/404 as invalid tokens
- Add HMS provider — native batch API (1000 tokens/chunk), OAuth2 token cache
  with 5-min buffer, detects code 80100016
- Add Xiaomi provider — `/v3/message/regids` batch endpoint (1000/chunk),
  parses `bad_regids` field
- Add OPPO provider — single-send with Promise-based mutex to prevent concurrent
  auth token refresh races
- Add vivo provider — `/message/pushToList` batch endpoint, mutex same as OPPO,
  parses `invalidMap`
- Add `OfflinePushService` — groups tokens by platform, sends concurrently,
  auto-deletes invalid tokens; fire-and-forget trigger after notification creation
- Add `DevicePushTokenController` — POST/DELETE `/api/v1/notifications/device-token`
- Wire offline push into `NotificationAdminController` and `EventTriggerService`
- Add Kong route for device-token endpoint (JWT required)
- Add all push provider env vars to docker-compose notification-service

## Flutter (it0_app)
- Add `PushService` singleton — detects OEM (Huawei/Xiaomi/OPPO/vivo/FCM),
  initialises correct push SDK, registers token with backend
  - FCM: full init with background handler, foreground local notifications,
    tap stream, iOS APNs support
  - HMS: `HuaweiPush` async token via `onTokenEvent`, no FCM fallback on failure
    (Huawei without GMS cannot use FCM)
  - Mi/OPPO/vivo: MethodChannel bridge to Kotlin receivers; handler set before
    `getToken()` call to avoid race
  - `_initialized` guard prevents double-init on hot-restart
  - `static Stream<void> onNotificationTap` for router navigation
- Add Kotlin OEM bridge classes: `MiPushReceiver`, `OppoPushService`,
  `VivoPushReceiver` — forward token/message/tap events to Flutter via MethodChannel
- Update `MainActivity` — register all three OEM MethodChannels; OEM credentials
  injected from `BuildConfig` (read from `local.properties` at build time)
- Update `build.gradle.kts` — add Google Services + HMS AgConnect plugins,
  BuildConfig fields for OEM credentials, `fileTree("libs")` for OEM AARs
- Update `android/build.gradle.kts` — add buildscript classpath for GMS + HMS,
  Huawei Maven repo
- Update `AndroidManifest.xml` — HMS service, Xiaomi receiver + services,
  vivo receiver; OPPO handled via AAR manifest merge
- Add OEM SDK AARs to `android/app/libs/`:
  MiPush 7.9.2, HeytapPush 3.7.1, vivo Push 4.1.3
- Add `google-services.json` (Firebase project: it0-iagent, package: com.iagent.it0_app)
- Add `firebase_core ^3.6.0`, `firebase_messaging ^15.1.3`, `huawei_push ^6.11.0+300`
  to pubspec.yaml
- Add `ApiEndpoints.notificationDeviceToken` endpoint constant

## Ops
- Add FCM_PROJECT_ID, FCM_CLIENT_EMAIL, FCM_PRIVATE_KEY (+ HMS/Mi/OPPO/vivo placeholders)
  to `.env.example` with comments pointing to each OEM's developer portal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 02:42:34 -07:00
hailin 0e4159c2fd fix(my-agents): scope instance list to current user
GET /instances returned all tenant instances for admin accounts,
causing cross-user agent visibility. Changed to
GET /instances/user/:userId so each user only sees their own agents.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 23:44:09 -07:00
hailin c9ee93fffd feat(instance-chat): full multimodal attachment support via OpenClaw bridge
After verifying that the OpenClaw gateway's chat.send WebSocket RPC
accepts an 'attachments' array (confirmed from openclaw/openclaw source
and documentation), implement end-to-end image/file attachment support
for instance chat:

Bridge (openclaw-client.ts):
- chatSendAndWait() now accepts optional `attachments[]` parameter
- Passes attachments to chat.send RPC only when non-empty

Bridge (index.ts):
- /task-async accepts `attachments[]` from request body
- Forwards to chatSendAndWait unchanged

Backend (agent.controller.ts):
- executeInstanceTask() accepts IT0 attachment format
  { base64Data, mediaType, fileName? }
- Converts to OpenClaw format { name, mimeType, media: "data:..." }
- Saves attachments to conversation history via contextService
- Forwards to bridge via bridgeAttachments spread

Flutter (agent_instance_chat_remote_datasource.dart):
- createTask() now includes attachments in POST body when present

Flutter (chat_page.dart):
- Reverted Fix 5 (disabled button) — attachment button fully enabled
  in instance mode since the bridge now supports it

Attachment format (OpenClaw wire):
  { name: string, mimeType: string, media: "data:<mime>;base64,<data>" }

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 21:18:14 -07:00
hailin ea3cbf64a5 feat(agent): complete instance-chat robustness fixes (Fix 2-6)
Fix 2 — Callback timeout wiring:
- Store callbackTimer in pendingCallbackTimers Map after creation
- handleOpenClawAppCallback clears the timer immediately on arrival,
  preventing spurious "timeout" errors when the bridge replies in time

Fix 3 — Provider scope isolation:
- Override agentStatusProvider and robotStateProvider in child ProviderScope
  so the robot avatar/FAB reflects the instance chat state, not iAgent's

Fix 4 — Voice routing to OpenClaw:
- AgentInstanceChatDatasource.sendVoiceMessage() now calls transcribeAudio()
  then routes the transcript through instance-specific createTask() endpoint,
  ensuring voice messages reach the user's OpenClaw agent, not iAgent

Fix 5 — Attachment UI in instance mode:
- Attachment button shown as disabled (onPressed: null) with explanatory
  tooltip ("附件功能暂不支持智能体对话") when agentName != null
- Prevents misleading UX where attachments appear to work but are silently
  dropped by the OpenClaw bridge

Fix 6 — DB schema template:
- Add agent_instance_id UUID NULL to agent_sessions table in migration 002
  (tenant schema template) so new tenants get the column from creation
- Add covering index idx_agent_sessions_instance for efficient instance queries

All TypeScript and Flutter analyze checks pass clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 19:49:36 -07:00
hailin 8865985019 feat(agent-instance-chat): 实现用户与自己的 OpenClaw 智能体直接对话功能
## 功能概述
用户可在「我的智能体」页面点击运行中的 OpenClaw 实例卡片,
直接打开与该智能体的专属对话页面,完整复用 iAgent 的聊天 UI
(流式输出、工具时间线、审批卡片、语音输入等),同时保证
iAgent 对话完全不受影响。

## 架构设计
- 使用 Riverpod ProviderScope 子作用域覆盖 chatRemoteDatasourceProvider
  / chatProvider / sessionListProvider,实现 iAgent 与实例对话的
  provider 完全隔离,无任何共享状态。
- OpenClaw bridge 采用已有的 /task-async 异步回调模式:
    Flutter → POST /api/v1/agent/instances/:id/tasks(立即返回 sessionId/taskId)
    → 订阅 WS /ws/agent(等待事件)
    → Bridge 完成后 POST /api/v1/agent/instances/openclaw-app-callback(公开端点)
    → 后端发 WS text+completed 事件 → Flutter 收到回复
- 每个实例的会话通过 agent_sessions.agent_instance_id 字段隔离,
  会话抽屉只显示当前实例的历史记录。

## 后端变更
### packages/shared/database/src/migrations/013-add-agent-instance-id-to-sessions.sql
- 新增迁移:ALTER TABLE agent_sessions ADD COLUMN agent_instance_id UUID NULL
- 为按实例过滤会话建立索引

### packages/services/agent-service/src/domain/entities/agent-session.entity.ts
- 新增可选字段 agentInstanceId: string(对应 agent_instance_id 列)
- iAgent 会话该字段为 null;实例聊天会话存储对应的 instance UUID

### packages/services/agent-service/src/infrastructure/repositories/session.repository.ts
- 新增 findByInstanceId(tenantId, agentInstanceId) 方法
- 用于 GET /instances/:id/sessions 按实例过滤会话列表

### packages/services/agent-service/src/interfaces/rest/controllers/agent.controller.ts
新增三个端点(注意:已知存在以下待修复问题,见后续 fix commit):
1. POST /api/v1/agent/instances/:instanceId/tasks
   - 校验 instance 归属(userId 匹配)和 running 状态
   - 创建会话(engineType='openclaw',携带 agentInstanceId)
   - 保存用户消息到 conversation_messages 表
   - 向 OpenClaw bridge POST /task-async,sessionKey=it0:{sessionId}
   - 立即返回 { sessionId, taskId },Flutter 订阅 WS 等待回调
2. GET /api/v1/agent/instances/:instanceId/sessions
   - 返回该实例的会话列表(含 title/status/时间戳)
3. POST /api/v1/agent/instances/openclaw-app-callback(公开端点,无 JWT)
   - bridge 完成后回调此端点
   - 成功:发 WS text+completed 事件,保存 assistant 消息,更新 task 状态
   - 失败/超时:发 WS error 事件,标记 task 为 FAILED
- 注入 AgentInstanceRepository 依赖
- 新增私有方法 createInstanceSession()

### packages/gateway/config/kong.yml
- 新增 openclaw-app-callback-public service(无 JWT 插件)
- 路由:POST /api/v1/agent/instances/openclaw-app-callback
- 必须在 agent-service 之前声明,确保路由优先匹配(同 wecom-public 模式)

## Flutter 变更
### it0_app/lib/core/config/api_endpoints.dart
- 新增 instanceTasks(instanceId) 和 instanceSessions(instanceId) 静态方法

### it0_app/lib/features/chat/presentation/pages/chat_page.dart
- 新增可选参数 agentName(默认 null = iAgent 模式)
- agentName != null 时:AppBar 显示智能体名称,隐藏语音通话按钮
- 不传 agentName 时行为与原来完全一致,iAgent 功能零影响

### it0_app/lib/features/my_agents/presentation/pages/my_agents_page.dart
- _InstanceCard 新增 onTap 回调参数
- 卡片用 Material+InkWell 包裹,支持圆角水波纹点击效果
- 新增 _openInstanceChat() 顶层函数:
    running → 滑入式跳转到 AgentInstanceChatPage
    其他状态 → SnackBar 提示(部署中/已停止/错误)
- 导入 AgentInstanceChatPage

### it0_app/lib/features/agent_instance_chat/(新建功能模块)
data/datasources/agent_instance_chat_remote_datasource.dart:
- AgentInstanceChatDatasource implements ChatRemoteDatasource
- 通过组合模式包装 ChatRemoteDatasource 委托所有通用操作
- 覆盖 createTask → POST /api/v1/agent/instances/:id/tasks
- 覆盖 listSessions → GET /api/v1/agent/instances/:id/sessions(仅当前实例会话)

presentation/pages/agent_instance_chat_page.dart:
- AgentInstanceChatPage(instance: AgentInstance)
- ProviderScope 子作用域覆盖三个 provider 实现完全隔离:
    chatRemoteDatasourceProvider → AgentInstanceChatDatasource
    chatProvider → 独立 ChatNotifier 实例(与 iAgent 零共享)
    sessionListProvider → 仅当前实例的会话列表
- child: ChatPage(agentName: instance.name) 完整复用 UI

## 已知待修复问题(下一个 commit)
1. [安全] 鉴权检查逻辑:if (userId && ...) 应为 if (!userId || ...)
2. [可靠性] fetch 未处理 HTTP 4xx/5xx 错误,任务可能永久挂起
3. [可靠性] bridge 回调无超时机制,bridge 崩溃后任务永久 RUNNING
4. [UX] robotStateProvider 未在子 ProviderScope 覆盖,头像动画反映 iAgent 状态
5. [UX] 实例聊天附件 UI 未禁用,上传附件被静默丢弃
6. [UX] 语音消息在实例模式下错误路由到 iAgent 引擎(非 OpenClaw)
7. [DB] 002 模板未加 agent_instance_id 列,新租户缺失此字段

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 19:30:38 -07:00
hailin 647df6e42f feat(wecom): add WeChat Customer Service channel — sync_msg polling + code binding + bridge callback 2026-03-09 10:54:36 -07:00
hailin 233c1c77b2 fix(agent): revert operator-sees-all, restore per-user isolation
Operators now only see their own instances (same as regular users).
Admin role retains superuser view. Orphaned running instances were
reassigned to hailin via DB update.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 09:58:00 -07:00
hailin 4f9f456f85 fix(agent): operator role can see all agent instances 2026-03-09 09:13:24 -07:00
hailin 29c433c7c3 fix(agent): scope instance list to requesting user (multi-user isolation)
GET /api/v1/agent/instances was returning all instances regardless of user.
Now decodes JWT: non-admin users only see their own instances; admins see all.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 08:58:28 -07:00
hailin f186c57afb fix(agent): decode JWT directly to get userId for system prompt
req.user is never populated in agent-service (Kong verifies JWT, no Passport strategy).
This caused userId to always be undefined → system prompt had no 'Current User ID' →
Claude used tenant slug 'shenzhengj' as userId → DB error 'invalid input syntax for
type uuid'.

Fix: decode JWT payload from Authorization header (no signature verify needed — Kong
already verified it) to extract sub (user UUID) for both AgentController and
VoiceSessionController.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 08:51:28 -07:00
hailin da6bfbf896 fix(auth): add name to JWT payload, fix phone-user session restore
JWT payload was missing 'name' field — phone-invited users showed
empty name after app restart (session restore from JWT).
Also added phone fallback in Flutter _decodeUserFromJwt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 08:23:26 -07:00
hailin 4b2b3dca0c fix(app): make AuthUser.email nullable, add phone field
Phone-invited users have null email — casting null to String crashed login.
email: String → String?, added phone: String? to AuthUser and AuthUserEntity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 08:20:13 -07:00
hailin 1cf502ef91 fix(app): allow phone number in password login field
Phone-invited users register with phone+password.
Changed identifier field from email-only to email/phone,
removed @ validation so phone numbers pass through.
Backend already auto-detects email vs phone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 08:06:29 -07:00
hailin 1ccdbc0526 feat(invite): show App download page after phone-invite registration
Phone-invited users are mobile App users, not web admin users.
After accepting a phone invitation, display App download QR + APK link
instead of redirecting to /dashboard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 08:01:42 -07:00
hailin d73f07d688 fix(sms): remove url param from invite SMS template (SMS_501956050 has no url var)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 07:36:54 -07:00
hailin 6291d6591e fix(feishu): read message_type instead of msg_type (SDK field name mismatch)
Feishu @larksuiteoapi/node-sdk uses message_type, not msg_type (which is DingTalk).
This caused all incoming messages to be treated as non-text, returning
'我目前只能处理文字消息' for every message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 07:27:29 -07:00
hailin eb2d73bb7e fix(auth-service): add full Aliyun SMS env vars to docker-compose
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 07:22:05 -07:00
hailin 733e6525e3 fix(auth-service): pass ALIYUN_SMS_INVITE_TEMPLATE_CODE env var to container
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 07:16:17 -07:00
hailin 4a00baa0e3 fix(web-admin): add Next.js proxy route for /api/app/version/check
Fixes QR code not showing on my-org and register pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 07:13:29 -07:00
hailin afc1ae6fbe feat(voice): randomly pick thinking sound from all 7 built-in clips per session
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 06:56:31 -07:00
hailin 38bea33074 fix(flutter): voice OAuth sheet shows correct channel (Feishu vs DingTalk)
The _showOAuthBottomSheet title/subtitle were hardcoded to 钉钉. Now detects
channel from the URL (feishu.cn → 飞书, else → 钉钉) and shows correct text
and button color (#3370FF for Feishu, #1677FF for DingTalk).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 06:48:16 -07:00
hailin 79d6e0b98a feat(invite): support phone number invitation with SMS notification
- TenantInvite entity: email nullable + phone field added
- createInvite() auto-detects email vs phone, routes to email/SMS
- SmsService: add sendInviteSms() with ALIYUN_SMS_INVITE_TEMPLATE_CODE
- acceptInvite(): handle phone-based invites (uniqueness check + insert)
- my-org page: email/phone toggle on invite form
- /invite/[token] page: display phone or email from invite info
- DB migration: phone column added, email made nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 06:42:49 -07:00
hailin e2057bfe68 feat(feishu): add Feishu OAuth trigger for voice sessions
- Add POST /sessions/:sessionId/feishu/oauth-trigger endpoint (mirrors DingTalk)
  which emits oauth_prompt WS event so Flutter opens the Feishu authorization
  page automatically instead of asking the user to enter a bind code
- Update SystemPromptBuilder: voice sessions now use the Feishu OAuth trigger
  endpoint; text sessions still use the code-based flow as fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 06:31:45 -07:00
hailin 75f20075f6 fix(auth-service): pass EMAIL_* env vars into container via docker-compose
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 06:25:42 -07:00
hailin 7555f1ad5a feat(register): move app download banner to top with QR code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 06:18:16 -07:00
hailin 146b396427 feat(invite): send email notification on invite + QR codes in user management
- Add EmailService (nodemailer/SMTP) with invite email HTML template
- createInvite() now fires email notification after saving (fire-and-forget)
- my-org page: add App download QR code + invite link QR code panels
- Install react-qr-code in web-admin, nodemailer in auth-service

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 06:01:22 -07:00
hailin d9a785d49d fix(iagent): make dingtalk/feishu endpoint separation explicit in system prompt
Add CRITICAL note and clear IF/ELSE branching so Claude never calls
dingtalk endpoints for feishu binding or vice versa.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 05:57:38 -07:00
hailin 6d1e31dd36 feat(iagent): add Feishu to channel binding flow in system prompt
After creating an instance, iAgent now asks user to choose:
钉钉 / 飞书 / 都绑定 / 跳过
- DingTalk: existing OAuth card push flow
- Feishu: bind-code flow (user sends code to Feishu bot)
- Also adds Feishu status/unbind API references

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 05:51:21 -07:00
hailin 83ed55ce1c feat(flutter): add Feishu OAuth binding UI — mirrors DingTalk flow
- AgentInstance model: add feishuUserId field
- Instance card: show 飞书 binding badge (blue #3370FF) alongside DingTalk badge
- Context menu: add 绑定飞书 / 重新绑定飞书 / 解绑飞书 options
- _FeishuBindSheet: full OAuth-first binding sheet with polling, code fallback,
  countdown timer, success/expired/error states — same UX pattern as DingTalk

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 05:44:11 -07:00