Commit Graph

439 Commits

Author SHA1 Message Date
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 20e96f31cd fix(openclaw-bridge): use regex replace for base64url (ES2020 compat)
replaceAll() requires ES2021+, tsconfig targets ES2020.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 07:40:56 -07:00
hailin a7c5b264fa fix(openclaw-bridge): implement correct v3 device auth protocol
- device.id = SHA256(rawPubKey, hex) — not a random UUID
- device.publicKey = raw 32-byte key encoded as base64url (not SPKI DER)
- sign the full v3 payload string (not just raw nonce bytes):
  "v3|{deviceId}|{clientId}|{mode}|{role}|{scopes}|{ts}|{token}|{nonce}|{platform}|"
- device.signature encoded as base64url

Matches buildDeviceAuthPayloadV3/verifyDeviceSignature from openclaw dist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 07:40:15 -07:00
hailin bb4b73f847 fix(openclaw-bridge): include nonce in device handshake params
The gateway expects device.nonce (the challenge nonce) to be echoed
back in the connect request. Without it the connection is rejected with
'device: must have required property nonce'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 07:32:29 -07:00
hailin 00a944c9a9 fix(openclaw-bridge): use 'gateway-client'/'backend' as WS client id/mode
openclaw gateway validates client.id against a fixed set of known IDs.
Using a random UUID caused the connection to be rejected immediately with
'client/id must be equal to constant'. Use 'gateway-client' + 'backend'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 07:30:06 -07:00
hailin d13b9b7df9 fix(openclaw-bridge): add --allow-unconfigured to gateway command
openclaw gateway requires either a config file or --allow-unconfigured
flag to start without prior setup. Without it the process exits with
'Missing config' and enters a restart loop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 07:25:39 -07:00
hailin 72825d5526 fix(openclaw-bridge): add 'gateway --port 18789' subcommand to openclaw supervisord entry
Without a subcommand the CLI prints help and exits (status 1), causing
supervisord to restart it in a loop and the OC gateway to never start.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 07:07:39 -07:00
hailin a47e894d4b fix(bridge): upgrade Node from 20 to 22 — openclaw requires >=22.12.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 06:23:46 -07:00
hailin 805cd849fb fix(bridge): use correct openclaw entry point dist/index.js
openclaw package.json main=dist/index.js, not dist/openclaw.mjs.
The bin wrapper file is not copied into the Docker image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 06:21:56 -07:00
hailin 09e50154d3 fix(deploy): pass AGENT_SERVICE_PUBLIC_URL into agent-service container
Required so agent-service can inject IT0_AGENT_SERVICE_URL into openclaw
containers when deploying agent instances to pool servers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 06:17:15 -07:00
hailin c3b31ebf47 fix(bridge): remove environment= from supervisord programs, inherit from container
supervisord %(ENV_VAR)s expansion fails hard if the variable is absent.
For optional vars like DINGTALK_CLIENT_ID this crashes the entire supervisor.
Fix: remove all per-program environment= directives. All vars are injected
via docker run -e and supervisord child processes inherit them automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 06:12:52 -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 b0801e0983 feat(bridge): DingTalk channel plugin + OpenClaw Protocol v3 rewrite
Core changes:
- src/channels/dingtalk.ts: DingTalk Stream SDK channel (no public IP needed)
  - TokenManager: auto-refresh with refreshPromise mutex (prevents race condition)
  - UserQueue: per-user serial queue, max depth 5
  - MsgDedup: O(1) Map<string,timestamp> with 10min TTL + 10k cap
  - RateLimiter: sliding window 10 msg/min per user
  - ResilientOcClient: 10s heartbeat poll + atomic reconnect guard
  - DingTalkStream: exponential backoff reconnect (2s→60s), immediate ACK
  - replyToUser: sessionWebhook expiry check + 4800-char chunking

- src/openclaw-client.ts: rewritten for correct Protocol v3 wire format
  - Request frame: { type:"req", id, method, params }
  - Challenge-response Ed25519 handshake (connect.challenge → connect req)
  - Correct rpc() with configurable timeoutMs

- src/index.ts: fixed RPC method names
  - agent.run → chat.send with { sessionKey, message, timeoutSeconds }
  - metrics.get → gateway.status

