chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:54 +08:00
commit bf9395e022
2349 changed files with 588574 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package source
import (
"context"
"encoding/json"
"fmt"
"log"
"regexp"
"strings"
"time"
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
larkevent "github.com/larksuite/oapi-sdk-go/v3/event"
"github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
larkws "github.com/larksuite/oapi-sdk-go/v3/ws"
"github.com/larksuite/cli/internal/event"
"github.com/larksuite/cli/internal/event/protocol"
)
const maxEventBodyBytes = 1 << 20 // bound per-subscriber sendCh memory under runaway payloads
type FeishuSource struct {
AppID string
AppSecret string
Domain string
Logger *log.Logger
}
func (s *FeishuSource) Name() string { return "feishu-websocket" }
func (s *FeishuSource) Start(ctx context.Context, eventTypes []string, emit func(*event.RawEvent), notify StatusNotifier) error {
d := dispatcher.NewEventDispatcher("", "")
rawHandler := s.buildRawHandler(emit)
for _, et := range eventTypes {
d.OnCustomizedEvent(et, rawHandler)
}
opts := []larkws.ClientOption{larkws.WithEventHandler(d)}
if s.Domain != "" {
opts = append(opts, larkws.WithDomain(s.Domain))
}
if s.Logger != nil || notify != nil {
opts = append(opts, larkws.WithLogLevel(larkcore.LogLevelInfo))
opts = append(opts, larkws.WithLogger(&sdkLogger{l: s.Logger, notify: notify}))
}
if notify != nil {
notify(protocol.SourceStateConnecting, "")
}
cli := larkws.NewClient(s.AppID, s.AppSecret, opts...)
errCh := make(chan error, 1)
go func() { errCh <- cli.Start(ctx) }()
select {
case <-ctx.Done():
return ctx.Err()
case err := <-errCh:
return err
}
}
// buildRawHandler is extracted from Start so unit tests can exercise it without a WS client.
func (s *FeishuSource) buildRawHandler(emit func(*event.RawEvent)) func(context.Context, *larkevent.EventReq) error {
return func(_ context.Context, e *larkevent.EventReq) error {
if e.Body == nil {
return nil
}
if len(e.Body) > maxEventBodyBytes {
if s.Logger != nil {
s.Logger.Printf("[feishu] drop oversized event: %d bytes > cap %d", len(e.Body), maxEventBodyBytes)
}
return nil
}
var envelope struct {
Header struct {
EventID string `json:"event_id"`
EventType string `json:"event_type"`
CreateTime string `json:"create_time"`
} `json:"header"`
}
if err := json.Unmarshal(e.Body, &envelope); err != nil {
if s.Logger != nil {
preview := string(e.Body)
if len(preview) > 200 {
preview = preview[:200] + "...(truncated)"
}
s.Logger.Printf("[feishu] drop malformed event: unmarshal error: %v body=%s", err, preview)
}
return nil
}
if envelope.Header.EventID == "" || envelope.Header.EventType == "" {
if s.Logger != nil {
s.Logger.Printf("[feishu] drop event missing header fields: event_id=%q event_type=%q",
envelope.Header.EventID, envelope.Header.EventType)
}
return nil
}
emit(&event.RawEvent{
EventID: envelope.Header.EventID,
EventType: envelope.Header.EventType,
SourceTime: envelope.Header.CreateTime,
Payload: json.RawMessage(e.Body),
Timestamp: time.Now(),
})
return nil
}
}
// sdkLogger forwards every SDK line to bus.log; lifecycle lines also fire notify.
type sdkLogger struct {
l *log.Logger
notify StatusNotifier
}
func (a *sdkLogger) Debug(_ context.Context, _ ...interface{}) {}
func (a *sdkLogger) Info(_ context.Context, args ...interface{}) {
msg := fmt.Sprint(args...)
if a.l != nil {
a.l.Output(2, "[SDK] "+msg)
}
a.tryNotify(msg, "")
}
func (a *sdkLogger) Warn(_ context.Context, args ...interface{}) {
msg := fmt.Sprint(args...)
if a.l != nil {
a.l.Output(2, "[SDK WARN] "+msg)
}
a.tryNotify(msg, "")
}
func (a *sdkLogger) Error(_ context.Context, args ...interface{}) {
msg := fmt.Sprint(args...)
if a.l != nil {
a.l.Output(2, "[SDK ERROR] "+msg)
}
// Errors usually manifest as disconnects; pass msg as detail.
a.tryNotify(msg, msg)
}
var reconnectAttemptRe = regexp.MustCompile(`reconnect:?\s*(\d+)`)
// tryNotify uses HasPrefix (not Contains): "connected to" matches inside "disconnected to" otherwise.
func (a *sdkLogger) tryNotify(msg, errDetail string) {
if a.notify == nil {
return
}
lower := strings.ToLower(msg)
switch {
case strings.HasPrefix(lower, sdkLogReconnecting):
detail := ""
if m := reconnectAttemptRe.FindStringSubmatch(lower); len(m) == 2 {
detail = "attempt " + m[1]
}
a.notify(protocol.SourceStateReconnecting, detail)
case strings.HasPrefix(lower, sdkLogDisconnected):
a.notify(protocol.SourceStateDisconnected, errDetail)
case strings.HasPrefix(lower, sdkLogConnected):
a.notify(protocol.SourceStateConnected, "")
}
}
var _ larkcore.Logger = (*sdkLogger)(nil)
+107
View File
@@ -0,0 +1,107 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package source
import (
"bytes"
"context"
"log"
"strings"
"testing"
larkevent "github.com/larksuite/oapi-sdk-go/v3/event"
"github.com/larksuite/cli/internal/event"
)
func TestRawHandlerLogsMalformedJSON(t *testing.T) {
var buf bytes.Buffer
s := &FeishuSource{Logger: log.New(&buf, "", 0)}
emitted := 0
handler := s.buildRawHandler(func(_ *event.RawEvent) { emitted++ })
req := &larkevent.EventReq{Body: []byte("not-json-{{{")}
if err := handler(context.Background(), req); err != nil {
t.Fatalf("handler returned err: %v", err)
}
if emitted != 0 {
t.Errorf("expected 0 emits, got %d", emitted)
}
out := buf.String()
if !strings.Contains(out, "malformed") {
t.Errorf("expected log to mention 'malformed', got: %s", out)
}
if !strings.Contains(out, "not-json") {
t.Errorf("expected log to include body preview, got: %s", out)
}
}
func TestRawHandlerLogsMissingHeaderFields(t *testing.T) {
var buf bytes.Buffer
s := &FeishuSource{Logger: log.New(&buf, "", 0)}
emitted := 0
handler := s.buildRawHandler(func(_ *event.RawEvent) { emitted++ })
req := &larkevent.EventReq{Body: []byte(`{"header":{"event_type":"im.receive"}}`)}
handler(context.Background(), req)
req2 := &larkevent.EventReq{Body: []byte(`{"header":{"event_id":"abc"}}`)}
handler(context.Background(), req2)
if emitted != 0 {
t.Errorf("expected 0 emits (both missing fields), got %d", emitted)
}
out := buf.String()
if strings.Count(out, "missing header fields") != 2 {
t.Errorf("expected 2 'missing header fields' logs, got: %s", out)
}
}
func TestRawHandlerNilBodyNoLog(t *testing.T) {
var buf bytes.Buffer
s := &FeishuSource{Logger: log.New(&buf, "", 0)}
emitted := 0
handler := s.buildRawHandler(func(_ *event.RawEvent) { emitted++ })
req := &larkevent.EventReq{Body: nil}
handler(context.Background(), req)
if emitted != 0 {
t.Errorf("expected 0 emits, got %d", emitted)
}
if buf.Len() > 0 {
t.Errorf("expected no log output, got: %s", buf.String())
}
}
func TestRawHandlerValidEnvelopeEmits(t *testing.T) {
s := &FeishuSource{}
var captured *event.RawEvent
handler := s.buildRawHandler(func(e *event.RawEvent) { captured = e })
body := []byte(`{"header":{"event_id":"evt-42","event_type":"im.message.receive_v1","create_time":"1700000000000"}}`)
handler(context.Background(), &larkevent.EventReq{Body: body})
if captured == nil {
t.Fatal("expected emit to fire")
}
if captured.EventID != "evt-42" {
t.Errorf("EventID: got %q, expected evt-42", captured.EventID)
}
if captured.EventType != "im.message.receive_v1" {
t.Errorf("EventType: got %q, expected im.message.receive_v1", captured.EventType)
}
if captured.SourceTime != "1700000000000" {
t.Errorf("SourceTime: got %q, expected 1700000000000", captured.SourceTime)
}
if string(captured.Payload) != string(body) {
t.Errorf("Payload should be raw bytes")
}
}
func TestRawHandlerNilLoggerDoesNotPanic(t *testing.T) {
s := &FeishuSource{Logger: nil}
handler := s.buildRawHandler(func(_ *event.RawEvent) {})
handler(context.Background(), &larkevent.EventReq{Body: []byte("bad json")})
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package source
import (
"testing"
"github.com/larksuite/cli/internal/event/protocol"
)
// "disconnected to <url>" contains "connected to ws" — must use HasPrefix to avoid misclassifying as connect.
func TestTryNotify_Classify(t *testing.T) {
cases := []struct {
name string
msg string
errDetail string
wantState string
wantDetail string
wantCalled bool
}{
{
name: "connected (SDK connect success)",
msg: "connected to wss://example.com/gw [conn_id=abc]",
wantState: protocol.SourceStateConnected,
wantCalled: true,
},
{
name: "disconnected must not be misclassified as connected",
msg: "disconnected to wss://example.com/gw [conn_id=abc]",
wantState: protocol.SourceStateDisconnected,
wantCalled: true,
},
{
name: "disconnected carries errDetail through",
msg: "disconnected to wss://example.com/gw [conn_id=abc]",
errDetail: "read tcp: broken pipe",
wantState: protocol.SourceStateDisconnected,
wantDetail: "read tcp: broken pipe",
wantCalled: true,
},
{
name: "reconnecting with attempt 1",
msg: "trying to reconnect: 1 [conn_id=abc]",
wantState: protocol.SourceStateReconnecting,
wantDetail: "attempt 1",
wantCalled: true,
},
{
name: "reconnecting with attempt 12",
msg: "trying to reconnect: 12",
wantState: protocol.SourceStateReconnecting,
wantDetail: "attempt 12",
wantCalled: true,
},
{
name: "case-insensitive connected",
msg: "CONNECTED TO WSS://example.com",
wantState: protocol.SourceStateConnected,
wantCalled: true,
},
{
name: "ignore generic connect-failed error",
msg: "connect failed, err: dial tcp: i/o timeout",
errDetail: "connect failed, err: dial tcp: i/o timeout",
wantCalled: false,
},
{
name: "ignore read-loop failure",
msg: "receive message failed, err: websocket: close 1006",
errDetail: "receive message failed, err: websocket: close 1006",
wantCalled: false,
},
{
name: "ignore heartbeat noise",
msg: "receive pong",
wantCalled: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var gotState, gotDetail string
called := false
lg := &sdkLogger{notify: func(state, detail string) {
called = true
gotState = state
gotDetail = detail
}}
lg.tryNotify(tc.msg, tc.errDetail)
if called != tc.wantCalled {
t.Fatalf("called=%v, want %v (msg=%q)", called, tc.wantCalled, tc.msg)
}
if !called {
return
}
if gotState != tc.wantState {
t.Errorf("state = %q, want %q", gotState, tc.wantState)
}
if gotDetail != tc.wantDetail {
t.Errorf("detail = %q, want %q", gotDetail, tc.wantDetail)
}
})
}
}
func TestTryNotify_NilNotifySafe(t *testing.T) {
lg := &sdkLogger{notify: nil}
lg.tryNotify("disconnected to wss://example.com", "")
lg.tryNotify("connected to wss://example.com", "")
lg.tryNotify("trying to reconnect: 1", "")
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package source
// DO NOT trim trailing spaces — the HasPrefix disambiguator depends on them.
const (
sdkLogReconnecting = "trying to reconnect"
sdkLogConnected = "connected to "
sdkLogDisconnected = "disconnected to "
)
@@ -0,0 +1,109 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package source
import (
"context"
"strings"
"sync"
"testing"
"github.com/larksuite/cli/internal/event/protocol"
)
// Samples preserve the real SDK shape ("<verb> to <url>[conn_id=...]" — no space before bracket).
func TestSDKLogPatternsMatchKnownSDKOutput(t *testing.T) {
cases := []struct {
name string
sdkLogSample string
expectedState string
}{
{
name: "reconnect with attempt number",
sdkLogSample: "trying to reconnect: 2[conn_id=abc123]",
expectedState: protocol.SourceStateReconnecting,
},
{
name: "reconnect high attempt",
sdkLogSample: "trying to reconnect: 12",
expectedState: protocol.SourceStateReconnecting,
},
{
name: "connected success with conn_id",
sdkLogSample: "connected to wss://open.feishu.cn/gateway[conn_id=abc123]",
expectedState: protocol.SourceStateConnected,
},
{
name: "connected to custom gateway",
sdkLogSample: "connected to wss://internal.example.com/gw",
expectedState: protocol.SourceStateConnected,
},
{
name: "disconnected does not alias connected",
sdkLogSample: "disconnected to wss://open.feishu.cn/gateway[conn_id=abc123]",
expectedState: protocol.SourceStateDisconnected,
},
{
name: "connected uppercase",
sdkLogSample: "CONNECTED TO WSS://OPEN.FEISHU.CN/GATEWAY",
expectedState: protocol.SourceStateConnected,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var mu sync.Mutex
var gotState string
called := false
notify := func(state, detail string) {
mu.Lock()
gotState = state
called = true
mu.Unlock()
}
logger := &sdkLogger{notify: notify}
logger.Info(context.Background(), tc.sdkLogSample)
mu.Lock()
defer mu.Unlock()
if !called {
t.Fatalf("SDK log sample %q did not trigger notify — SDK log format may have changed", tc.sdkLogSample)
}
if gotState != tc.expectedState {
t.Errorf("SDK log sample %q classified as %q, want %q", tc.sdkLogSample, gotState, tc.expectedState)
}
})
}
}
func TestSDKLogPatternsConstantsContainExpectedSubstrings(t *testing.T) {
if !strings.Contains(sdkLogReconnecting, "reconnect") {
t.Errorf("sdkLogReconnecting should contain 'reconnect', got %q", sdkLogReconnecting)
}
if !strings.Contains(sdkLogConnected, "connected") {
t.Errorf("sdkLogConnected should contain 'connected', got %q", sdkLogConnected)
}
if !strings.Contains(sdkLogDisconnected, "disconnected") {
t.Errorf("sdkLogDisconnected should contain 'disconnected', got %q", sdkLogDisconnected)
}
if sdkLogReconnecting != strings.ToLower(sdkLogReconnecting) {
t.Errorf("sdkLogReconnecting must be lowercase, got %q", sdkLogReconnecting)
}
if sdkLogConnected != strings.ToLower(sdkLogConnected) {
t.Errorf("sdkLogConnected must be lowercase, got %q", sdkLogConnected)
}
if sdkLogDisconnected != strings.ToLower(sdkLogDisconnected) {
t.Errorf("sdkLogDisconnected must be lowercase, got %q", sdkLogDisconnected)
}
if strings.HasPrefix(sdkLogDisconnected, sdkLogConnected) {
t.Errorf("sdkLogConnected %q is a prefix of sdkLogDisconnected %q — restore the trailing space.",
sdkLogConnected, sdkLogDisconnected)
}
if !strings.HasSuffix(sdkLogConnected, " ") {
t.Error("sdkLogConnected must keep its trailing space")
}
if !strings.HasSuffix(sdkLogDisconnected, " ") {
t.Error("sdkLogDisconnected must keep its trailing space")
}
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
// Package source is a pluggable event source abstraction (separate package to keep
// business registrations free of SDK transitive deps).
package source
import (
"context"
"sync"
"github.com/larksuite/cli/internal/event"
)
// StatusNotifier surfaces SourceState* lifecycle states; detail is free-form context.
type StatusNotifier func(state, detail string)
// Source produces events; emit MUST return quickly (anything slow stalls the SDK read loop).
type Source interface {
Name() string
Start(ctx context.Context, eventTypes []string, emit func(*event.RawEvent), notify StatusNotifier) error
}
var (
registry []Source
registryMu sync.Mutex
)
func Register(s Source) {
registryMu.Lock()
defer registryMu.Unlock()
registry = append(registry, s)
}
func All() []Source {
registryMu.Lock()
defer registryMu.Unlock()
out := make([]Source, len(registry))
copy(out, registry)
return out
}
func ResetForTest() {
registryMu.Lock()
defer registryMu.Unlock()
registry = nil
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package source
import (
"context"
"testing"
"time"
"github.com/larksuite/cli/internal/event"
)
type mockSource struct {
name string
events []*event.RawEvent
}
func (s *mockSource) Name() string { return s.name }
func (s *mockSource) Start(ctx context.Context, _ []string, emit func(*event.RawEvent), _ StatusNotifier) error {
for _, e := range s.events {
emit(e)
}
<-ctx.Done()
return nil
}
func TestRegister(t *testing.T) {
ResetForTest()
src := &mockSource{name: "test-source"}
Register(src)
sources := All()
if len(sources) != 1 || sources[0].Name() != "test-source" {
t.Errorf("unexpected sources: %v", sources)
}
}
func TestMockSource_EmitsEvents(t *testing.T) {
src := &mockSource{
name: "test",
events: []*event.RawEvent{
{EventID: "1", EventType: "im.message.receive_v1"},
{EventID: "2", EventType: "im.message.receive_v1"},
},
}
received := make(chan *event.RawEvent, 10)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
go src.Start(ctx, nil, func(e *event.RawEvent) {
received <- e
}, nil)
time.Sleep(50 * time.Millisecond)
if len(received) != 2 {
t.Errorf("expected 2 events, got %d", len(received))
}
}