Skip to content

feat: DingTalk media channel, OAuth2 SSO, multi-tenant management & Identity architecture adaptation#258

Open
nap-liu wants to merge 84 commits intodataelement:mainfrom
nap-liu:feature/merge-upstream-20260401
Open

feat: DingTalk media channel, OAuth2 SSO, multi-tenant management & Identity architecture adaptation#258
nap-liu wants to merge 84 commits intodataelement:mainfrom
nap-liu:feature/merge-upstream-20260401

Conversation

@nap-liu
Copy link
Copy Markdown

@nap-liu nap-liu commented Apr 2, 2026

Overview

Comprehensive enhancements to the Clawith platform covering channel capabilities, enterprise authentication, multi-tenant management, and full adaptation for the upstream Identity architecture refactor.


📡 DingTalk Channel Enhancements

Multimedia Message Support

  • Send and receive image, file, voice, and video messages
  • Thinking reaction indicator (emoji status during message processing)
  • Auto-reconnect with exponential backoff
  • /new command to reset sessions (DingTalk and Feishu)
  • Immediate ACK + message_id deduplication to prevent duplicate processing

User Matching

  • Multi-dimension matching chain: staff_id → username → unionid → mobile → email
  • Cross-channel user auto-merge via mobile/email
  • Unified DingTalk Token management with global cache

🔐 OAuth2 SSO Login

  • Complete Generic OAuth2 SSO login flow
  • Configurable Token URL, UserInfo URL, Scope
  • OAuth2 field mapping for different IdP user info formats
  • User matching chain: provider_user_id → email → mobile → create new
  • Unbound users auto-bind to current tenant
  • SSO callback URL dynamically resolved from request (multi-domain support)

🏢 Multi-Tenant Management

Company Administration

  • Platform admin company management page (CRUD + enable/disable)
  • Cascade delete with associated data cleanup
  • Auto-seed default agents on company creation
  • Invitation code management
  • Default tenant protection (cannot be disabled)

Domain Resolution

  • Three-level fallback: tenant domain → global config → request origin
  • Auto-generate tenant subdomain prefix
  • Replace hardcoded domains with dynamic resolve_base_url
  • Inject platform URL into agent system prompts
  • Dynamic webhook URL resolution

User Management

  • User profile editing (Profile API + EditUserModal)
  • Batch admin UI improvements (i18n, password reset, org member display)
  • Allow unbound users (tenant_id=None) to pass login checks

🏗 Identity Architecture Adaptation

Full adaptation after upstream introduced the global Identity system:

Core Fixes

  • Set lazy="selectin" on User.identity relationship to prevent MissingGreenlet crashes in async contexts
  • Fix association_proxy incorrectly used as SQL column expressions
  • Fix missing model imports in DingTalk message handler causing NameError

Channel Adaptation

  • Rewrite auth_provider login matching to join Identity table
  • Adapt DingTalk, Feishu, WeCom, and SSO channels for Identity lookups
  • Add merge migration to resolve dual alembic heads

🔒 Security Hardening

  • secrets.md: creator-only sensitive file with redaction pipeline
  • Sensitive data sanitization in WebSocket transport, activity logs, and agent-to-agent history
  • Migrate in-memory token caches to Redis (with memory fallback)
  • Audit fixes: missing imports, cascade delete gaps, async error logging

🛠 Other Improvements

  • SQL execution tool (MySQL/PostgreSQL/SQLite)
  • Multi-turn image context re-hydration
  • Auto-title sessions on first message
  • Dynamic context window size (remove hardcoded truncation)
  • Feishu tools: rename feishu_doc_sharefeishu_drive_share, add feishu_drive_delete
  • Replace all frontend emoji icons with Lucide React icons
  • i18n improvements (Chinese + English)

📦 Database Migrations

Migration Description
add_user_source User source tracking fields
merge_heads Merge existing migration branches
add_agent_credentials Agent credential management table
440261f5594f Identity architecture refactor
d9cbd43b62e5 LLM request_timeout field
f8a934bf9f17 Merge Identity + tenant_is_default heads