- Dockerfile: adds start-dingtalk.sh COPY + chmod
- supervisord.conf: dingtalk-channel program block (autorestart=unexpected)
- start-dingtalk.sh: exits 0 if DINGTALK_CLIENT_ID unset (no restart loop)
- CHANNEL_DEV_GUIDE.md: full dev guide for future channel integrations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 05:10:01 -07:00
hailin 688219ab74 fix(bridge): remove user= from supervisord.conf to fix non-root startup
Container runs as 'node' user (USER node in Dockerfile). Setting user=root
in [supervisord] causes "Can't drop privilege as nonroot user" error.
Remove all user= directives — user is managed at the Docker/container level.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 05:07:25 -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 49e48d7b3e feat(flutter): add 4 general-purpose official agents to home page
New agents (shown first in horizontal scroll):
- 日常办公助手 / Office Assistant
- 在线客服智能体 / Customer Service Bot
- 市场营销助手 / Marketing Assistant
- 外语学习助手 / Language Tutor

All 4 agents fully localized in en/zh/zh_TW.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 01:54:29 -08:00
hailin b666bed740 fix: show OpenClaw in non-Chinese, 小龙虾 only in zh/zh_TW
Flutter app:
- app_zh.arb: OpenClaw → 小龙虾
- app_zh_TW.arb: OpenClaw → 小龍蝦
- app_en.arb: revert 小龙虾 back to OpenClaw

Web admin:
- Add serverPool/openclawInstances keys to en/zh sidebar.json
- en: "OpenClaw Instances", zh: "小龙虾实例"
- sidebar.tsx: use t() instead of hardcoded strings
- openclaw-instances + server-pool pages: use t('openclawInstances')

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 01:48:25 -08:00
hailin 0846452e8d fix(flutter): replace all 'My Agent' brand references with 'iAgent' in English ARB
Also replace 'OpenClaw' with '小龙虾' in English user-facing strings.
'My Agents' plural (section names) intentionally kept as-is.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 01:35:50 -08:00
hailin 924b826542 fix(flutter): redirect to /home after login (was /dashboard which no longer exists)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 01:32:54 -08:00
hailin 164d42e6b8 fix(flutter): localize all hardcoded Chinese strings in home_page + restore iAgent brand name
- home_page.dart: use l10n for greeting, default username, agent status, message count
- app_en.arb: fix appTitle back to 'iAgent' (was incorrectly changed to 'My Agent')
- Add defaultUserName and agentInConversation keys to en/zh/zh_TW ARBs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 01:23:43 -08:00
hailin 3f47a7b149 feat(web-admin): rename OpenClaw to 小龙虾 in all UI labels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 01:17:00 -08:00
hailin 99c883fad9 fix(flutter): replace ListTile with custom InkWell+Row in _SettingsRow to fix vertical text
ListTile in Material 3 constrains trailing to ~72px, causing long title text
like "Refer & Earn" to be squeezed vertically letter-by-letter. Custom layout
uses Expanded on the title to take all available space, with trailing/chevron
floated to the right — matching how major apps handle settings rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 01:12:49 -08:00
hailin ece778e593 fix(web-admin): use apiClient for openclaw-instances to forward JWT
Bare fetch() was not sending Authorization header, causing 401.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 01:06:28 -08:00
hailin c33c45d474 chore(flutter): auto-format generated l10n dart files
Flutter gen-l10n added zh translation comments and reflowed long lines.
No functional changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 00:58:40 -08:00
hailin 60f09feb50 feat(flutter): apply Inter font + add language picker in profile
- Add google_fonts ^6.2.1; apply Inter via GoogleFonts.interTextTheme
  for both dark and light themes (English/Latin chars use Inter,
  CJK chars fall back to system font automatically)
- Add _showLanguagePicker bottom sheet in profile page with 4 options:
  Auto (follow system), 简体中文, 繁體中文, English
- Wire language row onTap to open the picker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 00:57:45 -08:00
hailin b2594317d7 feat(flutter): localize referral page + fix language auto-detection
- Add 27 new l10n keys (ARB + generated dart) for the referral screen,
  covering both Enterprise tab and personal circle tab strings
