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
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 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.