mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
e6a6dbe3f6
* feat(channel): add qq adapter and outbound delivery * feat(channel): ingest inbound qq messages * feat(web): expose qq channel in management ui * feat(channel): support qq attachment ingestion * fix(mcp): fail read raw immediately for missing files * fix(agent): parse inline image data into native image parts * test(agent): align read_media tool tests with SDK options * fix(channel): harden qq image delivery and reconnect loop Avoid data URLs for qq channel images, reset reconnect backoff after healthy sessions, and fall back gracefully for malformed public image URLs. * fix(channel): restore qq media delivery and target resolution * fix(qq,mcp,agent): fix message/qq regressions and pass go lint * fix(qq,agent): validate inline base64 and sync heartbeat seq * fix(qq): validate remote voice mime for upload checks * fix(qq): fall back intents and restore adapter wiring * fix(qq): prevent final text leakage and dedupe persisted inbound query
172 lines
4.7 KiB
Go
172 lines
4.7 KiB
Go
package qq
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/memohai/memoh/internal/channel"
|
|
)
|
|
|
|
type Config struct {
|
|
AppID string
|
|
AppSecret string
|
|
MarkdownSupport bool
|
|
EnableInputHint bool
|
|
}
|
|
|
|
type UserConfig struct {
|
|
TargetType string
|
|
TargetID string
|
|
}
|
|
|
|
func normalizeConfig(raw map[string]any) (map[string]any, error) {
|
|
cfg, err := parseConfig(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{
|
|
"appId": cfg.AppID,
|
|
"clientSecret": cfg.AppSecret,
|
|
"markdownSupport": cfg.MarkdownSupport,
|
|
"enableInputHint": cfg.EnableInputHint,
|
|
}, nil
|
|
}
|
|
|
|
func normalizeUserConfig(raw map[string]any) (map[string]any, error) {
|
|
cfg, err := parseUserConfig(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{
|
|
"target_type": cfg.TargetType,
|
|
"target_id": cfg.TargetID,
|
|
}, nil
|
|
}
|
|
|
|
func resolveTarget(raw map[string]any) (string, error) {
|
|
cfg, err := parseUserConfig(raw)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return cfg.TargetType + ":" + cfg.TargetID, nil
|
|
}
|
|
|
|
func matchBinding(raw map[string]any, criteria channel.BindingCriteria) bool {
|
|
cfg, err := parseUserConfig(raw)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
subjectID := strings.TrimSpace(criteria.SubjectID)
|
|
if cfg.TargetType == "c2c" && subjectID != "" && subjectID == cfg.TargetID {
|
|
return true
|
|
}
|
|
if cfg.TargetType == "c2c" && strings.TrimSpace(criteria.Attribute("user_openid")) == cfg.TargetID {
|
|
return true
|
|
}
|
|
if cfg.TargetType == "group" && strings.TrimSpace(criteria.Attribute("group_openid")) == cfg.TargetID {
|
|
return true
|
|
}
|
|
if cfg.TargetType == "channel" && strings.TrimSpace(criteria.Attribute("channel_id")) == cfg.TargetID {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func buildUserConfig(identity channel.Identity) map[string]any {
|
|
targetID := strings.TrimSpace(identity.Attribute("user_openid"))
|
|
if targetID == "" {
|
|
targetID = strings.TrimSpace(identity.SubjectID)
|
|
}
|
|
if targetID == "" {
|
|
return map[string]any{}
|
|
}
|
|
return map[string]any{
|
|
"target_type": "c2c",
|
|
"target_id": targetID,
|
|
}
|
|
}
|
|
|
|
func parseConfig(raw map[string]any) (Config, error) {
|
|
appID := strings.TrimSpace(channel.ReadString(raw, "appId", "app_id"))
|
|
clientSecret := strings.TrimSpace(channel.ReadString(raw, "clientSecret", "client_secret"))
|
|
if appID == "" {
|
|
return Config{}, errors.New("qq appId is required")
|
|
}
|
|
if clientSecret == "" {
|
|
return Config{}, errors.New("qq clientSecret is required")
|
|
}
|
|
return Config{
|
|
AppID: appID,
|
|
AppSecret: clientSecret,
|
|
MarkdownSupport: readBool(raw, true, "markdownSupport", "markdown_support"),
|
|
EnableInputHint: readBool(raw, true, "enableInputHint", "enable_input_hint"),
|
|
}, nil
|
|
}
|
|
|
|
func parseUserConfig(raw map[string]any) (UserConfig, error) {
|
|
targetType := strings.ToLower(strings.TrimSpace(channel.ReadString(raw, "targetType", "target_type")))
|
|
targetID := strings.TrimSpace(channel.ReadString(raw, "targetId", "target_id"))
|
|
if targetType == "" || targetID == "" {
|
|
switch {
|
|
case strings.TrimSpace(channel.ReadString(raw, "userOpenid", "user_openid")) != "":
|
|
targetType = "c2c"
|
|
targetID = strings.TrimSpace(channel.ReadString(raw, "userOpenid", "user_openid"))
|
|
case strings.TrimSpace(channel.ReadString(raw, "groupOpenid", "group_openid")) != "":
|
|
targetType = "group"
|
|
targetID = strings.TrimSpace(channel.ReadString(raw, "groupOpenid", "group_openid"))
|
|
case strings.TrimSpace(channel.ReadString(raw, "channelId", "channel_id")) != "":
|
|
targetType = "channel"
|
|
targetID = strings.TrimSpace(channel.ReadString(raw, "channelId", "channel_id"))
|
|
}
|
|
}
|
|
if targetType == "" || targetID == "" {
|
|
return UserConfig{}, errors.New("qq user config requires target_type and target_id")
|
|
}
|
|
switch targetType {
|
|
case "c2c", "group", "channel":
|
|
default:
|
|
return UserConfig{}, errors.New("qq target_type must be c2c, group, or channel")
|
|
}
|
|
return UserConfig{TargetType: targetType, TargetID: targetID}, nil
|
|
}
|
|
|
|
func normalizeTarget(raw string) string {
|
|
value := strings.TrimSpace(raw)
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
for _, prefix := range []string{"qq:", "qqbot:"} {
|
|
if strings.HasPrefix(strings.ToLower(value), prefix) {
|
|
value = strings.TrimSpace(value[len(prefix):])
|
|
break
|
|
}
|
|
}
|
|
for _, targetType := range []string{"c2c:", "group:", "channel:"} {
|
|
if strings.HasPrefix(strings.ToLower(value), targetType) {
|
|
return strings.ToLower(targetType[:len(targetType)-1]) + ":" + strings.TrimSpace(value[len(targetType):])
|
|
}
|
|
}
|
|
return "c2c:" + value
|
|
}
|
|
|
|
func readBool(raw map[string]any, fallback bool, keys ...string) bool {
|
|
for _, key := range keys {
|
|
value, ok := raw[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch v := value.(type) {
|
|
case bool:
|
|
return v
|
|
case string:
|
|
switch strings.ToLower(strings.TrimSpace(v)) {
|
|
case "true", "1", "yes", "on":
|
|
return true
|
|
case "false", "0", "no", "off":
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return fallback
|
|
}
|