- Replace all hardcoded Chinese strings in referral_screen.dart with
  l10n calls (tab labels, section headers, status labels, rules, etc.)
- Fix language auto-detection: default to '' instead of 'en', and
  return null from localeProvider to follow device locale
- Fix 'Refer & Earn' vertical text: wrap trailing with Flexible in
  _SettingsRow on profile page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 00:51:25 -08: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 6be84617d2 feat(flutter): i18n体系(zh/zh_TW/en) + 智能体解聘功能
- 建立完整 flutter_localizations i18n 体系:zh/zh_TW/en 三语言
- l10n.yaml + ARB 文件 (app_zh.arb 约120键作模板,zh_TW/en 对应覆盖)
- localeProvider 连接 SharedPreferences language 设置,实时切换语言
- 设置页加入语言选择器(简体中文/繁体中文/English)
- 我的智能体页实现解聘(解聘确认弹窗 + DELETE API)与重命名功能
- 全部页面 (~18个) UI 字符串替换为 AppLocalizations.of(context).xxx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 00:05:55 -08:00
hailin 3074ea54a9 fix(dockerfile): add notification-service/package.json to builder and production stages
The package.json was missing from both the builder stage (lines ~20-31)
and the production stage (lines ~60-70), causing pnpm to skip installing
@nestjs/core and all other dependencies for notification-service.
Container started but immediately crashed with 'Cannot find module @nestjs/core'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 23:12:13 -08:00
hailin 38594d6fd4 feat(flutter): rename iAgent→我智能体,创建/删除→招募,拉近人机关系距离
- App title、登录页、导航Tab、通话页等全局将 iAgent 改为 我智能体
- 底部导航 Tab "我的创建" → "我的智能体"
- 智能体语境下 "创建" → "招募":招募你的专属智能体、帮我招募一个...
- tasks_page 空状态文案 "创建" → "新增"(非智能体语境保持语义准确)
- 终端欢迎语、通知渠道描述同步更新

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 23:10:07 -08:00
hailin 3453731bc8 fix(web-admin): add type cast to NotificationModal initial prop
TypeScript strict check rejects NotificationItem|{} union as
Partial<CreateNotificationPayload>&{id?}. Add explicit cast to satisfy
the type checker without changing runtime behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 23:09:35 -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 52c443d937 fix(flutter): remove incorrect .dio accessor from dioClientProvider calls
dioClientProvider returns Dio directly (not a wrapper class).
Removed spurious .dio property access from 3 files:
- in_site_notification_repository.dart
- notification_preferences_page.dart
- referral_repository.dart

Also fix _SettingsRow usage in profile_page: replaced incorrect
`label`/`subtitle` params with the correct `title`/`iconBg` params.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 22:41:07 -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 3020ecc465 fix(dockerfile): add referral-service package.json COPY in production stage
Without this, pnpm install --prod in the production stage doesn't know
about referral-service dependencies (@nestjs/core etc.) and they are missing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 21:37:23 -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 18049c47a3 fix(dockerfile): add referral-service package.json COPY step for pnpm install cache
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 21:26:01 -08:00
hailin 715327608f chore: update pnpm-lock.yaml to include referral-service dependencies
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 21:20: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 432cdc46a8 fix(dockerfile): use pnpm exec prisma generate in production stage
pnpm does not hoist workspace package binaries to /app/node_modules/.bin;
each package's .bin/ is only available within that package's node_modules.
Use 'pnpm exec prisma generate' from the service directory so pnpm can
resolve the prisma binary from the local node_modules/.bin symlink.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 20:16:19 -08:00
hailin 7edbea6ff0 fix(dockerfile): correct prisma generate path + add openssl for Alpine detection
Two fixes for Prisma on Alpine Linux:
1. Use /app/node_modules/.bin/prisma (workspace root) instead of
   node_modules/.bin/prisma — pnpm does not hoist binaries into each
   service's local node_modules/.bin, so the previous command silently
   skipped via || true, leaving only the default linux-musl (libssl 1.1) binary.
2. Add openssl to apk packages so Prisma can run 'openssl version' at
   runtime to detect OpenSSL 3.x and load the linux-musl-openssl-3.0.x
   engine binary instead of defaulting to the missing libssl.so.1.1 variant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 20:10:07 -08:00