Larksuite (Feishu)
Feishu (国内版) 와 Lark Suite (海外版) 는 동일한 OpenAPI 표면을 공유합니다. Ongrid provider 는 둘을 단일 통합으로 취급합니다; 테넌트에 맞는 base URL 을 선택하세요.
| Mode | 동작 |
|---|---|
| 알림 | 커스텀 봇 webhook 으로 Feishu/Lark 그룹에 알림 push. |
| IM 브릿지 | WebSocket long-connection 을 사용한 양방향 에이전트 채팅. |
알림 모드 (커스텀 봇)
Feishu sender 는 봇 관리자가 제공한 webhook URL 에 Feishu / Lark 커스텀 봇 payload 를 게시합니다. payload 형태:
{
"msg_type": "text",
"content": {"text": "[CRITICAL] swap_high node-01\nswap_in_pages > 1000 for 5m\nsource: alert\ndedupe: alert:swap_high:device=7"},
"timestamp": "1717012345",
"sign": "<base64-hmac>"
}서명 — sign 필드
Feishu 커스텀 봇에서 签名校验 (signature verification) 를 켜면 Feishu 가 공유 시크릿을 줍니다. sender 는 sign 필드를 다음과 같이 계산합니다:
stringToSign = timestamp + "\n" + secret
sign = base64(HMAC-SHA256(key=stringToSign, message=<empty>))예 — 시크릿이 HMAC 의 키 자료 와 string-to-sign 의 일부 역할을 둘 다 합니다. 이는 Feishu 가 문서화한 알고리즘이며 signFeishu 가 구현하는 것입니다.
Secret 필드를 비우면 Ongrid 는 sign / timestamp 없이 POST 합니다 — 봇에 서명 검증이 꺼져 있을 (또는 IP 화이트리스트가 대신 있는) 때만 사용 가능.
설정
- Feishu 그룹에서 → 设置 → 群机器人 → 添加机器人 → 自定义机器人. 이름과 아바타 부여.
- 签名校验 체크; 시크릿 복사.
- webhook URL 복사 — 다음과 같이 생김:
https://open.feishu.cn/open-apis/bot/v2/hook/<uuid>(Lark Suite 는…/open.larksuite.com/…). - Ongrid 에서: Settings → Channels → New → Provider =
feishu→ Endpoint = webhook URL → Secret = 서명 시크릿.
커스텀 봇 ≠ 앱
알림 모드는 커스텀 봇 (그룹 범위, webhook 전용) 을 사용합니다. IM 브릿지 모드는 Feishu 앱 (테넌트 범위, OAuth + 이벤트) 을 사용합니다. 이들은 다른 개념입니다; 자격 증명이 겹치지 않습니다.
IM 브릿지 모드 (long-connection 스트림)
양방향 브릿지는 공식 github.com/larksuite/oapi-sdk-go/v3/ws 클라이언트가 전달하는 Feishu long-connection 을 사용합니다. 매니저가 Feishu 의 이벤트 엔드포인트로 다이얼하면 Feishu 가 WebSocket 으로 이벤트를 push 합니다 — 공개 webhook URL 불필요.
왜 스트림이고 webhook 이 아닌가
webhook 모드 (mode=webhook) 는 하위 호환을 위해 스키마에서 지원되지만 long-connection 스트림이 권장 경로입니다.
- 매니저에 공개 ingress 불필요.
- supervisor 의 reconnect-with-backoff 재사용 (SDK 자체 reconnect 가 있지만 supervisor 는 터미널 실패용 외부 루프 추가).
- SDK 가 내부적으로 서명 검증 + AES 복호화를 처리하므로 스트림 변종에
encrypt_key를 채울 필요 없음 — webhook 전용입니다.
자격 증명 매핑
im_apps column | Feishu 의미 |
|---|---|
provider | "feishu" |
mode | "stream" (권장) 또는 "webhook" |
app_id | Feishu app_id (cli_…). |
app_secret | Feishu app_secret. |
verify_token | 선택. webhook 모드 서명 검증에 사용. |
encrypt_key | webhook 모드에서 필수, 스트림 모드에서 무시. |
allow_from | 선택. Feishu 는 테넌트 게이트라서 화이트리스트는 비필수. |
default_locale
운영자가 중국어를 쓰는 Feishu 테넌트는 zh, 영어 Lark 팀은 en 으로 설정. 비어 있음 (기본) 은 LLM 이 사용자를 미러링.
설정
- 开发者后台 → 创建企业自建应用.
app_id+app_secret획득. - 应用功能 → 机器人 → 활성화. 테스트 채팅에 봇 추가.
- 权限管理 → 부여:
im:message— 봇에 주소된 메시지 읽기.im:message.group_at_msg— 그룹 멘션 이벤트.im:message.p2p_msg— DM 이벤트.im:message:send_as_bot—SendText/EditText.
- 事件订阅 → 长连接模式 (long-connection) → 활성화.
- Ongrid 에서: Settings → IM bridge → New → Provider =
feishu→ Mode =stream→app_id+app_secret붙여넣기. 저장 후 Enable. - 채팅에서 봇을
@. 에이전트가 픽업.
tenant_access_token 캐싱
아웃바운드 호출 (SendText, EditText) 은 만료 200 초 이내일 때 적극 갱신되는 tenant_access_token 으로 인증합니다. 토큰은 클라이언트 인스턴스별로 캐시되며 sync.Mutex 로 보호됩니다; 자격 증명 회전은 클라이언트 재구축을 의미합니다. tenantAccessToken 참고.
편집에도 msg_type 이 필수
Feishu 의 PUT /open-apis/im/v1/messages/<id> 는 body 에 msg_type 이 필요합니다 — 누락 시 code: 99992402 (field validation failed) 를 반환합니다 (문서 "edit message" 페이지는 이를 명확히 하지 않음). provider 는 항상 msg_type: text 를 보냅니다.
Webhook 모드 (레거시, 호환성을 위해 유지)
webhook 모드는 여전히 UI 에서 선택 가능. supervisor 가 직접 서명을 검증하고 payload 를 복호화합니다.
서명 검증
HTTP 핸들러가 X-Lark-Signature 를 읽고 VerifyEventSignature 실행:
sig = sha256(timestamp + nonce + encrypt_key + body), hex-encoded예 — SHA-256, HMAC 아님. encrypt_key 가 해시 입력 안에서 공유 시크릿 역할. verifier 는 잘못된 입력에 절대 panic 하지 않습니다; 불일치는 ErrBadSignature 를 반환.
Payload 복호화
encrypt_key 가 설정되면 Feishu 는 이벤트 JSON 을 AES-256-CBC 로 래핑합니다. 복호화는:
- Key:
SHA-256(encrypt_key). - IV: base64 디코드된 ciphertext 의 첫 16 바이트.
- Padding: PKCS#7.
DecryptEvent 참고.
가능하면 스트림 모드 선택
webhook 모드는 공개 HTTPS 엔드포인트와 위의 encrypt_key 배선이 필요합니다. long-connection 스트림은 둘 다 회피합니다. 특별히 필요할 때만 (예: 기존 공개 webhook 라우터와 Feishu 이벤트 통합) webhook 모드에 손을 뻗으세요.
특이점
- Feishu 스트림 클라이언트는 오늘
allow_from을 강제하지 않습니다 — Feishu 플랫폼 자체가 테넌트 게이트라서 enterprise 멤버만 봇에 도달합니다. 테넌트에 게스트 / 외부 협력자가 있다면 그들의open_id값으로allow_from을 채워 대화를 더 잠그세요. - 스티커, 파일, 카드, 리치 메시지는 버려집니다 (
msg_type == "text"만 에이전트를 트리거). Telegram / Slack 과 같은 S1 계약. RootId필드는ImThread.ImThreadID로 캡처되어 스레드 내 응답이 같은 세션을 잇습니다 — 사용자가 질문마다 컨텍스트를 리셋할 필요 없음.