Root cause of "Bridge call failed" errors: bridge /task endpoint defaults
to 25s agent reply timeout, but LLM calls through the iConsulting gateway
can take 30-60s. Fix: pass timeoutSeconds=55 explicitly in POST body.
Also add batchSend fallback in routeToAgent: if the sessionWebhook has
expired by the time the LLM replies (user sent a message, LLM took >30s,
webhook window closed), the reply is now sent via proactive batchSend
using senderStaffId instead of being silently dropped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the voice agent triggers DingTalk OAuth, the user leaves the app
to authorize in DingTalk/browser, causing the LiveKit participant to
disconnect. The voice-agent then calls DELETE /voice to terminate the
session — but the user intends to return after completing OAuth.
Fix: mark the session as "oauth_pending" in VoiceSessionController when
oauth-trigger fires. If terminateVoiceSession is called while the flag
is active (10-min grace), suppress the terminate and return 200 OK so
the voice-agent exits cleanly. The session stays alive; when the user
returns to the voice screen, voice/start + inject auto-resume it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two binding paths store different DingTalk ID types:
- OAuth binding stores staffId (resolved via unionId→userId at auth time)
- Code binding stores senderId ($:LWCP_v1:$... format from bot message)
DingTalk Stream API senderId != OAuth openId (different encodings), so
primary lookup by senderId always missed OAuth-bound instances, requiring
a fallback every time. Reverse the lookup order: try senderStaffId first
(direct hit for OAuth binding), fall back to senderId (code binding).
Also add MAX_RESPONSE_BYTES cap to httpPostJson — previously uncapped
unlike the DingTalk API helpers which already had the 256KB guard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OpenClaw daemon checks ANTHROPIC_API_KEY env var on startup. We were passing
CLAUDE_API_KEY which openclaw ignores, so it fell back to auth-profiles.json
containing the raw Anthropic key, causing 401 from iConsulting LLM gateway.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OpenClaw reads API key from auth-profiles.json. Was writing raw Anthropic key
sk-ant-api03-... which gateway doesn't recognize. Must use effectiveApiKey
(sk-gw-oc-... gateway key) so authentication with iConsulting LLM gateway succeeds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
- 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>
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>
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>
- 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>
- 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>
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>
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>
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>
- 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>
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>
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>
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>
- 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>
- 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>
- 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>
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>
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>
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>
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>
- 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>
- 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>