Commit Graph

326 Commits

Author SHA1 Message Date
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 29a85dfe92 fix(home): import accountProfileProvider from settings_providers instead of auth_provider
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:47:19 -08:00
hailin faecd18cbd docs(app): add detailed redesign notes for navigation refactor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:43:29 -08:00
hailin d5930ff4c8 feat(app): redesign navigation — floating robot FAB + 4-tab layout
- Add animated robot avatar widget (CustomPainter, 5 states: idle/thinking/executing/speaking/alert)
- Add FloatingRobotFab that mirrors chatProvider AgentStatus as robot animation state
- Replace 5-tab nav (dashboard/chat/tasks/alerts/settings) with 4-tab (home/my-agents/billing/profile)
- Chat is now pushed full-screen from the robot FAB with slide-up transition
- HomePage: active agent status card + official agent horizontal scroll + quick tips
- MyAgentsPage: empty state with 3-step guide + template grid; shows list when agents exist
- ProfilePage: merged settings + prominent billing entry (replaces old SettingsPage as tab)
- ChatPage AppBar: robot avatar replaces plain text title, reflects real-time agent state
- Add agentConfigs endpoint to ApiEndpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:42:17 -08:00
hailin 23daa8e122 fix(metrics): guard toFixed() calls against undefined/null values
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:34:05 -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 bd8d339424 fix(web-admin): fix remaining data ?? [] patterns missed by batch fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 08:40:48 -08:00
hailin 513e575f1f fix(web-admin): guard all data?? [] against non-array API responses
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 08:37:11 -08:00
hailin 7e20e68e13 fix(hooks): guard against non-array API response to prevent .map crash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 08:35:24 -08:00
hailin b9c83c1db7 fix(login): replace phone OTP login with phone+password login
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 08:24:42 -08:00
hailin 06c2d02c21 feat(web-admin): add phone+OTP login mode to login page 2026-03-07 08:18:30 -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 550813c772 fix(web-admin): suppress hydration warning from i18n language detection 2026-03-07 07:56:56 -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 6e50f4cc50 feat(web-admin): show copyable invite link after sending invite
After a platform admin sends an invite, the generated invite URL is
displayed inline with a one-click copy button so it can be shared via
any channel (email, WeChat, etc.). Link auto-dismisses when the invite
form is reopened.

Also adds i18n keys for invite link UI in en/zh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 05:37:30 -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 2813c6a1bf feat(tenants): make tenant name a link to detail page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 05:04:19 -08:00
hailin 0bc81bbe40 feat(tenants): add delete button to tenant list page
Each tenant row now has a Delete button with confirmation dialog.
Previously delete was only accessible from the detail page which
had no navigation link from the list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 04:54:51 -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 074e031685 fix(api-test): use correct tokens and accept 404 for billing subscription
- Users list: use ADMIN_TOKEN (platform_admin role required)
- Billing subscription: accept 200 or 404 (new tenants have no subscription)
- Invite flow: use TOKEN (tenant admin 'admin' role) not ADMIN_TOKEN

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 04:15:06 -08:00
hailin a24eb84e13 fix(provisioning): prevent search_path pool contamination via SET LOCAL + RESET
- Change SET search_path to SET LOCAL in tenant schema template (002)
  so it reverts on COMMIT and doesn't contaminate the connection pool