nap.liu added 30 commits March 29, 2026 02:56
…& UI polish

Backend:
- DingTalk: full media send/receive (image/file/voice/video)
- DingTalk: thinking reaction indicator
- DingTalk: auto-reconnect with exponential backoff
- DingTalk: user source tracking + sender_nick display
- /new command for DingTalk and Feishu session reset
- secrets.md: creator-only file with redaction pipeline
- Sensitive data sanitization (WebSocket, activity logs, A2A history)
- SQL execute tool (MySQL/PostgreSQL/SQLite)
- Multi-turn image context re-hydration
- Agent seeding only on new company creation
- Fix upstream Tenant.custom_domain -> sso_domain references

Frontend:
- Replace all emoji with Lucide React icons
- Add secrets section to Mind tab
- Chat tab moved to first position as default
- i18n cleanup (zh + en)
…dia-support

# Conflicts:
#	frontend/src/pages/EnterpriseSettings.tsx
…dia-support

# Conflicts:
#	backend/app/services/registration_service.py
#	backend/app/services/sso_service.py
…dia-support

# Conflicts:
#	backend/app/api/feishu.py
- Add DingTalkTokenManager singleton with per-app_key caching
- Token cached for 7200s, auto-refresh 300s before expiry
- Concurrency-safe with asyncio.Lock (prevents duplicate refresh)
- Replace 4 independent token functions across dingtalk_stream.py,
  dingtalk_reaction.py, and dingtalk_service.py
- Reduces token API calls from 5-6 per message to max 1
…_llm call

_call_agent_llm (defined in feishu.py) does not accept context_size.
The history is already truncated by SQL .limit(ctx_size) before the call,
so passing context_size was both unnecessary and caused TypeError.
…截断 - websocket 查询用 ctx_size 替代硬编码 .limit(20) - 各通道 fallback 默认值统一为 100
- Tenant model: new subdomain_prefix field (String(50), unique, indexed)
- Migration: add_subdomain_prefix (merges add_tool_source + merge_upstream_and_local heads)
- domain.py: fallback chain Level 2 - subdomain_prefix + global hostname
- tenants.py: check-prefix API, resolve-by-domain subdomain matching,
  schema updates (TenantOut/TenantUpdate), update_tenant validation
- AdminCompanies: EditCompanyModal with subdomain prefix input + availability check
- EnterpriseSettings: OrgTab Company URL display (read-only)
Prevent duplicate user creation when the same person uses both DingTalk
bot and SSO login. Match chain: username -> unionId (org_members) ->
mobile -> email -> create new.

- Add _get_corp_access_token() with in-memory cache (2h, refresh 5min early)
- Add _get_dingtalk_user_detail() for corp API user lookup
- Load ChannelConfig early for API calls before user matching
- Graceful degradation: API failure falls back to creating new user
# Conflicts:
#	backend/app/api/dingtalk.py
…nput from EditCompanyModal

- CompanyStats model now includes subdomain_prefix field
- list_companies endpoint now returns subdomain_prefix from tenant
- EditCompanyModal: removed the sso_domain (Custom Access Domain) input
  as subdomain_prefix + global domain covers this use case
- handleSave no longer sends sso_domain in payload
The backend _sync_tenant_sso_state() already auto-manages sso_enabled
based on active identity providers. The manual toggle was redundant and
caused confusion (users could configure SSO providers but forget to
enable the toggle).

Changes:
- AdminCompanies.tsx: remove ssoEnabled state/checkbox from EditCompanyModal,
  remove sso_enabled from updateCompany API call, update description text
- EnterpriseSettings.tsx: remove SSO on/off toggle from OrgTab SsoStatus,
  keep custom domain field always visible, remove sso_enabled from save payload
- backend: auto-generate subdomain_prefix from slug on company creation
- backend: add _generate_subdomain_prefix helper (strips random hex suffix)
- backend: add /tenants/check-slug API endpoint
- backend: TenantUpdate now accepts slug field
- backend: update_tenant validates + updates slug (format, reserved, uniqueness)
- frontend: EditCompanyModal refactored with slug edit + domain config section
- frontend: slug input with blur check-slug availability
- frontend: domain config section with clearer headings, no emoji icons
… preview URL fallback

