refactor: 移除 FakeMessagePlatform channel 全部逻辑、展示、配置

- 删除 channels/fake/ 整个 Node.js FakeMessagePlatform (8文件)
- 删除后端 FakeProvider (fake.go + fake_test.go) + FakeWebhookHandler (fake_webhook.go)
- bootstrap.go: 移除 FakeProvider 接线 + isFakeChannelEnabled()
- router.go: 移除 FakeWebhook 字段 + /webhooks/fake/:identifier 路由
- provider.go: 移除 ChannelFake 常量
- inbox_service.go: 移除 fake channel 类型验证 + isFakeChannelAllowed()
- 前端: 移除 Fake.vue + ChannelFactory/ChannelList/ChannelItem 引用 + i18n (en/zh_CN)
- package.json: 移除 fake:start/dev/test/dev:all 脚本
- pnpm-workspace.yaml: 移除 channels/fake
- .env/.env.example/quickstart: 移除 GOCHAT_ALLOW_FAKE_CHANNEL
- 测试: bridge_listener_test 改用 web_widget, router_test 移除 fake 路由断言
- 保留 fake:ai LLM Provider (backend/internal/llm/fake_provider.go) — 与 channel 无关
This commit is contained in:
Rogee
2026-07-30 09:09:17 +08:00
parent 92f0d51375
commit 04b999e377
31 changed files with 8 additions and 2537 deletions
@@ -1,201 +0,0 @@
package webhook
// FakeWebhookHandler processes incoming FakeMessagePlatform webhook HTTP
// requests via Gin.
//
// URL pattern: /webhooks/fake/:identifier
// - GET: webhook verification (echo challenge)
// - POST: incoming message/event processing
//
// Unlike Telegram/LINE which have a dedicated channel model table, the fake
// channel stores all config (identifier, webhook_url, token) directly in the
// Inbox.ChannelConfig JSON column. lookupInbox therefore queries by
// channel_type='fake' and filters the identifier in Go (plan §7.2 错误 2).
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/channel"
channelprovider "github.com/gochat/gochat/internal/channel/provider"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/worker"
applogger "github.com/gochat/gochat/pkg/logger"
)
// FakeWebhookHandler processes FakeMessagePlatform webhook requests via Gin.
type FakeWebhookHandler struct {
provider *channelprovider.FakeProvider
db *gorm.DB
persister *IncomingPersister
}
// NewFakeWebhookHandler creates a Fake webhook handler for Gin integration.
func NewFakeWebhookHandler(
provider *channelprovider.FakeProvider,
db *gorm.DB,
dispatcher ...*channel.Dispatcher,
) *FakeWebhookHandler {
return &FakeWebhookHandler{
provider: provider,
db: db,
persister: NewIncomingPersister(db, dispatcher...),
}
}
func (h *FakeWebhookHandler) WithWorkerPool(wp *worker.WorkerPool) *FakeWebhookHandler {
if h != nil && h.persister != nil {
h.persister.SetWorkerPool(wp)
}
return h
}
func (h *FakeWebhookHandler) WithSearchIndexer(indexer IncomingSearchIndexer) *FakeWebhookHandler {
if h != nil && h.persister != nil {
h.persister.SetSearchIndexer(indexer)
}
return h
}
// HandleFakeWebhookVerification echoes a challenge token for webhook URL
// verification. FakeMessagePlatform sends GET /webhooks/fake/:identifier with
// a "hub.challenge" query param (mirroring the Facebook/WhatsApp pattern).
func (h *FakeWebhookHandler) HandleFakeWebhookVerification(c *gin.Context) {
challenge := c.Query("hub.challenge")
if challenge == "" {
challenge = c.Query("challenge")
}
if challenge == "" {
c.JSON(http.StatusOK, gin.H{"status": "verified"})
return
}
c.String(http.StatusOK, challenge)
}
// HandleFakeWebhook processes an incoming FakeMessagePlatform webhook POST.
func (h *FakeWebhookHandler) HandleFakeWebhook(c *gin.Context) {
identifier := c.Param("identifier")
if identifier == "" {
applogger.L().Warn("Fake webhook: missing identifier in path")
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
return
}
// Read request body
body, err := io.ReadAll(c.Request.Body)
if err != nil {
applogger.L().Errorf("Fake webhook: failed to read body: %v", err)
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
return
}
defer c.Request.Body.Close()
applogger.L().Infof("Fake webhook received for identifier=%s", identifier)
// Look up Inbox by channel_type='fake' and config identifier
inbox, err := h.lookupInbox(identifier)
if err != nil {
applogger.L().Errorf("Fake webhook: inbox lookup failed for identifier=%s: %v", identifier, err)
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
return
}
// Validate webhook request (X-Fake-Token header)
headers := make(map[string]string)
for k, v := range c.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
wr := &channel.WebhookRequest{
ChannelType: channel.ChannelFake,
Identifier: identifier,
Headers: headers,
Body: body,
Method: c.Request.Method,
}
if err := h.provider.ValidateWebhookRequest(c.Request.Context(), inbox, wr); err != nil {
applogger.L().Warnf("Fake webhook: validation failed for inbox=%d: %v", inbox.ID, err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
// Process the incoming message via the provider
incomingMsg, err := h.provider.ProcessIncoming(c.Request.Context(), inbox, body)
if err != nil {
applogger.L().Errorf("Fake webhook: message processing failed for inbox=%d: %v", inbox.ID, err)
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
return
}
if incomingMsg == nil {
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
return
}
// Persist the incoming message
if _, persistErr := h.persister.PersistIncoming(c.Request.Context(), inbox, incomingMsg); persistErr != nil {
applogger.L().Errorf("Fake webhook: persist message failed for inbox=%d source_id=%s: %v",
inbox.ID, incomingMsg.SourceID, persistErr)
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
return
}
applogger.L().Infof("Fake webhook: message persisted (inbox_id=%d, source_id=%s, sender=%s)",
inbox.ID, incomingMsg.SourceID, incomingMsg.SenderID)
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
// lookupInbox finds the Inbox for a fake channel by config identifier.
// On PostgreSQL, uses jsonb containment for a DB-side indexed lookup.
// On SQLite (test mode), falls back to loading all fake inboxes and
// filtering in Go — acceptable for low-volume test traffic.
func (h *FakeWebhookHandler) lookupInbox(identifier string) (*model.Inbox, error) {
if h.db == nil {
return nil, fmt.Errorf("fake webhook database is not configured")
}
// PostgreSQL: use jsonb @> for a server-side, indexable query.
if h.db.Dialector.Name() == "postgres" {
var inbox model.Inbox
// channel_config is TEXT, so cast to jsonb before using the @> operator.
// channel_config::jsonb @> '{"identifier":"<id>"}'
query := fmt.Sprintf(`channel_type = 'fake' AND channel_config::jsonb @> '{"identifier":"%s"}'`, identifier)
if err := h.db.Where(query).First(&inbox).Error; err != nil {
return nil, fmt.Errorf("fake inbox not found for identifier=%s: %w", identifier, err)
}
return &inbox, nil
}
// SQLite fallback: load all fake inboxes and filter in Go.
var inboxes []model.Inbox
if err := h.db.Where("channel_type = ?", "fake").Find(&inboxes).Error; err != nil {
return nil, fmt.Errorf("failed to query fake inboxes: %w", err)
}
for i := range inboxes {
cfg := parseFakeInboxConfig(&inboxes[i])
if id, _ := cfg["identifier"].(string); id == identifier {
return &inboxes[i], nil
}
}
return nil, fmt.Errorf("fake inbox not found for identifier=%s", identifier)
}
// parseFakeInboxConfig decodes the Inbox.ChannelConfig JSON into a map.
func parseFakeInboxConfig(inbox *model.Inbox) map[string]interface{} {
if inbox == nil || inbox.ChannelConfig == "" {
return map[string]interface{}{}
}
var cfg map[string]interface{}
if err := json.Unmarshal([]byte(inbox.ChannelConfig), &cfg); err != nil {
applogger.L().Warnf("Fake: failed to parse ChannelConfig for inbox %d: %v", inbox.ID, err)
return map[string]interface{}{}
}
return cfg
}