- Guard fake channel with GOCHAT_ENV check: skip init() registration,
bootstrap wiring, and inbox creation in production
- Reject empty-token webhooks in production (was silently skipped)
- Use PostgreSQL jsonb @> query for inbox lookup, keep SQLite fallback
- Replace isValidURL string-prefix hack with net/url.Parse
- Handle typing.start/typing.stop by returning nil (no garbage messages)
- Narrow Capabilities to only implemented features (Attachments, Replies)
- Hide fake channel from frontend channel list in production builds
- Add Chinese translations for ASSIGNMENT_POLICY, CONVERSATION_WORKFLOW
sections and SIDEBAR labels in zh_CN settings.json
- Fix AssignmentCard.vue: v-for :key used feature.id which was always
undefined (feature objects have no id field), causing duplicate keys
and Vue reconciliation crash (Cannot read properties of null reading
parentNode) when navigating to the Agent Assignment settings page
Renumber migrations from merged feature/ai-feature-activation branch
to resolve golang-migrate duplicate version conflict:
- 000048_add_captain_auto_reply_rules → 000050
- 000049_add_article_embedding_vector → 000051
Both migrations were already applied to the database; schema_migrations
version updated from 49 to 51 to reflect the new numbering.
EinoProvider (eino_provider.go, new):
- Implements GoChat's llm.Provider interface by delegating to Eino's
model.BaseChatModel (Generate + Stream) and embedding.Embedder
- Converts GoChat ChatMessage ↔ Eino schema.Message (role/content/tool_calls)
- Converts Eino ResponseMeta (FinishReason/Usage) → GoChat ChatResponse
- Converts Eino StreamReader → GoChat onChunk callback for SSE streaming
- Embedder type alias = eino's embedding.Embedder interface
NewProviderFromConfig factory:
- Uses eino-ext/components/model/openai.NewChatModel for chat model
- Uses eino-ext/components/embedding/openai.NewEmbedder for embeddings
- Works for all OpenAI-compatible providers (OpenAI/Azure/Ark/Doubao/Qwen)
by setting llm_base_url in config
- Graceful fallback to hand-written OpenAIProvider if Eino init fails
- Removed previous Anthropic provider switch (Eino's OpenAI impl handles
Claude via OpenAI-compat endpoint)
Dependencies added:
- github.com/cloudwego/eino v0.9.12 (core framework)
- github.com/cloudwego/eino-ext/components/model/openai v0.1.13
- github.com/cloudwego/eino-ext/components/embedding/openai
Verified:
- go build ./... passes
- go vet passes (llm + app packages)
- go test passes (llm + service, SQLite mode)
- Server starts with Eino provider initialized
- All existing routes work (assistants, auto-reply, RAG, conversation)
- RAG query reaches Eino provider (fails on LLM call without API key,
confirming Eino is the active provider)
- token_estimator.go (new): TokenEstimator with ~4 chars/token heuristic,
EstimateText/EstimateMessages methods, TruncateMessages sliding window
that drops oldest messages to fit token budget, BuildContextWindow
entry point that converts conversation messages to LLM format with
token-budgeted truncation (default 4096 tokens)
- copilot_context_service.go: fetch up to 100 messages (was 20), then
apply BuildContextWindow truncation to fit within 4096 token budget;
log how many messages were dropped
Verified: go build + go vet + go test all pass
Semantic search route reaches handler (times out on LLM call without API key,
confirming route + service wiring is correct)
- anthropic_provider.go (new): AnthropicProvider implementing the Provider
interface using Claude's messages API. Handles:
- System prompt as top-level param (not in messages array)
- Content blocks response format → extract text
- SSE streaming with Anthropic event types (content_block_delta, message_stop)
- Anthropic-specific headers (x-api-key, anthropic-version)
- Retry with exponential backoff (shared logic with OpenAI provider)
- Embedding API returns error (Anthropic has no embeddings; OpenAI-compat
provider should be used for embeddings)
- NewProviderFromConfig factory function: selects AnthropicProvider for
provider="anthropic"/"claude", OpenAIProvider for all others
- bootstrap.go: use NewProviderFromConfig instead of hardcoded NewOpenAIProvider,
allowing config.captain.llm_provider to switch between providers
Note: domestic providers (Volcengine/Doubao/Qwen) use OpenAI-compatible API
and work with the existing OpenAIProvider by setting llm_base_url.
Verified: go build + go vet + go test all pass
- llm/provider.go: extend ChatMessage with ToolCalls, ToolCallID, Name
fields; add ToolCall + ToolCallFunction structs for parsing LLM
function call responses
- tool_execution_service.go (new): ToolExecutionService that converts
CaptainCustomTool → LLM ToolDefinition, executes HTTP tool calls
(GET/POST/PUT with bearer/basic/api-key auth), and runs the full
tool_call loop (LLM → tool_call → execute → result → LLM → final
answer) with maxIterations safeguard
- captain_conversation_service.go: add toolExecSvc field +
SetToolExecutionService method; use RunToolCallLoop in
generateConversationResponse when tools are available, with graceful
fallback to plain LLM call on error
- bootstrap.go: instantiate ToolExecutionService and inject into
CaptainConversationService
Verified: go build + go vet + go test all pass
The seed data only enabled 16 of 37 feature flags used by the frontend.
Two missing flags caused sidebar navigation bugs found during QA:
- agent_bots: the '机器人' settings link was hidden (isAllowed=false)
- campaigns: the '活动' (Campaigns) parent had no accessible children,
so clicking it did nothing instead of expanding to show sub-items
Add all 37 feature flags from FEATURE_FLAGS to the smoke seed data so
every sidebar menu item is visible and navigable.
Also includes QA test report for the full-page CDP testing session.
The canned_responses table was registered in autoMigrate() (app.go) but
autoMigrate() is never called on the serve path (Bootstrap -> NewDatabase
-> RunMigrations only). With no SQL migration file, the table never
existed, so POST /api/v1/accounts/:id/canned_responses failed with
'relation does not exist' -> 500.
Add migration 000049 to create the table matching the GORM model
(id, account_id, content, short_code, timestamps) with the account+
short_code partial unique index and deleted_at index.
Also include two related fixes staged in the working tree:
- router.go: register notifications.GET("") alongside GET("/") to avoid
Gin 301 trailing-slash redirect that the Vite proxy doesn't follow
- notifications/actions.js: stop the infinite loader on fetch error to
prevent IntersectionObserver re-fire loop
Account.Locale gorm default was changed from 'en' to 'zh_CN' in the
locale cleanup commit (a1ae852), but the default-values test still
asserted 'en', causing TestAccountDefaultValues to fail. Align the
assertion with the new default.
ISS-01: Fix reporting_events_rollups table schema mismatch
- The init migration (000001) created columns (dimension, dimension_value,
metric_name, value, value_in_business_hours, period) that do not match
the GORM model which expects (date, dimension_type, dimension_id, metric,
count, sum_value, sum_value_business_hours)
- Add migration 000048 to drop and recreate the table with correct schema
matching the GORM model and all Go code that references it
- Fixes: GET /api/v2/accounts/:id/reports returning HTTP 500 with
'column date does not exist (SQLSTATE 42703)'
ISS-02: Fix Settings > General page white screen
- AccountId.vue: add null-safe access for currentAccount.value.id before
calling toString() — currentAccount can be {} when account data not yet
loaded in store
- BuildInfo.vue: add null-safe access for globalConfig.value.gitSha and
globalConfig.value.appVersion; guard copyGitSha with existence check;
add v-if guard on gitSha span in template
ISS-04: Fix SidebarGroup mounted hook exceptions
- SidebarGroup.vue: Object.keys(child.to.params) throws TypeError when
child.to.params is undefined; add null-safe fallback Object.keys(child.to?.params ?? {})
- This was the source of the 29 recurring JS exceptions on every Settings
page navigation
ISS-05: Fix Vue Router root path warning
- Add root route { path: '/', redirect: '/app/login' } to silence
'No match found for location with path /' warning
- widget/i18n/index.js: only import en.json and zh_CN.json (the only
locale files present); remove 40+ imports for missing locale JSONs
that caused Vite compile failure and global white screen
- dashboard/i18n/index.js: remove unused locale imports for consistency
- Remove 62 unused locale JSON files from widget/i18n/locale/
- Minor: update index.html, test helpers, e2e test, languages spec
Replace '/rails/active_storage/direct_uploads' with GoChat endpoint
'/api/v1/widget/direct_uploads' in two dashboard upload components:
- ActionButtons.vue (new conversation attachment upload)
- ReplyBottomPanel.vue (conversation reply attachment upload)
These data attributes are consumed by vue-upload-component; the actual
upload logic in useFileUpload/fileUploadMixin already used the correct
GoChat endpoints. Spotted by decoupling audit report.