- Add RESET search_path before queryRunner.release() as defensive measure
- Add ALTER TABLE public.tenants admin_email DROP NOT NULL to migration 007
  to sync the direct server change back to source

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 04:04:53 -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 938802da96 fix(scripts): fix bash arithmetic exit code in api-test.sh 2026-03-07 03:24:44 -08:00
hailin b8128b7a07 fix(auth): make JwtPayload email optional, add phone to JWT payload 2026-03-07 03:21:32 -08:00
hailin 96bf5e7390 feat(auth): add phone registration support + enterprise register page redesign
- User entity: email nullable, add phone field (nullable unique)
- AuthService/Controller: login/register accept email OR phone
- UserRepository: findByPhone(), findByIdentifier() (auto-detects email vs phone)
- Migration 007: ALTER public.users + all existing tenant schemas to add phone
- Tenant schema template (002): users table now includes phone column
- Register page: enterprise-focused design, email/phone toggle, app download section
- Auth i18n (zh/en): new keys for phone, enterprise messaging, download CTA

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 03:14:47 -08:00
hailin 67691fc24d fix(users): handle {data, total} response shape from listUsers API
The backend returns { data: User[], total: number } but the frontend
was treating usersData directly as User[], causing filteredUsers.map
to throw 'not a function' when the page loaded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 02:46:04 -08:00
hailin 60cf49432e feat(billing): add CNY overage rate field and auto-detect currency from locale
- Add overage_rate_fen_per_m_token_cny column (migration 006)
- Plan entity and seed updated with CNY overage rates (Pro ¥58, Enterprise ¥36)
- upsertSeedPlans now updates existing plans (not insert-only)
- Plan controller exposes overageRateCnyPerMToken
- Frontend: currency auto-selects from i18n locale (zh→CNY, en→USD)
- Frontend: Intl.NumberFormat for proper currency formatting
- Currency toggle redesigned as pill selector

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 02:18:25 -08:00
hailin af1cae9da8 feat(i18n): add billing namespace, fully internationalize billing pages
billing/page.tsx, billing/plans/page.tsx, billing/invoices/page.tsx
were hardcoded in English. Added zh/billing.json and en/billing.json
covering overview, plans, and invoices sections. Registered billing
namespace in i18n config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 01:46:05 -08:00
hailin c1fb39c3c0 fix(topbar): hide tenant indicator for platform admin users
Platform admins operate across all tenants and don't belong to any
specific tenant — showing 'Tenant: Not selected' was misleading.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 01:36:39 -08:00
hailin 816c5461f9 feat(auth): add platform_super_admin role for two-level platform access control
在 platform_admin 之上新增 platform_super_admin 角色,实现平台管理员的两级权限体系。

## 角色层级

  platform_super_admin > platform_admin > admin > operator > viewer

- platform_super_admin:最高平台权限,含所有 platform_admin 操作 + 破坏性操作(删除租户/用户/版本)
- platform_admin:日常平台运营,可查看/编辑租户、管理 App 版本、配置账单套餐,不可执行删除

## 变更明细

### auth-service — role-type.vo.ts
- 新增 RoleType.PLATFORM_SUPER_ADMIN = 'platform_super_admin'

### auth-service — tenant.controller.ts
- 租户列表/创建/查看/编辑:@Roles('platform_admin', 'platform_super_admin')(两级均可)
- 删除租户 DELETE /:id:@Roles('platform_super_admin')(仅超管)

### auth-service — user.controller.ts
- 类级别:@Roles('platform_admin', 'platform_super_admin')(两级均可访问用户列表/创建/编辑)
- 删除用户 DELETE /:id:@Roles('platform_super_admin')(仅超管)

### version-service — guards/platform-admin.guard.ts
- 更新:接受 platform_admin 或 platform_super_admin 任一角色
- 重构:抽取 decodeJwtRoles() 工具函数,供 PlatformSuperAdminGuard 复用

### version-service — guards/platform-super-admin.guard.ts(新文件)
- 仅接受 platform_super_admin 角色
- 与 PlatformAdminGuard(类级别)叠加使用,实现方法级别的超管限制

### version-service — version.controller.ts
- DELETE /:id:叠加 @UseGuards(PlatformSuperAdminGuard)(仅超管可删除版本文件)

### web-admin — sidebar.tsx
- isPlatformAdmin 检测同时涵盖 platform_admin 和 platform_super_admin
- 两级平台管理员均显示相同侧边栏菜单

## 升级现有账号为 platform_super_admin
  UPDATE public.users SET roles = '{platform_super_admin}' WHERE email = 'xxx@xxx.com';

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 01:17:27 -08:00
hailin 0ab7261129 feat(auth): introduce platform_admin role with proper access separation
新增 platform_admin 角色,将平台超管与租户管理员的权限彻底分离。

## 后端变更

### auth-service — role-type.vo.ts
- 新增 RoleType.PLATFORM_ADMIN = 'platform_admin'
- DEFAULT_ROLE_PERMISSIONS 中为 PLATFORM_ADMIN 添加空权限集(平台层操作,不参与租户内权限体系)

