Commit Graph

236 Commits

Author SHA1 Message Date
hailin 54ed8290b1 fix(deploy): patch models.generated.js for LLM gateway + fix AGENTS.md template symlink
After container starts, sed-replace api.anthropic.com with iConsulting LLM gateway URL
in all models.generated.js files (ANTHROPIC_BASE_URL env alone is not enough since
baseUrl is hardcoded). Also create missing AGENTS.md template symlink so OpenClaw
does not 500 on workspace init.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 15:25:52 -07:00
hailin 50401660ef fix(dingtalk): exclude removed instances from routing + clear binding on remove
Two bugs fixed:
1. findByDingTalkUserId now filters status != 'removed' so a re-bound new instance
   is not shadowed by an old removed one with the same DingTalk user ID.
2. When an agent is deleted (removed), its dingtalkUserId is cleared so the
   DingTalk ID is freed for reuse by the next binding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 14:49:00 -07:00
hailin a8b5571aea fix(deploy): pre-create workspace dir with correct ownership for OpenClaw containers
OpenClaw runs as node user (uid 1000) but the host directory was created as root,
causing EACCES when the container tried to create /home/node/.openclaw/workspace.

Now mkdir workspace/ and chown -R 1000:1000 before starting the container.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 14:32:50 -07:00
hailin 495407d25b fix(agent): pass sessionId to system prompt for text chat OAuth trigger
Text sessions were not passing sessionId to SystemPromptBuilder, causing
Claude to use the `initiate_dingtalk_binding` custom tool (claude_api only).
When the engine is claude_agent_sdk, this tool does not exist → 404.

Fix: pass session.id as sessionId to systemPromptBuilder.build() in
agent.controller.ts. Claude will now use the wget oauth-trigger endpoint
for ALL session types (text and voice), which works with every engine.

Also: store userId (staffId) as the DingTalk binding ID when resolvable,
falling back to openId. Bot messages deliver senderStaffId which matches
userId, not openId — this prevents the "binding not found" routing failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 14:20:58 -07:00
hailin 3ca3982c28 fix(dingtalk): correct greeting flow — use userId (staffId) for batchSend
Problem: sendGreeting() was passing openId as `userIds` to batchSend, but
the API requires the enterprise staffId (userId). This caused HTTP 400
"staffId.notExisted" for every OAuth-bound greeting.

Fix:
1. completeOAuthBinding now resolves unionId → userId via
   oapi.dingtalk.com/topapi/user/getbyunionid with corp app token.
   Non-fatal: if the user has no enterprise context, greeting is skipped
   with a clear log explaining why (no Contact.User.Read permission or
   user is not an enterprise member).

2. sendGreeting accepts userId (staffId) and openId separately; uses
   the correct staffId for batchSend. If userId is undefined, emits a
   WARN and skips (user gets greeting on first message instead).

3. routeToAgent now tries senderStaffId as fallback if senderId lookup
   misses — handles edge cases where DingTalk delivers staffId in senderId.

4. Added detailed logging: all three IDs (openId, unionId, userId) are
   logged at binding time so future issues are immediately diagnosable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 13:53:08 -07:00
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 aca4dd0177 feat(openclaw-bridge): patch models.generated.js to route Anthropic calls via LLM gateway
Replace hardcoded https://api.anthropic.com with http://154.84.135.121:3008 in
@mariozechner/pi-ai's models.generated.js at image build time. Uses find+xargs
to be version-agnostic. The gateway key sk-gw-oc-881239445c76b8d349a13be9fc4507a3
is configured in auth-profiles.json on the mounted volume.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 13:15:29 -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 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 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 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