- StatusBadge now uses t() for checking/available/taken text
- Subdomain prefix input placeholder now shows current company slug instead of hardcoded "acme"
- Preview URL falls back to slug when subdomain_prefix is empty
- Added admin.* i18n keys to en.json and zh.json (slug, slugDesc, domainConfig, domainConfigDesc, subdomainPrefix, prefixAvailable, prefixTaken, prefixChecking, plus backfill of other admin keys missing from en.json)
…pport merge)

The _get_corp_access_token() and _get_dingtalk_user_detail() helpers and
the 6-step user matching chain were accidentally dropped when feature/
dingtalk-media-support was merged via 2cb741a. This restores them so that
DingTalk bot messages match SSO users (via unionId/mobile/email) instead
of always creating dingtalk_xxx dummy accounts.
…tegy

- Fix _get_corp_access_token: use GET instead of POST (errcode 43001)
- Add sender_id parameter to process_dingtalk_message
- Redesign 5-step user matching: local org_members first, API last
  Step 1: sender_id -> org_members.external_id (fastest, no API)
  Step 2: sender_staff_id -> org_members.external_id (compat)
  Step 3: username = dingtalk_{staffId} (compat with old users)
  Step 4: DingTalk API -> unionId/mobile/email matching (first time only)
  Step 5: Create new user
- Auto-create org_member record after matching/creating user
  so subsequent messages hit Step 1 directly (zero API calls)
- Pass sender_id from dingtalk_stream.py through to process_dingtalk_message
- Tenant model: add is_default field
- alembic: migration to add is_default column, set earliest active tenant as default
- tenants API: support set/clear is_default (platform_admin only), remove slug update logic, remove check-slug endpoint
- resolve-by-domain: use is_default=True for global domain fallback instead of ORDER BY created_at
- AdminCompanies: show Default badge in company list
- EditCompanyModal: remove slug input, add default company toggle/indicator
- i18n: add en/zh translations for new features
nap.liu added 30 commits March 31, 2026 14:31
- username 匹配逻辑支持 tenant_id=NULL 的情况
- 匹配成功后自动绑定租户到现有 web 用户
- 用户名冲突时抛 409 异常而不是自动加后缀
- 修复 zhuzhichao 重复账号问题
- 之前会把超过 8 个字符的 provider_user_id 截断(如 zhuzhichao→zhuzhich)
- 现在直接使用完整的 provider_user_id 作为用户名
- 修复生产环境用户名截断问题
- 当 tenant_id 有值时,同时匹配该租户的用户和 tenant_id=NULL 的用户
- 修复早期 web 注册用户(无租户)无法通过 OAuth2 登录的问题
- 移除"此域名已启用单点登录"的蓝色提示框
- 保留 SSO 按钮和"or"分隔线
- 优化登录页面视觉效果
- 新建 backend/app/schemas/oauth2.py Pydantic 模型定义
  - OAuth2FieldMapping: 4 个字段映射字段
  - OAuth2Config: 7 个 OAuth2 配置字段 + URL 验证器
  - OAuth2ProviderCreate/Update: 严格 config 格式模型

- 修改 backend/app/api/enterprise.py
  - 导入新的 Pydantic 模型
  - 添加 IdentityProviderUpdate 类
  - 重写 create_oauth2_provider: 使用 OAuth2ProviderCreate
  - 重写 update_oauth2_provider: 使用 OAuth2ProviderUpdate,移除兼容逻辑
  - field_mapping 处理:None/null = 清空,{} = 清空,{...} = 自定义

- 修改 frontend/src/pages/EnterpriseSettings.tsx
  - initOAuth2FromConfig 返回完整 OAuth2Config
  - handleExpand 直接使用 config 结构
  - 所有 OAuth2 字段绑定到 form.config.xxx
  - 字段映射单个清空按钮 (✕)
  - 字段映射全部清空按钮 (🗑️)
  - Mutations 直接发送数据(无转换)