### auth-service — tenant.controller.ts
- 移除类级别 @Roles('admin'),改为方法级别精细控制:
  - 租户 CRUD(列表/创建/GET/:id/PATCH/:id/PUT/:id/DELETE/:id)→ @Roles('platform_admin')
  - 成员管理(listMembers/updateMember/removeMember)→ @Roles('admin')
  - 邀请管理(listInvites/createInvite/revokeInvite)→ @Roles('admin')
  - 租户管理员可继续管理自己团队的成员和邀请,但无法访问跨租户的租户 CRUD

### auth-service — user.controller.ts
- /api/v1/auth/users(跨租户用户列表/CRUD)→ @Roles('platform_admin')
- 原来任意 admin 均可查看所有用户,现仅平台超管可访问

### version-service — guards/platform-admin.guard.ts(新文件)
- 新增 PlatformAdminGuard:从 Authorization: Bearer <JWT> 中 base64 解码 payload,
  检查 roles 包含 'platform_admin'(无需重复验签,Kong 已完成签名校验)
- 不依赖 @nestjs/passport,轻量、无额外依赖

### version-service — version.controller.ts
- 整个 /api/v1/versions 控制器挂载 @UseGuards(PlatformAdminGuard)
- App 版本管理(上传/发布/删除 APK/IPA)仅平台超管可操作

## 前端变更

### it0-web-admin — sidebar.tsx
- 登录时从 localStorage.user.roles 检测是否为 platform_admin
- 平台超管侧边栏:仪表盘 / 租户管理 / 用户(跨租户)/ App版本 / 账单(套餐+概览+账单记录)/ 设置
- 租户用户侧边栏:仪表盘 / Agent配置 / Runbooks / 常驻指令 / 服务器 / 监控 / 终端 / 安全 / 审计 / 通信 / 账单(概览+账单记录,无套餐管理)/ 设置

## 创建第一个平台超管账号
直接更新数据库:
  UPDATE it0_t_default.users SET roles = '{platform_admin}' WHERE email = 'xxx@xxx.com';
或通过已有 platform_admin 账号调用 POST /api/v1/auth/users 并指定 role: 'platform_admin'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 00:57:40 -08:00
hailin ecc64e0ff9 fix(stt): always use Whisper auto language detection, remove app language hint
Whisper detects language from audio content — speaks Chinese gets Chinese,
speaks English gets English. App language setting is irrelevant to STT.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 00:03:58 -08:00
hailin 4c7c05eb37 feat(stt): support auto language detection for mixed Chinese-English input
- Flutter: language='auto' omits the language field → backend receives none
- Backend: no language field → passes undefined to STT service
- STT service: language=undefined → omits language param from Whisper request
- Whisper auto-detects language per utterance when no hint is provided

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 08:13:26 -08:00
hailin 23675fa5a5 feat(chat): use app language setting for voice-to-text STT language
Reads settingsProvider.language (BCP-47 code) and passes it to the
Whisper transcribe call instead of hardcoding 'zh'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 08:11:21 -08:00
hailin 0aac693b5d fix(app): re-check connectivity on foreground resume to clear false-offline banner
When backgrounded, the periodic TCP ping times out causing isOnline=false.
On resume, immediately re-check so the banner clears as soon as the app
is foregrounded rather than waiting up to 30s for the next scheduled check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 08:08:01 -08:00
hailin 947a47869e fix(agent-service): use https.request for Whisper STT to bypass self-signed cert
Node 18 native fetch (undici) ignores https.Agent, causing fetch failed
on the self-signed proxy at 67.223.119.33:8443. Switch to https.request
with rejectUnauthorized: false which works reliably.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 07:51:37 -08:00
hailin 72584182df fix(chat): fix VoiceMicButton gesture conflict with IconButton tooltip
GestureDetector was fighting with IconButton's inner Tooltip gesture
recognizer — onLongPressStart was never called (only vibration from
tooltip). Replaced with Listener (raw pointer events) + manual 500ms
Timer, which bypasses the gesture arena entirely.

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