Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions internal/channel/feishu/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ import (
)

const (
// The persisted Manager participant uses a typed ID (for example,
// "pt-manager"), so resolve its app through the canonical Agent ID instead
// of assuming a participant named "manager".
feishuManagerAgentID = "agent-manager"

// feishuManagerBotID is retained for services constructed with an in-memory
// app map, whose historical Manager key is "manager".
feishuManagerBotID = "manager"
)

Expand Down Expand Up @@ -531,6 +538,7 @@ func (s *Service) CreateRoom(req im.CreateRoomRequest) (im.Room, error) {
if err != nil {
return im.Room{}, err
}
memberBotIDs, memberAppIDs = excludeCallingBot(memberBotIDs, memberAppIDs, app.AppID)
description := strings.TrimSpace(req.Description)

created, err := s.createChat(context.Background(), app, CreateChatRequest{
Expand Down Expand Up @@ -1943,6 +1951,12 @@ func (s *Service) participantMentionOpenIDLocked(participantID string) (string,
}

func (s *Service) managerAppConfigLocked() (AppConfig, error) {
if s.configProvider != nil {
participantID, app, ok := s.configProvider.BotConfigForAgent(feishuManagerAgentID)
if ok {
return validateAppConfig(app, participantID)
}
}
app, ok := s.appConfigByIDLocked(feishuManagerBotID)
if !ok {
return AppConfig{}, fmt.Errorf("feishu app is not configured for %q", feishuManagerBotID)
Expand Down Expand Up @@ -2031,6 +2045,29 @@ func (s *Service) appIDsForMembers(memberIDs []string) ([]string, error) {
return appIDs, nil
}

// excludeCallingBot keeps the CSGClaw membership record intact while avoiding
// a duplicate Feishu invitation for the bot app that created the chat. Feishu
// adds that app's bot to the chat automatically.
func excludeCallingBot(memberBotIDs, memberAppIDs []string, callingAppID string) ([]string, []string) {
callingAppID = strings.TrimSpace(callingAppID)
if callingAppID == "" || len(memberAppIDs) == 0 {
return memberBotIDs, memberAppIDs
}

filteredBotIDs := make([]string, 0, len(memberBotIDs))
filteredAppIDs := make([]string, 0, len(memberAppIDs))
for index, appID := range memberAppIDs {
if strings.EqualFold(strings.TrimSpace(appID), callingAppID) {
continue
}
filteredAppIDs = append(filteredAppIDs, appID)
if index < len(memberBotIDs) {
filteredBotIDs = append(filteredBotIDs, memberBotIDs[index])
}
}
return filteredBotIDs, filteredAppIDs
}

func normalizeNonEmptyStrings(values []string) []string {
normalized := make([]string, 0, len(values))
for _, value := range values {
Expand Down
56 changes: 53 additions & 3 deletions internal/channel/feishu/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func testBotInfoResolver(t *testing.T, openIDsByAppID map[string]string) func(co

type testFeishuConfigProvider struct {
bots map[string]AppConfig
agentBots map[string]string
mentionOpenIDs map[string]string
adminOpenID string
}
Expand All @@ -86,8 +87,10 @@ func (p testFeishuConfigProvider) BotConfig(participantID string) (AppConfig, bo
return app, ok
}

func (p testFeishuConfigProvider) BotConfigForAgent(string) (string, AppConfig, bool) {
return "", AppConfig{}, false
func (p testFeishuConfigProvider) BotConfigForAgent(agentID string) (string, AppConfig, bool) {
participantID := strings.TrimSpace(p.agentBots[strings.TrimSpace(agentID)])
app, ok := p.bots[participantID]
return participantID, app, participantID != "" && ok
}

func (p testFeishuConfigProvider) DefaultAdminOpenID() (string, bool) {
Expand Down Expand Up @@ -314,7 +317,8 @@ func TestFeishuCreateRoomUsesConfiguredAdminOpenID(t *testing.T) {
},
)

if _, err := svc.CreateRoom(im.CreateRoomRequest{Title: "alpha", CreatorID: "u-manager", MemberIDs: []string{"u-dev"}}); err != nil {
room, err := svc.CreateRoom(im.CreateRoomRequest{Title: "alpha", CreatorID: "admin", MemberIDs: []string{"manager", "u-dev"}})
if err != nil {
t.Fatalf("CreateRoom() error = %v", err)
}

Expand All @@ -333,6 +337,52 @@ func TestFeishuCreateRoomUsesConfiguredAdminOpenID(t *testing.T) {
if len(gotAddReq.MemberAppIDs) != 1 || gotAddReq.MemberAppIDs[0] != "cli_dev" {
t.Fatalf("add members app_ids = %+v, want [cli_dev]", gotAddReq.MemberAppIDs)
}
if got, want := strings.Join(room.Members, ","), "admin,manager,u-dev"; got != want {
t.Fatalf("room members = %+v, want manager recorded without invitation", room.Members)
}
}

func TestFeishuCreateRoomResolvesManagerAppByAgentID(t *testing.T) {
var gotApp AppConfig
var gotAddReq AddChatMembersRequest
svc := NewServiceWithProvider(testFeishuConfigProvider{
bots: map[string]AppConfig{
"pt-manager": {AppID: "cli_manager", AppSecret: "manager-secret"},
"pt-worker": {AppID: "cli_worker", AppSecret: "worker-secret"},
},
agentBots: map[string]string{
feishuManagerAgentID: "pt-manager",
},
adminOpenID: "ou_admin",
})
svc.createChat = func(_ context.Context, app AppConfig, req CreateChatRequest) (CreateChatResponse, error) {
gotApp = app
if got, want := req.CreatorID, "ou_admin"; got != want {
t.Fatalf("create chat creator_id = %q, want %q", got, want)
}
return CreateChatResponse{ChatID: "oc_agent_manager", Name: req.Title}, nil
}
svc.addChatMembers = func(_ context.Context, _ AppConfig, req AddChatMembersRequest) error {
gotAddReq = req
return nil
}

if _, err := svc.CreateRoom(im.CreateRoomRequest{
Title: "alpha",
CreatorID: "user-manager",
MemberIDs: []string{"pt-manager", "pt-worker"},
}); err != nil {
t.Fatalf("CreateRoom() error = %v", err)
}
if got, want := gotApp.AppID, "cli_manager"; got != want {
t.Fatalf("create chat app_id = %q, want %q", got, want)
}
if got, want := strings.Join(gotAddReq.MemberBotIDs, ","), "pt-worker"; got != want {
t.Fatalf("add member bot ids = %+v, want %q", gotAddReq.MemberBotIDs, want)
}
if got, want := strings.Join(gotAddReq.MemberAppIDs, ","), "cli_worker"; got != want {
t.Fatalf("add member app ids = %+v, want %q", gotAddReq.MemberAppIDs, want)
}
}

func TestFeishuCreateRoomRequiresConfiguredMemberBots(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ If the user wants to create, add, set up, or provision an agent, robot, bot, or
This includes capability-specific workers such as GitLab, frontend, backend, QA, review, or Feishu-connected workers.
Never run `participant create --type agent` for a new CSGClaw worker unless it binds a real Agent with `--bind create` or `--bind reuse`.
Never run `participant create --bind create` without `--from-template` for a new worker.
For a request that only connects an already-named worker to Feishu, do not infer that the Agent is missing from `participant list`; read `skills/feishu/SKILL.md` first so its helper can resolve the global Agent registry by runtime ID or display name.

### Single-worker task assignment second

Expand Down
Loading
Loading