Commit Graph

201 Commits

Author SHA1 Message Date
hailin 13f2d68754 feat(ux): agent list refresh + OAuth keep-alive + deploy token fix
Flutter:
- my_agents_page: refresh agent list on every My Agents tab tap
  (ref.invalidate in ScaffoldWithNav.onDestinationSelected)
- chat_page + my_agents_page: activate AudioSession before launching OAuth
  browser so iOS keeps network connections alive in background; deactivate
  when app resumes or binding polling completes

agent-service deploy:
- Write openclaw.json with correct gateway token and auth-profiles.json
  with API key BEFORE starting the container, so OpenClaw and bridge
  always agree on the auth token (fixes token_mismatch on new deployments)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 13:26:05 -07:00
hailin 5cf72c4780 feat(dingtalk+bridge): event-based agent reply + greeting on binding
openclaw-bridge:
- index.ts: /task endpoint now calls chatSendAndWait() with idempotencyKey
  (removes broken timeoutSeconds param; uses caller-supplied msgId for dedup)
- openclaw-client.ts: added onEvent() subscription + chatSendAndWait() that
  subscribes to 'chat' WS events, waits for state='final' matching runId,
  and extracts text from the message payload

dingtalk-router:
- After OAuth binding completes, sends a proactive greeting to the user via
  DingTalk batchSend API (/v1.0/robot/oToMessages/batchSend) introducing the
  agent by name and explaining what it can do

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 13:18:52 -07:00
hailin 54e6f13405 fix(dingtalk): bridge 请求参数错误导致消息无法转发给小龙虾
问题:routeToAgent 调用 OpenClaw bridge /task 时传了 timeoutSeconds
(bridge schema 不认识),且缺少必须字段 idempotencyKey,导致 bridge
返回 INVALID_REQUEST,机器人沉默不回复。

修复:
- 移除 timeoutSeconds(不是 bridge API 参数)
- 改用 msg.msgId 作为 idempotencyKey(每条消息唯一,满足 bridge 要求)

根因定位:
  docker logs openclaw-83cc9ac3 显示
  "invalid chat.send params: must have required property 'idempotencyKey';
   at root: unexpected property 'timeoutSeconds'"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 11:53:52 -07:00
hailin 64499a5d86 feat(dingtalk): 小龙虾招募全语音/文字引导流程 + OAuth 一键授权卡片
## 功能说明
用户通过语音或文字说「帮我招募一只小龙虾」,iAgent 全程引导完成
OpenClaw 实例创建 + 钉钉 OAuth 一键授权绑定。

## 核心设计
- 语音场景 (claude_agent_sdk): Claude 通过 Bash/wget 调用内部 HTTP
  端点触发 OAuth,绕开 ToolExecutor 限制,两引擎均兼容
- 文字场景 (claude_api): 使用 initiate_dingtalk_binding 自定义工具,
  通过 uiEvent 机制传递 OAuth URL

## agent-service 变更
- agent-engine.port.ts: EngineStreamEvent 联合类型新增 oauth_prompt
- allowed-tools-resolver.service.ts: initiate_dingtalk_binding 加入
  ALL_SDK_TOOLS / admin / operator 工具白名单
- tool-executor.ts: 新增 executeInitiateDingTalkBinding(),调用内部
  oauth/init 端点获取 OAuth URL,返回 uiEvent
- claude-api-engine.ts: 在 tool_result 之后检查 result.uiEvent 并
  yield 出去;buildToolDefinitions 注册 initiate_dingtalk_binding schema
- system-prompt-builder.ts:
  - SystemPromptContext 新增 sessionId? 字段
  - 语音 session (sessionId 存在) → Step 3 使用 wget 调用
    POST /sessions/{sessionId}/dingtalk/oauth-trigger(两引擎通用)
  - 文字 session (无 sessionId) → Step 3 调用 initiate_dingtalk_binding
    工具(claude_api 专用)
- voice-session.controller.ts:
  - 注入 AgentStreamGateway / DingTalkRouterService / AgentInstanceRepository
  - startVoiceSession: 提前确定 sessionId,在 build() 前传入,使系统
    提示能内嵌正确的端点 URL
  - 新增 POST :sessionId/dingtalk/oauth-trigger — 无 JWT(内部端点,
    由 Claude Bash 工具调用),sessionId 作为能力令牌;生成 OAuth URL
    并通过 gateway.emitStreamEvent 直接推送 oauth_prompt 事件到 WS 流

## voice-agent 变更
- agent.py: 构造 AgentServiceLLM 时传入 room=ctx.room
- agent_llm.py:
  - __init__ 增加 room 参数,存储为 self._room
  - 新增 _publish_oauth_prompt(evt_data): null-safe,通过 LiveKit
    publish_data(topic="oauth_prompt") 推送到 Flutter
  - _do_inject_voice / _do_inject / _do_stream_voice / _do_stream:
    处理 oauth_prompt 事件 → asyncio.create_task(_publish_oauth_prompt)
  - 替换已弃用的 asyncio.ensure_future / get_event_loop().create_task
    → asyncio.create_task(Python 3.10+ 兼容)