- 测试验证
  - 新建/编辑 OAuth2 Provider 正常
  - Scope 等字段更新正常
  - 字段映射单个清空正常
  - 字段映射全部清空正常

Closes: OAuth2 表单数据结构不一致问题
Restores:
- HTTPException import (P0: NameError on username conflict)
- OAuth2 field_mapping + _get_field system (front-end config had no effect)
- get_user_info_from_token_data with _get_field support (P0: sso.py:196 call)
- OAuth2 _create_new_user override (provider_user_id as username)
- Feishu token Redis cache with TTL (was pure memory, no expiry refresh)
- tenant_id OR NULL user matching (prevent duplicate accounts for legacy users)
Changes:
- dingtalk: save mobile/email from corp API when creating new users (Step4)
- dingtalk: add Step3d display_name unique match as fallback
- dingtalk: enrich existing matched users with mobile/email from corp API
- dingtalk: add Step3 user_detail logging for debugging
- dingtalk: also check org_email field from corp API
- auth_provider: add Step6 display_name unique match as fallback

Covers all 6 registration order scenarios:
- OAuth2 first → DingTalk (with/without mobile): merge via mobile/display_name
- DingTalk first → OAuth2 (with/without mobile): merge via mobile/display_name
- Single channel only: no change
Merged 88 upstream commits. Resolved 14 conflict files:
- dingtalk.py: kept our cross-channel user merge implementation
- sso.py: adopted upstream platform_service for base URL resolution
- auth.py: merged upstream registration/email features with our tenant-scoped login check
- enterprise.py: adopted upstream platform_service SSO domain auto-assign
- feishu.py: adopted upstream channel_user_service (our OrgMember logic was redundant)
- tenants.py: merged upstream multi-tenant self-create with our subdomain_prefix + agent seeding
- agent_tools.py: adopted upstream platform_service for webhook URL
- Login.tsx: adopted upstream localhost skip for tenant resolution
- EnterpriseSettings.tsx: adopted upstream SsoChannelSection component, kept our i18n IDP descriptions
- AgentDetail.tsx: adopted upstream chat-composer CSS refactor
- zh.json: merged all i18n keys from both sides
- teams.py: removed redundant agent reload (already loaded earlier)
- wecom.py: adopted upstream auth_provider pattern (handles OrgMember internally)
- ChannelConfig.tsx: merged imports from both sides
…platform_service

- sso.py: use resolve_base_url instead of platform_service for OAuth callbacks
- auth.py: use resolve_base_url for SSO redirect URL
- enterprise.py: use resolve_base_url for SSO address display
- platform_service.py: add system_settings DB lookup as fallback (ENV > DB > Request)
- AdminCompanies.tsx: restore platform domain config UI (Public URL card)
- auth_provider: _create_new_user uses find_or_create_identity, fixed variable order bug
- auth_provider: OAuth2 _create_new_user override adapted to Identity model
- auth_provider: _update_existing_user operates through user.identity
- auth_provider: Step5 username query via join(Identity)
- auth_provider: ensure identity loaded before updating proxy fields
- dingtalk: Step2/3b/3c queries via join(Identity)
- dingtalk: Step4 creates Identity before User
- dingtalk: update matched users through identity relationship
- sso_service: match_user_by_email/mobile via join(Identity) with selectinload
- sso.py: get_sso_session_status loads identity for UserOut validation
- EnterpriseSettings.tsx: callback URL falls back to window.location.origin
…ations

- tenants.py: strip port from domain before subdomain prefix matching
- users.py: admin user edit operates through identity relationship
- wecom_stream.py: user creation via find_or_create_identity
- agent_tools.py: user lookup via join(Identity)
…ion_proxy lazy loading

- Set User.identity relationship to lazy="selectin" for automatic eager loading
- Fix chat_sessions and agent_tools using association_proxy as SQL column expression
- Add missing UserModel import in dingtalk message handler
- Add selectinload for creator query in agent detail endpoint
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant