Claude API cannot fetch arbitrary URLs. Text-based attachments (txt, csv,
json, md) are now downloaded via their presigned MinIO URL and embedded
directly as text blocks. PDF uses Claude's native document block. Added
50KB size limit with truncation for large text files.
buildMessages() is now async to support text content fetching.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Coordinator now sends all attachment types to Claude:
- Images → native image blocks (existing)
- PDF → native document blocks (Claude PDF support)
- Text files (txt, csv, json, md) → text blocks with filename
Extracted common buildAttachmentBlocks() helper.
2. File-service generates thumbnails on image upload:
- Uses sharp to resize to 400x400 max (inside fit, no upscale)
- Output as WebP at 80% quality for smaller file size
- Stored in MinIO under thumbnails/ prefix
- Generated for both direct upload and presigned URL confirm
- Non-blocking: thumbnail failure doesn't break upload
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The model was putting internal notes like "引导回移民话题" in the followUp
field instead of actual user-facing questions. Two fixes:
1. Schema: describe followUp as "必须以?结尾,禁止填写内部策略备注"
2. agent-loop: only yield followUp if it contains ?or ? (question mark)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
claude-sonnet-4-20250514 does not support output_config (structured outputs).
Changed coordinator model to claude-sonnet-4-5-20250929 which supports it.
Specialist agents remain on their original models (no output_config needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- findForEvolution() now excludes DELETED conversations (should not learn from deleted data)
- getConversation() rejects DELETED conversations for user-facing operations (sendMessage, getMessages, etc.)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The delete conversation endpoint was a no-op — it verified ownership but
never actually modified the record. Users saw conversations disappear
(frontend optimistic removal) but they reappeared on refresh.
Changes:
- conversation.entity.ts: Add DELETED status, softDelete() and isDeleted()
- conversation.service.ts: Call softDelete() + update instead of no-op
- conversation-postgres.repository.ts: Exclude DELETED conversations
from findByUserId() queries so they don't appear in user's list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AdminConversationController's GET /:id was intercepting requests to
AdminEvaluationRuleController (matching "evaluation-rules" as an id param).
Similarly, DELETE /:id was matching "cache" as an id.
Changes:
- conversation.module.ts: Register AdminMcpController and
AdminEvaluationRuleController before AdminConversationController
(more specific prefixes must come first in NestJS)
- admin-evaluation-rule.controller.ts: Move static routes (POST /test,
DELETE /cache) before dynamic routes (GET/:id, DELETE/:id)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The AdminEvaluationRuleController in ConversationModule needs the
EVALUATION_RULE_REPOSITORY token. Even though AgentsModule is @Global(),
Symbol-based providers must be explicitly exported to be available
in other modules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
claude-haiku-3-5-20241022 returns 404 on the proxy. Updated to
claude-haiku-4-5-20251001 in agent configs and context injector.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The mapEventToStreamChunk was mapping both 'usage' (per-turn) and 'end'
(final) events to type 'end', causing the gateway to emit multiple
stream_end events. This made the frontend create a separate message
bubble (with its own bot avatar) for each agent loop turn.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WebSocket gateway was missing AsyncLocalStorage tenant context setup,
causing 'Tenant context not set' error on every message. Now extracts
tenantId from handshake and wraps handleMessage in tenantContext.runAsync().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add tenant_id column to conversations, messages, token_usages tables
- Create standalone migration SQL script for production deployment
- Add agent_executions table to init-db.sql for new installations
- Fix MessageORM created_at nullable mismatch with database schema
- Backfill existing data with default tenant ID
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace IKnowledgeClient TypeScript interface (erased at runtime) with KnowledgeClientService concrete class injection
- Fix method signatures to match KnowledgeClientService API:
- getUserMemories() → getUserTopMemories(), field type → memoryType
- retrieveForPrompt(query, userId) → retrieveForPrompt({ query, userId })
- getRelevantExperiences(query, n) → searchExperiences({ query, limit: n }), field type → experienceType
- Remove unused ContextData import
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All services were providing TenantContextService directly without
making it global, causing DI resolution failures in child modules.
Now using TenantContextModule.forRoot() which exports TenantContextService
globally so all repositories can access it.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
npm install was clearing the @iconsulting/shared folder that was
copied before it. Moving the COPY command after npm install ensures
the shared package remains in node_modules.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Apply TenantContextMiddleware to all 6 services
- Add SimpleTenantFinder for services without direct tenant DB access
- Add TenantFinderService for evolution-service with database access
- Refactor 8 repositories to extend BaseTenantRepository:
- user-postgres.repository.ts
- verification-code-postgres.repository.ts
- conversation-postgres.repository.ts
- message-postgres.repository.ts
- token-usage-postgres.repository.ts
- file-postgres.repository.ts
- order-postgres.repository.ts
- payment-postgres.repository.ts
- Add @iconsulting/shared dependency to evolution-service and knowledge-service
- Configure middleware to exclude health and super-admin paths
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add token aggregation to statistics/overview endpoint
- Include total tokens, cost, and API calls for all time
- Include today's token usage and cost breakdown
- Display token stats in ConversationsPage with 2 rows of cards
- Add formatNumber helper for K/M number formatting
- Export GlobalTokenStats and TodayTokenStats types
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 'error' chunk type to StreamChunk for partial token capture
- Record partial tokens to token_usage table even on API errors
- Capture error chunk tokens in conversation.service.ts
- Save partial response and tokens before re-throwing errors
- Add token aggregation from token_usage table for accurate stats
- Display detailed token info in admin (cache tokens, cost, API calls)
- Export TokenDetails type for frontend consumption
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Problem:
- Token usage was recorded to token_usage table but not to conversation entity
- Message count was not being incremented
- Dashboard showed 0 tokens for all conversations
Solution:
- Add inputTokens/outputTokens fields to StreamChunk interface
- Return token usage in 'end' chunk from ClaudeAgentServiceV2
- Capture token usage in conversation.service.ts sendMessage
- Call conversation.addTokens() and incrementMessageCount() after each exchange
- Consolidate conversation updates into single repo.update() call
Files changed:
- claude-agent-v2.service.ts: Add token fields to StreamChunk, return in 'end'
- conversation.service.ts: Track tokens and message counts properly
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
## Device Tracking (conversation-service)
- Add DeviceInfoDto class for validating device information
- Extract client IP from X-Forwarded-For and X-Real-IP headers
- Capture User-Agent header automatically on conversation creation
- Support optional fingerprint and region from client
- Pass deviceInfo through service layer to entity for persistence
Files changed:
- conversation.controller.ts: Add extractClientIp() method and header capture
- conversation.dto.ts: Add DeviceInfoDto with validation decorators
- conversation.service.ts: Update CreateConversationParams interface
## Build Optimization (admin-client)
- Implement code splitting via Rollup manualChunks
- Separate vendor libraries into cacheable chunks:
- vendor-react: react, react-dom, react-router-dom (160KB)
- vendor-antd: antd, @ant-design/icons (1013KB)
- vendor-charts: recharts (409KB)
- vendor-data: @tanstack/react-query, axios, zustand (82KB)
- Main bundle reduced from 1732KB to 61KB (96% reduction)
- Set chunkSizeWarningLimit to 1100KB for antd
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add TransactionService for atomic database operations with optimistic lock retry
- Implement pessimistic locking in payment callback handling to prevent race conditions
- Add idempotency check via transactionId unique index to prevent duplicate processing
- Add version columns to PaymentORM and OrderORM for optimistic locking
- Add composite indexes for performance (order_status, transaction_id)
- Optimize connection pool settings for both payment and conversation services
- Update init-db.sql with version columns and new indexes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Breaking change: evolution-service no longer directly accesses
conversations and messages tables.
Changes:
- Add internal API endpoints to conversation-service for service-to-service calls
- Create ConversationClient in evolution-service to call conversation-service API
- Remove ConversationORM and MessageORM from evolution-service
- Update evolution.service to use ConversationClient
This follows microservices best practices:
- Each service owns its data
- Services communicate via API, not shared tables
TODO: Apply same pattern to system_experiences (knowledge-service)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use init-db.sql for schema management instead of TypeORM auto-sync.
synchronize:true is dangerous in production and causes conflicts
when multiple services share tables.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add @Index decorators for conversation_id, created_at, role
- Set created_at to timestamptz type to match database
- Set columns nullable to match database schema
This prevents synchronize:true from trying to modify columns
that have dependent indexes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add input_tokens and output_tokens columns that evolution-service
defines to prevent synchronize:true from trying to drop them.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add columns that evolution-service defines to prevent synchronize:true
from trying to drop them:
- userMessageCount, assistantMessageCount
- totalInputTokens, totalOutputTokens
- rating, feedback, hasConverted
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove migration-based approach which kept failing
- Enable synchronize:true to auto-sync Entity with database
- Schema will always match Entity definition on startup
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add migrations path to TypeORM config
- Set migrationsRun: true to run pending migrations on startup
- This ensures V2 columns are created automatically when app starts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add data-source.prod.ts for compiled JS migrations
- Add migration:run:prod script to package.json
- Update deploy.sh to try prod migration first, fallback to dev
- Keep SQL fallback in full-reset as safety net with proper indexes
This ensures migrations work in Docker where ts-node may not be available.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Switch ConversationService to use ClaudeAgentServiceV2
- Pass consultingState and deviceInfo from conversation to context
- Handle state_update chunks and save updated state to database
- Move dotenv to dependencies for migration runtime
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>