## Flutter 变更
- agent_call_page.dart: DataReceivedEvent 监听 topic="oauth_prompt",
  解析 url/instanceName,弹出 _showOAuthBottomSheet(深色主题,🦞
  图标,「立即授权」按钮 launchUrl externalApplication)
- stream_event.dart: 新增 OAuthPromptEvent(url, instanceId, instanceName)
- stream_event_model.dart: toEntity() 新增 'oauth_prompt' case
- chat_message.dart: MessageType 枚举新增 oauthPrompt
- chat_providers.dart: _handleStreamEvent 新增 OAuthPromptEvent case,
  生成 type=oauthPrompt 的 ChatMessage(metadata 含 url/instanceName)
- chat_page.dart: 新增 oauthPrompt 时间线节点 + _OAuthPromptCard 组件
  (「立即授权」按钮,launchUrl externalApplication);import url_launcher

## 修复的关键 Bug
1. [严重] initiate_dingtalk_binding 只对 claude_api 有效,语音默认用
   claude_agent_sdk → 新 wget 端点两引擎均可用
2. [严重] 文字聊天页面不处理 oauth_prompt 事件(静默丢弃)→ 补全
   Flutter 4 处代码(entity/model/provider/page)
3. [中]   _publish_oauth_prompt 缺 local_participant null 检查 → 已修复
4. [轻]   asyncio.ensure_future / get_event_loop() 弃用警告 → 已修复

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 11:22:06 -07:00
hailin 3d626aebb5 feat(dingtalk): OAuth one-tap binding + voice tool + public Kong route
- DingTalk binding UX replaced with OAuth one-tap flow:
  - GET /api/v1/agent/channels/dingtalk/oauth/init returns OAuth URL
  - GET /api/v1/agent/channels/dingtalk/oauth/callback (public, no JWT)
    exchanges code+state for openId, saves binding, returns HTML page
  - oauthStates Map with 10-min TTL; state validated before exchange
- msg.senderId (openId) aligned with OAuth openId for consistent routing
- CODE_TTL_MS extended from 5→15 min (fallback code method preserved)
- Kong: dingtalk-oauth-public service declared before agent-service
  so callback path matches without JWT plugin
- Voice sessions: use stored session.systemPrompt + voice rules;
  allowedTools includes Bash so Claude can call internal APIs
- Flutter _DingTalkBindSheet: OAuth-first UX with code-based fallback
  phases: idle→loadingOAuth→waitingOAuth→success + polling every 2s
- docker-compose: IT0_BASE_URL env var for agent-service (redirect URI)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 09:09:00 -07:00
hailin 2d0bdbd27f feat(agent): voice-triggered DingTalk binding + GET instances by user
- Add GET /api/v1/agent/instances/user/:userId endpoint so Claude can
  look up the caller's agent instances without knowing the ID upfront
- Update SystemPromptBuilder DingTalk section with centralized binding
  flow (one-time code via iAgent DingTalk bot, no per-instance creds)
- VoiceSessionController.startVoiceSession now extracts userId from JWT
  and builds a full iAgent system prompt (userId + DingTalk instructions)
  so Claude knows who is speaking and how to call the binding API
- VoiceSessionManager.executeTurn now uses the session's stored system
  prompt (base context + voice rules) and allows the Bash tool so Claude
  can call internal APIs via wget during voice conversations

User flow: speak "帮我绑定钉钉" → Claude lists instances → generates
code via POST /api/v1/agent/channels/dingtalk/bind/:id → speaks code
letter-by-letter → user sends code in DingTalk → binding completes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 08:49:13 -07:00
hailin db0e1f1439 fix(dingtalk): robustness pass — 5 bugs fixed, stability 10/10
Critical fixes:
- ws.on('message') fully wrapped in try/catch — uncaught exception in
  wsSend() no longer propagates to EventEmitter boundary and crashes process
- wsSend() helper: checks readyState === OPEN before send(), never throws
- Stale-WS guard: close/message events from old WS ignored after reconnect
  (ws !== this.ws check); terminateCurrentWs() closes old WS before new one
- Queue tail: .catch(() => {}) appended to guarantee promise always resolves,
  preventing permanently dead queue tail from silently dropping future tasks
- DISCONNECT frame handler: force-close + reconnect immediately

High fixes:
- sessionWebhookExpiredTime unit auto-detection: values < 1e11 treated as
  seconds (×1000), values >= 1e11 treated as ms — prevents always-blocked reply
- httpsPost response capped at 256 KB to prevent memory spike on bad response

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 08:22:08 -07:00
hailin 8751c85881 feat(dingtalk): unified DingTalk bot router with binding flow
- Add DingTalkRouterService: maintains single DingTalk Stream WS
  connection, handles binding codes, routes messages to agent containers
