* dws event * fix consume fail * test: add stream ticket injection probe * feat: add portal ticket stream mode * user event * fix: allow portal ticket normal without app secret * event * user event * eventType filter * refactor(event): 优化IPC端点路径处理和改进相关测试 - 用dwsevent.IPCEndpoint替代原先根据GOOS判断的路径逻辑 - 新增event包实现Unix socket路径长度限制及长路径fallback机制 - 添加endpoint_test.go覆盖路径短长及唯一性的单元测试 - 修改busctl模块使用统一的IPC端点获取方法,避免重复实现 - transport_unix.go新增checkSocketPath函数检查路径长度,防止EINVAL错误 - 在监听和连接Unix socket时加入路径限制检查,提升错误明晰度 - 去除多个文件中无用的runtime导入,简化代码依赖 * opt * event skill * default value * install script event * fix: remove subscribe id event fanout filter * fix(personal): 修正指定发送人消息描述错误 * more im event * filter subId * fix: align personal event schema with stream payload * fix: avoid duplicate app helper name * feat: simplify personal event schemas * feat: simplify event schema output * docs: refine dingtalk event skill references * feat: align personal event consume flags * fix event stop and status visibility * hide app event public entrypoints * hide incomplete personal sender event * remove external event reference comments * chore(event): prepare official release * fix(event): harden personal stream lifecycle --------- Co-authored-by: 玉澜 <yulan.wqy@alibaba-inc.com> Co-authored-by: zhengyubai <zhengyubai618@gmail.com>
244 lines
8.5 KiB
Go
244 lines
8.5 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package registry
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/transport"
|
|
)
|
|
|
|
// CompactView is the JSON-encoded shape `dws event consume --compact`
|
|
// writes per event. Implementations populate the common header fields
|
|
// then layer event-type-specific semantic fields on top via "extra".
|
|
type CompactView struct {
|
|
// Type echoes EventType for routing/filtering at the agent layer.
|
|
Type string `json:"type"`
|
|
// EventID is the SDK-assigned identifier (DataFrameHeader "eventId").
|
|
EventID string `json:"event_id,omitempty"`
|
|
// Timestamp is the event-born-time milliseconds value, surfaced under a
|
|
// shorter name agents typically expect.
|
|
Timestamp int64 `json:"timestamp,omitempty"`
|
|
// CorpID propagates the SDK's eventCorpId when present.
|
|
CorpID string `json:"corp_id,omitempty"`
|
|
// AppID propagates eventUnifiedAppId when present.
|
|
AppID string `json:"app_id,omitempty"`
|
|
// Extra holds the event-type-specific flattened fields (e.g.
|
|
// chat_id/sender_id for IM messages). Marshalled inline.
|
|
Extra map[string]any `json:"-"`
|
|
}
|
|
|
|
// MarshalJSON serialises CompactView with the Extra fields lifted to the
|
|
// top level, dropping the wrapping "extra" key. This produces the flat
|
|
// map output that agents prefer.
|
|
func (v CompactView) MarshalJSON() ([]byte, error) {
|
|
out := make(map[string]any, 6+len(v.Extra))
|
|
out["type"] = v.Type
|
|
if v.EventID != "" {
|
|
out["event_id"] = v.EventID
|
|
}
|
|
if v.Timestamp != 0 {
|
|
out["timestamp"] = v.Timestamp
|
|
}
|
|
if v.CorpID != "" {
|
|
out["corp_id"] = v.CorpID
|
|
}
|
|
if v.AppID != "" {
|
|
out["app_id"] = v.AppID
|
|
}
|
|
for k, val := range v.Extra {
|
|
// Header fields win over Extra on collision — we never let a
|
|
// processor accidentally overwrite the documented top-level shape.
|
|
if _, taken := out[k]; taken {
|
|
continue
|
|
}
|
|
out[k] = val
|
|
}
|
|
return json.Marshal(out)
|
|
}
|
|
|
|
// Processor transforms one Event frame into a CompactView. Implementations
|
|
// MUST NOT modify the input. Returning a zero CompactView is allowed and
|
|
// falls back to the generic processor at the caller's discretion.
|
|
type Processor func(ev transport.Event) CompactView
|
|
|
|
// processors holds the registered specialised processors. Lookup is by
|
|
// exact EventType match; wildcard support is intentionally absent because
|
|
// compact rendering is identity-specific (per event_type schema).
|
|
//
|
|
// To register more event types, add entries here. Each processor receives
|
|
// the full transport.Event and returns a CompactView with the per-type
|
|
// semantic fields lifted into Extra. See GenericProcessor for the
|
|
// fall-through behaviour applied to unregistered types.
|
|
var processors = map[string]Processor{
|
|
// IM
|
|
"im.message.receive_v1": compactIMMessage,
|
|
"im.message.read_v1": compactIMMessageRead,
|
|
// Approval
|
|
"approval.instance.status_changed": compactApprovalInstance,
|
|
"approval.task.created": compactApprovalTask,
|
|
// Contact
|
|
"contact.user.created_v3": compactContactUser,
|
|
"contact.user.updated_v3": compactContactUser,
|
|
"contact.user.deleted_v3": compactContactUser,
|
|
// Calendar
|
|
"cal.event.created_v1": compactCalendarEvent,
|
|
"cal.event.updated_v1": compactCalendarEvent,
|
|
"cal.event.deleted_v1": compactCalendarEvent,
|
|
// Attendance
|
|
"attendance.check_v1": compactAttendanceCheck,
|
|
}
|
|
|
|
// LookupProcessor returns the registered processor for eventType, or
|
|
// GenericProcessor when no specialised one exists.
|
|
func LookupProcessor(eventType string) Processor {
|
|
if p, ok := processors[eventType]; ok {
|
|
return p
|
|
}
|
|
return GenericProcessor
|
|
}
|
|
|
|
// GenericProcessor is the fallback used when no specialised processor is
|
|
// registered for the event type. It surfaces the 5 SDK header fields and
|
|
// tries to parse the JSON payload — on parse failure it embeds the raw
|
|
// payload string under "data" so nothing is lost.
|
|
func GenericProcessor(ev transport.Event) CompactView {
|
|
v := CompactView{
|
|
Type: ev.EventType,
|
|
EventID: ev.EventID,
|
|
Timestamp: ev.EventBornTime,
|
|
CorpID: ev.EventCorpID,
|
|
AppID: ev.EventUnifiedAppID,
|
|
Extra: map[string]any{},
|
|
}
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal([]byte(ev.Data), &parsed); err == nil {
|
|
for k, val := range parsed {
|
|
v.Extra[k] = val
|
|
}
|
|
} else if ev.Data != "" {
|
|
v.Extra["data"] = ev.Data
|
|
}
|
|
return v
|
|
}
|
|
|
|
// liftFromNested copies named keys from a nested map at top level on the
|
|
// CompactView. No-op when nested is missing. Used by the per-type
|
|
// processors to lift "sender.sender_id.open_id" style nested fields.
|
|
func liftFromNested(v *CompactView, nestedKey string, keys ...string) {
|
|
nested, ok := v.Extra[nestedKey].(map[string]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
for _, k := range keys {
|
|
if val, ok := nested[k]; ok {
|
|
if _, taken := v.Extra[k]; !taken {
|
|
v.Extra[k] = val
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// compactIMMessage is the specialised processor for IM message receipts
|
|
// (event_type: im.message.receive_v1).
|
|
//
|
|
// The SDK payload for IM message events is a nested JSON document. The
|
|
// canonical agent fields we expose are: message_id, chat_id, chat_type,
|
|
// message_type, content (already a string for text messages), sender_id.
|
|
// Any field DingTalk adds later flows through via the generic merge —
|
|
// we never drop unknown fields, we just promote the common ones.
|
|
func compactIMMessage(ev transport.Event) CompactView {
|
|
v := GenericProcessor(ev)
|
|
liftFromNested(&v, "message", "message_id", "chat_id", "chat_type", "message_type", "content")
|
|
// Sender open_id may be doubly nested: sender.sender_id.open_id.
|
|
if sender, ok := v.Extra["sender"].(map[string]any); ok {
|
|
if sid, ok := sender["sender_id"].(map[string]any); ok {
|
|
if open, ok := sid["open_id"]; ok {
|
|
if _, taken := v.Extra["sender_id"]; !taken {
|
|
v.Extra["sender_id"] = open
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// compactIMMessageRead lifts message read receipts (open_id of the reader
|
|
// + the message_ids they acknowledged).
|
|
func compactIMMessageRead(ev transport.Event) CompactView {
|
|
v := GenericProcessor(ev)
|
|
liftFromNested(&v, "reader", "open_id", "read_time")
|
|
liftFromNested(&v, "context", "open_message_ids")
|
|
return v
|
|
}
|
|
|
|
// compactApprovalInstance promotes the approval-instance-level identity
|
|
// fields (instance_id / status / business_id / process_code) and the
|
|
// requestor name.
|
|
func compactApprovalInstance(ev transport.Event) CompactView {
|
|
v := GenericProcessor(ev)
|
|
liftFromNested(&v, "instance", "instance_id", "process_code", "business_id", "title")
|
|
liftFromNested(&v, "status_change", "from_status", "to_status", "operate_time")
|
|
if op, ok := v.Extra["operator"].(map[string]any); ok {
|
|
if id, ok := op["userid"]; ok {
|
|
v.Extra["operator_userid"] = id
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// compactApprovalTask lifts task assignment info (task_id, assignee user_id).
|
|
func compactApprovalTask(ev transport.Event) CompactView {
|
|
v := GenericProcessor(ev)
|
|
liftFromNested(&v, "task", "task_id", "instance_id", "status", "action_type")
|
|
if assignee, ok := v.Extra["assignee"].(map[string]any); ok {
|
|
if id, ok := assignee["userid"]; ok {
|
|
v.Extra["assignee_userid"] = id
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// compactContactUser lifts the user identity for user lifecycle events.
|
|
func compactContactUser(ev transport.Event) CompactView {
|
|
v := GenericProcessor(ev)
|
|
liftFromNested(&v, "user", "open_id", "userid", "name", "active")
|
|
if v.Extra["open_id"] == nil {
|
|
// older payload may put open_id at top level under "open_ids" array
|
|
if arr, ok := v.Extra["open_ids"].([]any); ok && len(arr) > 0 {
|
|
v.Extra["open_id"] = arr[0]
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// compactCalendarEvent lifts calendar event identity + organiser.
|
|
func compactCalendarEvent(ev transport.Event) CompactView {
|
|
v := GenericProcessor(ev)
|
|
liftFromNested(&v, "event", "event_id", "calendar_id", "title", "start_time", "end_time", "location")
|
|
if org, ok := v.Extra["organizer"].(map[string]any); ok {
|
|
if id, ok := org["userid"]; ok {
|
|
v.Extra["organizer_userid"] = id
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// compactAttendanceCheck lifts the punch info (userid, check_time, location).
|
|
func compactAttendanceCheck(ev transport.Event) CompactView {
|
|
v := GenericProcessor(ev)
|
|
liftFromNested(&v, "punch", "userid", "check_time", "check_type", "location_method")
|
|
return v
|
|
}
|