BUG-F (P1): Fake webhook lookupInbox used JSONB @> operator on a TEXT
column, causing all fake webhooks to return "ignored". Cast
channel_config::jsonb before the @> operator.
BUG-G (P3): Vue Router history.state warning on Activity page. Four
call sites replaced history.state with null/{}, destroying Vue Router's
internal navigation state. Now all replaceState calls preserve
window.history.state.
BUG-H (P3): Inbox list showed stale data because cache_keys endpoint
returned hardcoded "0000000000" for inbox/label/team, so the frontend
IndexedDB cache never invalidated. Cache keys are now derived from
actual DB state (row count + MAX(updated_at)), with defensive fallback
for missing tables.
BUG-I (P3): All worker goroutines shared the same Redis consumer name,
so XINFO CONSUMERS showed 1 consumer instead of N. Each goroutine now
generates a unique consumer ID (workerID-index).
- 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