- Add AgentChannelController: POST bind/:id, GET status/:id, POST unbind/:id
- Add findByDingTalkUserId() to AgentInstanceRepository
- Add dingTalkUserId field to AgentInstance entity + migration 011
- Register DingTalkRouterService + AgentChannelController in AgentModule
- Add IT0_DINGTALK_CLIENT_ID/SECRET env vars to docker-compose.yml
- Flutter: DingTalk bind UI in _InstanceCard (bottom sheet with code
  display, countdown, auto-poll, open DingTalk deep link, bound badge)

Robustness improvements in DingTalkRouterService:
  - Concurrent connect guard (connecting flag)
  - Periodic cleanup timer for dedup/rateWindows/bindingCodes maps
  - Non-text message graceful reply
  - Empty senderStaffId guard
  - serverHost null guard before bridge call
  - unref() cleanup timers from event loop

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 08:12:27 -07:00
hailin 5ec6f113cd feat(agent): DingTalk channel binding support in instance controller + system prompt
- agent-instance.controller.ts: accept dingTalkClientId/dingTalkClientSecret
  in POST /instances body, forward to deploy service
- system-prompt-builder.ts: add DingTalk 5-step binding guide for iAgent
  so the AI can walk users through connecting their DingTalk account

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 05:12:13 -07:00
hailin 90f11fc572 fix(agent): pass IT0_AGENT_SERVICE_URL env var to openclaw container
supervisord uses %(ENV_IT0_AGENT_SERVICE_URL)s expansion which fails
if the var is not present, crashing the entire supervisor process.
Add AGENT_SERVICE_PUBLIC_URL config and inject it via docker run -e.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 05:01:55 -07:00
hailin 3790284bc9 fix(agent): use wget instead of curl for internal API calls (curl not in container)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 03:29:53 -07:00
hailin b8979d521e fix(agent): AgentInstanceRepository use DataSource directly, not TenantAwareRepository
agent_instances is in public schema — no tenant context needed.
Fixes 'Tenant context not initialized' when iAgent calls internal API via Bash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 03:19:01 -07:00
hailin 5c5c365736 chore(agent): add empty prisma dir to fix Docker build COPY step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 03:10:19 -07:00
hailin 49ad47cf59 fix(agent): non-null assertion for serverHost/sshUser in deployToUserServer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 03:07:42 -07:00
hailin b87cebf465 feat(agent): inject userId into system prompt + fix agent-instance nullable columns
- SystemPromptBuilder: add userId/userEmail to context, expose internal API curl commands for OpenClaw creation
- agent.controller.ts: extract userId from JWT, build system prompt via SystemPromptBuilder so iAgent knows current user
- agent.module.ts: register SystemPromptBuilder as provider
- agent-instance.entity.ts: make serverHost/sshUser nullable (pool mode doesn't set these upfront)
- DB: ALTER TABLE agent_instances DROP NOT NULL on server_host/ssh_user

Now iAgent can create 小龙虾 instances autonomously when user asks in natural language.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 03:05:15 -07:00
hailin 8262d3f8e3 fix(auth-service): add prisma/.gitkeep for Dockerfile.service COPY step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 00:20:28 -08:00
hailin 4df699348f feat(referral): add user-level personal circle + points system
- Migration 011: 4 new tables (user_referral_codes, user_referral_relationships,
  user_point_transactions, user_point_balances)
- Referral service: user-level repositories, use cases, and controller endpoints
  (GET /me/user, /me/circle, /me/points; POST /internal/user-register)
- Admin endpoints: user-circles, user-points, user-balances listing
- Auth service: fire-and-forget user referral registration on signup
- Flutter: 2-tab UI (企业推荐 / 我的圈子) with personal code card,
  points balance, circle member list, and points history
- Web admin: 2 new tabs (用户圈子 / 用户积分) with transaction ledger and balance leaderboard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 00:18:17 -08:00
hailin 66a454df93 fix(notification-service): add empty prisma dir to satisfy Dockerfile.service COPY step
notification-service does not use Prisma/ORM (raw SQL via TypeORM DataSource).
Dockerfile.service unconditionally copies the prisma/ directory from builder stage,
which fails with 'not found' when the directory doesn't exist.
Adding a .gitkeep placeholder so the COPY succeeds; the subsequent
prisma generate step is skipped because no schema.prisma is present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 22:54:33 -08:00
hailin 5ff8bda99e feat(notification): 完整站内消息推送体系 (Phase 1-4)
## Phase 1 — 精准推送基础
- 新增 notification-service 微服务 (port 3013)
- DB迁移 007: notifications, notification_reads, notification_tenant_targets 表
- DB迁移 008: tenant_tags, tenant_tag_assignments, notification_user_targets 表
  + notifications 表新增 target_tag_ids/target_tag_logic/target_plans/target_statuses/channel_key 字段
- auth-service: TenantTagController — 租户标签 CRUD + 批量分配 (9个接口)
- notification-service 支持 7 种推送目标类型:
  ALL / SPECIFIC_TENANTS / SPECIFIC_USERS / BY_TENANT_TAG(ANY|ALL) / BY_PLAN / BY_TENANT_STATUS / BY_SEGMENT
- Web Admin: /tenant-tags 标签管理页 + 通知表单全面扩展

## Phase 2 — 通知频道与用户偏好
- DB迁移 009: notification_channels (6个预置频道) + user_notification_preferences
  + notification_segment_members 表 (Phase 4 人群包)
- notification-service: ChannelRepository + NotificationChannelController
  (频道 CRUD + 用户偏好 API,强制频道不可关闭)
- Web Admin: /notification-channels 频道管理页
- Flutter: NotificationPreferencesPage — 用户按频道 toggle 订阅,profile页新增入口

## Phase 3 — Campaign 活动与数据分析
- DB迁移 010: notification_campaigns, campaign_execution_log, notification_event_log 表
- notification-service: CampaignRepository + CampaignAdminController
  (ONCE/RECURRING调度, 排期/取消/删除, 发送量/阅读率统计)
- Web Admin: /campaigns 推送活动管理页 (状态机 + 数据统计弹窗)

## Phase 4 — 事件触发与人群包
- EventTriggerService: Redis Stream 消费者,监听并自动创建通知:
  billing.payment_failed / billing.quota_warning / tenant.registered / alert.fired
- SegmentRepository + SegmentAdminController (全量同步/增量添加/删除)
- Web Admin: /segments 人群包管理页 (成员管理 + ETL全量替换)

## 基础设施
- Kong: 新增 notification-service 服务 + 6条路由 + JWT插件
- Docker Compose: 新增 notification-service 容器 (13013:3013)
- notification-service 新增 ioredis 依赖 (Redis Stream 消费)

## Flutter (APK需手动编译)
- 新增路由: /notifications/inbox, /notifications/preferences
- 新增: NotificationInboxPage, NotificationPreferencesPage
- 新增: ForceReadNotificationDialog (强制阅读拦截弹窗)
- profile页: 站内消息行(未读角标) + 通知偏好设置入口

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 22:33:40 -08:00
hailin 252dc59bed fix(referral-service): add empty prisma dir for Dockerfile.service compatibility
Dockerfile.service copies prisma/ from each service; referral-service uses
TypeORM instead of Prisma, so an empty placeholder is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 21:30:43 -08:00
hailin 2f17266455 feat(referral): implement full referral system across all layers
## Overview
完整实现 IT0 推荐裂变系统,涵盖后端微服务、基础设施、Flutter 移动端、Next.js Web Admin。

## Backend — referral-service (packages/services/referral-service/)

### 架构设计
- 遵循 billing-service 模式:DataSource 直接访问 public schema(非 TenantAwareRepository)
- 推荐单元为租户级别(tenant-level),不区分租户内用户
- 最大 2 层推荐深度(L1 直接推荐 / L2 间接推荐)
- 推荐码格式:`IT0-{tenantPrefix3}-{random4}` 例:`IT0-ACM-X9K2`

### 领域实体(5个,均在 public schema)
- `referral_codes`:每个租户唯一推荐码,记录点击量
- `referral_relationships`:推荐关系,状态流转 PENDING→ACTIVE→REWARDED→EXPIRED
- `referral_rewards`:积分奖励记录,支持 PENDING/APPLIED/EXPIRED
- `referral_stats`:每租户聚合统计(直推数、积分总量等)
- `referral_processed_events`:Redis Stream 幂等性去重表

### 奖励规则
- Pro 套餐首次付款:推荐人 $15(1500分)/ 被推荐人 $5(500分)
- Enterprise 套餐首次付款:推荐人 $50(5000分)/ 被推荐人 $20(2000分)
- 续订奖励:付款金额 10%,最多持续 12 个月
- 奖励触发:监听 Redis Stream `events:payment.received`,消费者组 `referral-service`

### Use Cases(6个)
- `GetMyReferralInfoUseCase`:获取/自动创建推荐码,返回分享链接
- `ValidateReferralCodeUseCase`:验证码格式 + 存在性(公开接口,注册前使用)
- `RegisterWithCodeUseCase`:注册时绑定推荐关系,防止自推荐/重复注册
- `ConsumePaymentReceivedUseCase`:消费支付事件,发放首次/续订奖励,含幂等保护
- `GetReferralListUseCase`:分页查询推荐列表和奖励记录
- `GetPendingCreditsUseCase`:供 billing-service 查询待抵扣积分并标记已使用

### REST Controllers(3个)
- `ReferralController` (/api/v1/referral):用户端,JWT 验证
  - GET /me — 我的推荐码与统计
  - GET /me/referrals — 我的推荐列表(分页)
  - GET /me/rewards — 我的奖励记录(分页)
  - GET /validate?code=xxx — 公开验证推荐码(注册页使用)
- `ReferralInternalController` (/api/v1/referral/internal):服务间调用,X-Internal-Api-Key 验证
  - POST /register — auth-service 注册后回调,绑定推荐关系
  - GET /:tenantId/pending-credits — billing-service 查询待抵扣金额
  - POST /:tenantId/apply-credits — billing-service 账单生成后标记积分已使用
- `ReferralAdminController` (/api/v1/referral/admin):管理员端,JWT + platform_admin 角色
  - GET /relationships — 全量推荐关系(可按状态过滤,分页)
  - GET /rewards — 全量奖励记录(可按状态过滤,分页)
  - GET /stats — 平台汇总统计

## Infrastructure

### database migration (packages/shared/database/migrations/006-create-referral-tables.sql)
创建 5 张表,含必要索引(tenantId、code、status、createdAt)

### docker-compose.yml
新增 referral-service 服务定义(port 13012:3012),healthcheck 基于 HTTP 200,
api-gateway depends_on 中添加 referral-service healthy 条件

### kong.yml (packages/gateway/config/kong.yml)
新增 3 组路由:
- `referral-routes`:/api/v1/referral(JWT 插件,转发用户请求)
- `referral-admin-routes`:/api/v1/referral/admin(JWT 插件,管理员)
- `referral-validate-public`:/api/v1/referral/validate(无 JWT,注册页调用)
注:internal 路由不暴露到 Kong,仅服务间直接调用

## auth-service 集成 (packages/services/auth-service/src/application/services/auth.service.ts)
注册成功后(register + registerWithNewTenant 两个路径)fire-and-forget 调用
referral-service 内部接口 POST /api/v1/referral/internal/register,
传入 tenantId + referralCode(可选),使用 Node.js 内置 http 模块(无新依赖)

## Flutter 移动端 (it0_app/lib/features/referral/)

### 数据层
- `referral_info.dart`:ReferralInfo / ReferralItem / RewardItem 模型,含格式化 getter
- `referral_repository.dart`:Dio HTTP 请求 + Riverpod referralRepositoryProvider

### 状态管理(Riverpod FutureProvider)
- referralInfoProvider — 推荐码信息
- referralListProvider — 直推列表首页
- pendingRewardsProvider — 待抵扣奖励
- allRewardsProvider — 完整奖励历史

### UI(referral_screen.dart,630行)
- _ReferralCodeCard:推荐码展示 + 一键复制 + 系统分享(Share.share)
- _StatsRow:3格统计卡(直推数 / 已激活 / 待抵扣积分)
- _RewardRulesCard:奖励规则说明卡片
- _ReferralPreviewList + _RewardPreviewList:首页预览 + "查看全部"导航
- _ReferralListPage + _RewardListPage:完整分页列表子页面

### 入口集成
- profile_page.dart:Billing 分组新增"邀请有礼"设置行(Gift 图标)
- app_router.dart:ShellRoute 内新增 /referral 路由 → ReferralScreen

## Web Admin (it0-web-admin/)

### 数据层
- `src/domain/entities/referral.ts`:TypeScript 接口定义(ReferralRelationship / ReferralReward / ReferralAdminStats / PaginatedResult<T>)
- `src/infrastructure/repositories/api-referral.repository.ts`:React Query 数据获取函数(getAdminReferralStats / listAdminRelationships / listAdminRewards)

### 管理页面 (src/app/(admin)/referral/page.tsx)
3 Tab 布局(概览 / 推荐关系 / 积分奖励):
- StatsOverview:3张统计卡(总推荐数 / 已激活 / 待领积分记录)
- RelationshipsTable:状态筛选下拉 + 分页表格(推荐人、被推荐人租户ID、推荐码、层级、状态、时间)
- RewardsTable:状态筛选下拉 + 分页表格(受益租户、金额、触发类型、状态、来源账单、时间)
- StatusBadge:彩色状态标签组件(PENDING/ACTIVE/REWARDED/EXPIRED/APPLIED)

### 导航集成
- sidebar.tsx:platformAdminItems 新增"推荐管理"(Gift 图标,/referral 路由)
- i18n/locales/zh/sidebar.json:新增 "referral": "推荐管理"
- i18n/locales/en/sidebar.json:新增 "referral": "Referrals"

## 部署说明
1. 服务器执行数据库迁移:
   psql -U it0 -d it0 -f packages/shared/database/migrations/006-create-referral-tables.sql
2. 重建并启动新服务:
   docker compose build referral-service api-gateway && docker compose up -d
3. 确认 .env 中设置 INTERNAL_API_KEY(服务间认证密钥)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 21:15:27 -08:00
hailin aa2b8e3138 fix(presence-service): use linux-musl-openssl-3.0.x Prisma binary target for Alpine
Alpine Linux (node:18-alpine) ships OpenSSL 3 only; the default linux-musl engine
binary requires libssl.so.1.1 which is absent on Alpine 3.17+. Specifying
binaryTargets = ["native", "linux-musl-openssl-3.0.x"] forces Prisma to generate
the OpenSSL-3-compatible query engine, resolving the startup crash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 18:19:13 -08:00
hailin ddf221cece fix(presence-service): generate prisma client in docker production stage
- Move prisma from devDependencies to dependencies so it is available
  after pnpm install --prod in the Dockerfile production stage
- Replace failed COPY of /app/node_modules/.prisma (pnpm virtual store
  path differs) with: COPY schema.prisma + RUN prisma generate in stage-1
- Only runs if schema.prisma exists (safe for all other services)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 18:10:43 -08:00
hailin dd3abb3e67 fix(presence-service): set rootDir=../.. to match monorepo dist path structure
entrypoint.sh expects dist/services/presence-service/src/main.js
but without rootDir, tsc infers rootDir=src/ giving dist/main.js.
Setting rootDir=../.. (packages/ level) produces the correct nested path
dist/services/presence-service/src/main.js consistent with other services.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 18:04:56 -08:00
hailin 418a9b4196 fix(presence-service): run prisma generate before nest build
pnpm ignores @prisma/client postinstall scripts in Docker build context,
so generated types are missing. Run prisma generate explicitly as part
of the build script so @prisma/client exports are available to tsc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 17:51:34 -08:00
hailin 8d2fd3335a feat(telemetry): add presence-service + Flutter telemetry module
## Backend — packages/services/presence-service (新微服务)

完整的 DDD + Clean Architecture 实现,移植自 RWADurian presence-service,
针对 IT0 架构做了以下适配:

### 核心功能
- 心跳接口: POST /api/v1/presence/heartbeat(JWT 验证,60s 间隔)
  → Redis Sorted Set `presence:online_users` 记录在线时间戳
  → 默认 5 分钟窗口判断在线(PRESENCE_WINDOW_SECONDS=300)
- 事件上报: POST /api/v1/analytics/events(批量,最多 50 条)
  → 写入 presence_event_log 表 + 更新 presence_device_profile
  → Redis HyperLogLog `presence:dau:{date}` 实时 DAU 估算
- 查询接口(需 AdminGuard):
  - GET /api/v1/analytics/online-count  — 实时在线人数
  - GET /api/v1/analytics/online-history — 历史在线快照
  - GET /api/v1/analytics/dau — DAU 统计

### IT0 适配要点
- JWT payload: `sub` = UUID userId(非 RWADurian 的 userSerialNum)
  → JwtAuthGuard: request.user = { userId: payload.sub, roles, tenantId }
- AdminGuard: 改为检查 `roles.includes('admin')`(非 type==='admin')
- 移除 Kafka EventPublisherService(IT0 无 Kafka)
- 移除 Prometheus MetricsService(IT0 无 Prometheus)
- 表前缀改为 `presence_`(避免与其他服务冲突)
- userId 字段 VarChar(36)(UUID 格式,非原来的 VarChar(20))
- Redis DB=10 隔离(独立 key 空间)

### 数据库表(public schema)
- presence_event_log       — 事件流水(append-only)
- presence_device_profile  — 设备快照(upsert,每台设备一行)
- presence_daily_active_users — DAU 日统计
- presence_online_snapshots   — 在线人数每分钟快照

### 定时任务(@nestjs/schedule)
- 每分钟: 采集在线人数快照 → presence_online_snapshots
- 每天 01:05 (UTC+8): 计算前一天 DAU → presence_daily_active_users

---

## Flutter — it0_app/lib/core/telemetry (新模块)

### 文件结构
- telemetry_service.dart      — 单例入口,统筹所有组件
- models/telemetry_event.dart — 事件模型,toServerJson() 将设备字段提升为顶层列
- models/device_context.dart  — 设备上下文(Android/iOS 信息)
- models/telemetry_config.dart — 远程配置(采样率/开关,支持远端同步)
- collectors/device_info_collector.dart — 采集 device_info_plus 设备信息
- storage/telemetry_storage.dart  — SharedPreferences 队列(最多 500 条)
- uploader/telemetry_uploader.dart — 批量上传到 /api/v1/analytics/events
- session/session_manager.dart    — WidgetsBindingObserver 监听前后台切换
- session/session_events.dart     — 会话事件常量
- presence/heartbeat_service.dart — 定时心跳 POST /api/v1/presence/heartbeat
- presence/presence_config.dart   — 心跳配置(间隔/requiresAuth)
- telemetry.dart                  — barrel 导出

### 集成点
- app_router.dart _tryRestore(): TelemetryService().initialize() 在 auth 之前
- auth_provider.dart login/loginWithOtp: setUserId + setAccessToken + resumeAfterLogin
- auth_provider.dart tryRestoreSession: 恢复 userId + accessToken
- auth_provider.dart logout: pauseForLogout + clearUserId + clearAccessToken

### 新增依赖
- device_info_plus: ^10.1.0
- equatable: ^2.0.5

---

## 基础设施

### Dockerfile.service
- 在 builder 和 production 阶段均添加 presence-service/package.json 的 COPY

### docker-compose.yml
- 新增 presence-service 容器(端口 3011/13011)
  - DATABASE_URL: postgresql://... (Prisma 所需连接串格式)
  - REDIS_HOST/PORT/DB: 10(presence 独立 Redis DB)
  - APP_PORT=3011, JWT_SECRET, PRESENCE_WINDOW_SECONDS=300
- api-gateway depends_on 新增 presence-service

### kong.yml (dbless 声明式)
- 新增 presence-service 服务(http://presence-service:3011)
  - presence-routes: /api/v1/presence
  - analytics-routes: /api/v1/analytics
- 对整个 presence-service 启用 JWT 插件(Kong 层鉴权)

### DB 迁移
- packages/shared/database/src/migrations/010-create-presence-tables.sql
  — 4 张 presence_ 前缀表 + 完整索引(IF NOT EXISTS 幂等)
- run-migrations.ts: runSharedSchema() 中新增执行 010-create-presence-tables.sql

---

## 部署步骤(服务器)

1. git pull
2. 执行 presence 表迁移(首次):
   docker exec it0-postgres psql -U it0 -d it0 \
     -f /path/to/010-create-presence-tables.sql
   或通过 migration runner:
   cd /home/ceshi/it0 && node packages/shared/database/dist/run-migrations.js
3. 重建并启动 presence-service:
   docker compose build presence-service api-gateway
   docker compose up -d presence-service api-gateway

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 17:44:01 -08:00
hailin d56486a4aa fix(agent-service): use hailin168/openclaw-bridge Docker Hub image
The it0hub org doesn't exist on Docker Hub. Switch to hailin168/openclaw-bridge:latest
which was built and pushed from openclaw source + IT0 bridge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 12:21:27 -08:00
hailin ad46e45181 fix(agent-service): remove duplicate remove() override conflicting with TenantAwareRepository base
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:29:40 -08:00
hailin 2086eb8109 feat(openclaw): Phase 2 — heartbeat endpoint + iAgent OpenClaw deployment awareness
- agent-instance.controller: POST :id/heartbeat — bridge calls this every 60s;
  auto-transitions status from deploying→running when gateway is confirmed connected
- system-prompt-builder: teach iAgent about OpenClaw deployment capability:
  create/list/stop/remove instance API endpoints, when to trigger deployment,
  and what to tell users about channel connectivity (Telegram/WhatsApp etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:17:35 -08:00
hailin 7d5840c245 feat(openclaw): Phase 1 — server pool + agent instance deployment infrastructure
## inventory-service
- New: pool_servers table (public schema, platform-admin managed)
- New: PoolServer entity, PoolServerRepository, PoolServerController
- CRUD endpoints at /api/v1/inventory/pool-servers
- Internal /deploy-creds endpoint (x-internal-api-key protected) for SSH key retrieval
- increment/decrement endpoints for capacity tracking

## agent-service
- New: agent_instances table (tenant schema)
- New: AgentInstance entity, AgentInstanceRepository, AgentInstanceController
- New: AgentInstanceDeployService — SSH-based docker deployment
  - Queries pool server availability from inventory-service
  - AES-256 encrypts OpenClaw gateway token at rest
  - Allocates host ports in range 20000-29999
  - Fires docker run for it0hub/openclaw-bridge:latest
  - Async deploy with error capture
- Added ssh2 dependency for SSH execution
- Added INVENTORY_SERVICE_URL, INTERNAL_API_KEY, VAULT_MASTER_KEY to docker-compose

## openclaw-bridge (new package)
- packages/openclaw-bridge/ — custom Docker image
- Two processes via supervisord: OpenClaw gateway + IT0 Bridge (Node.js)
- IT0 Bridge exposes REST API on port 3000:
  GET /health, GET /status, POST /task, GET /sessions, GET /metrics
- Connects to OpenClaw gateway at ws://127.0.0.1:18789 via WebSocket RPC
- Sends heartbeat to IT0 agent-service every 60s
- Dockerfile: multi-stage build (openclaw source + bridge TS compilation)

## Web Admin
- New: /server-pool page — list/add/edit/delete pool servers with capacity bars
- New: /openclaw-instances page — cross-tenant instance monitoring with status filter
- Sidebar: added 服务器池 (Database icon) + OpenClaw 实例 (Boxes icon) to platform_admin nav

## Flutter App
- my_agents_page: rewritten to show real AgentInstance data from /api/v1/agent/instances
- Added AgentInstance model with status-driven UI (running/deploying/stopped/error)
- Status badges with color coding + spinner for deploying state
- Summary chips showing running vs stopped counts
- api_endpoints.dart: added agentInstances endpoint

## Design docs
- OPENCLAW_INTEGRATION_PLAN.md: complete architecture document with all confirmed decisions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:11:21 -08:00
hailin 149939f5f0 fix: store tenant slug (not UUID) in current_tenant; remove plan trial periods
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:01:21 -08:00
hailin 1e4aab378d feat(org): add tenant user management + invite system + fix tenant display
- Backend: GET /api/v1/auth/my-org returns tenant info + member list
- Backend: GET /api/v1/auth/my-org/invites lists pending invites
- Backend: POST /api/v1/auth/my-org/invite creates invite link
- Frontend: /my-org page with member list and invite creation
- Frontend: add '用户管理' to tenant sidebar
- Frontend: add '套餐' (plans) to tenant billing section
- Frontend: admin layout initializes tenant store (fixes '租户:未选择')

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 08:50:39 -08:00
hailin 7dc5881496 fix(auth): use UUID fallback slug when company name produces empty slug (e.g. Chinese-only names) 2026-03-07 08:07:49 -08:00
hailin 71ea80972d feat(auth): add SMS OTP verification for phone registration and login
- auth-service: add SmsService (Aliyun SMS) + RedisProvider for OTP storage
- POST /api/v1/auth/sms/send — send OTP (rate limited 1/min per phone)
- POST /api/v1/auth/sms/verify — verify OTP only
- POST /api/v1/auth/login/otp — passwordless login with phone + OTP
- register endpoint now requires smsCode when registering with phone
- Web Admin register page: add OTP input + 60s countdown button for phone mode
- Flutter login page: add 验证码登录 tab with phone + OTP flow
- SMS enabled via ALIYUN_ACCESS_KEY_ID/SECRET + SMS_ENABLED=true env vars
- Falls back to mock mode (logs code) when env vars not set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 06:43:27 -08:00
hailin 2773b6265c fix(billing): return default free-plan stub when no subscription exists
Previously GET /api/v1/billing/subscription threw 404 for tenants with no
subscription, causing React Query error state on the Plans and Overview pages.
Now returns a graceful default response so the UI renders without errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 06:09:32 -08:00
hailin b5d1f11104 fix(auth): allow platform_admin to access all web-admin endpoints
Systematically add platform_admin and platform_super_admin to all
controllers that were restricted to 'admin' only:
- audit-service: queryLogs, exportLogs
- inventory-service: decryptCredential
- auth-service: RoleController, PermissionController

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 05:54:05 -08:00
hailin 5b5b3ea70d fix(auth): allow platform_admin to access settings endpoints
SettingsController was restricted to 'admin' only, blocking platform_admin
from the dashboard settings page (403 on general/api-keys/theme/account).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 05:51:14 -08:00
hailin 4aabda440f fix(auth): allow platform_admin to manage tenant members and invites
Member/invite endpoints were restricted to 'admin' role only, blocking
platform_admin from accessing them on the tenant detail page (403).
Added platform_admin and platform_super_admin to all six endpoints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 05:45:59 -08:00
hailin e48615e713 fix(auth): fix listMembers response shape and updateMember role sync
- listMembers was returning { data, total } but frontend expects TenantMember[]
  directly, causing members.map is not a function crash on the detail page.
- updateMember now also syncs role changes to public.users so the new role
  takes effect the next time the user logs in (JWT is generated from public.users).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 05:39:52 -08:00
hailin e31baa1f40 fix(auth): fix invite flow UUID/slug mismatch and removeMember cleanup
- TenantController invite endpoints (list/create/revoke) were passing the
  tenant UUID from the URL param directly to AuthService methods that
  expect a slug, causing 404 on every invite operation. Now resolves
  tenant via findTenantOrFail() first and passes slug.
- removeMember now also deletes from public.users so removed members
  can no longer log in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 05:34:47 -08:00
hailin 915bd400c1 fix(auth): insert invited users into public.users on acceptInvite
Previously, acceptInvite only wrote to the tenant schema, causing invited
users to be invisible to the login() flow which queries public.users for
cross-tenant email/phone lookup. Now inserts into both public.users and
the tenant schema within the same transaction, matching registerWithNewTenant behavior.

Also tightens duplicate check to cross-tenant uniqueness (public.users)
instead of per-tenant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 05:31:50 -08:00
hailin 6459e5b500 fix(tenants): allow platform_admin to delete tenants; fix invite/user cleanup
- DELETE /api/v1/admin/tenants/:id now accepts platform_admin role
- Fix cascade cleanup to use tenant slug (not UUID) for users/invites/api_keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 04:44:42 -08:00
hailin 129c5cbeab fix(auth): use slug lookup for tenant in validateInvite and acceptInvite
invite.tenantId stores the slug (not UUID), so findOneBy must use { slug }

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 04:26:21 -08:00
hailin 100ca43460 fix(auth): use slug for tenant lookup in createInvite; fix getMemberCount search_path
- createInvite: findOneBy({ slug }) instead of { id } since JWT tenantId is slug
- getMemberCount: use SET LOCAL + transaction to prevent pool search_path leak

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 04:17:14 -08:00
hailin 76389a337e fix(auth): properly map raw SQL snake_case rows to User entity camelCase fields 2026-03-07 03:54:34 -08:00
hailin 00357d855d fix(auth): use explicit public. schema for all login/register queries to prevent search_path contamination 2026-03-07 03:50:05 -08:00
hailin c2ba432341 fix(auth): dual-write tenant admin to public.users for login + tenant schema for management 2026-03-07 03:42:27 -08:00
hailin 7a6752bf74 fix(auth): use SET LOCAL search_path to prevent connection pool contamination; fix api-test routing 2026-03-07 03:35:49 -08:00
hailin 09d9200235 fix(auth): make tenant.adminEmail nullable for phone-only registrations; fix api-test status parsing 2026-03-07 03:27:23 -08:00
hailin b8128b7a07 fix(auth): make JwtPayload email optional, add phone to JWT payload 2026-03-07 03:21:32 -08:00