mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
fix(channel,attachment): channel quality refactor & attachment pipeline fixes (#349)
* feat(channel): add DingTalk channel adapter - Add DingTalk channel adapter (`internal/channel/adapters/dingtalk/`) using dingtalk-stream-sdk-go, supporting inbound message receiving and outbound text/markdown reply - Register DingTalk adapter in cmd/agent and cmd/memoh - Add go.mod dependency: github.com/memohai/dingtalk-stream-sdk-go - Add Dingtalk and Wecom SVG icons and Vue components to @memohai/icon - Refactor existing icon components to remove redundant inline wrappers - Add `channelTypeDisplayName` util for consistent channel label resolution - Add DingTalk/WeCom i18n entries (en/zh) for types and typesShort - Extend channel-icon, bot-channels, channel-settings-panel to support dingtalk/wecom - Use channelTypeDisplayName in profile page to replace ad-hoc i18n lookup * fix(channel,attachment): channel quality refactor & attachment pipeline fixes Channel module: - Fix RemoveAdapter not cleaning connectionMeta (stale status leak) - Fix preparedAttachmentTypeFromMime misclassifying image/gif - Fix sleepWithContext time.After goroutine/timer leak - Export IsDataURL/IsHTTPURL/IsDataPath, dedup across packages - Cache OutboundPolicy in managerOutboundStream to avoid repeated lookups - Split OutboundAttachmentStore: extract ContainerAttachmentIngester interface - Add ManagerOption funcs (WithInboundQueueSize, WithInboundWorkers, WithRefreshInterval) - Add thread-safety docs on OutboundStream / managerOutboundStream - Add debug logs on successful send/edit paths - Expand outbound_prepare_test.go with 21 new cases - Convert no-receiver adapter helpers to package-level funcs; drop unused params DingTalk adapter: - Implement AttachmentResolver: download inbound media via /v1.0/robot/messageFiles/download - Fix pure-image inbound messages failing due to missing resolver Attachment pipeline: - Fix images invisible to LLM in pipeline (DCP) path: inject InlineImages into last user message when cfg.Query is empty - Fix public_url fallback: skip direct URL-to-LLM when ContentHash is set, always prefer inlined persisted asset - Inject path: carry ImageParts through agent.InjectMessage; inline persisted attachments in resolver inject goroutine so mid-stream images reach the model - Fix ResolveMime for images: prefer content-sniffed MIME over platform-declared MIME (fixes Feishu sending image/png header for actual JPEG content → API 400)
This commit is contained in:
@@ -14,12 +14,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, type Component } from 'vue'
|
||||
import {
|
||||
Dingtalk,
|
||||
Qq,
|
||||
Telegram,
|
||||
Discord,
|
||||
Slack,
|
||||
Feishu,
|
||||
Wechat,
|
||||
Wecom,
|
||||
Matrix,
|
||||
} from '@memohai/icon'
|
||||
|
||||
@@ -31,7 +33,9 @@ const channelIcons: Record<string, Component> = {
|
||||
feishu: Feishu,
|
||||
wechat: Wechat,
|
||||
weixin: Wechat,
|
||||
wecom: Wecom,
|
||||
matrix: Matrix,
|
||||
dingtalk: Dingtalk,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
||||
@@ -993,6 +993,7 @@
|
||||
"feishuWebhookSecurityHint": "For security, webhook mode requires either an Encrypt Key or a Verification Token; an unprotected public callback URL should not be exposed.",
|
||||
"feishuWebhookSecretRequired": "For security, configure at least one of Encrypt Key or Verification Token.",
|
||||
"noAvailableTypes": "All platform types have been configured",
|
||||
"platformKey": "Platform ID: {key}",
|
||||
"weixinQr": {
|
||||
"title": "QR Code Login",
|
||||
"description": "Scan the QR code with WeChat to connect your account.",
|
||||
@@ -1011,6 +1012,10 @@
|
||||
"matrix": "Matrix",
|
||||
"telegram": "Telegram",
|
||||
"weixin": "WeChat",
|
||||
"wecom": "WeCom",
|
||||
"dingtalk": "DingTalk",
|
||||
"web": "Web",
|
||||
"cli": "CLI",
|
||||
"local": "Local"
|
||||
},
|
||||
"typesShort": {
|
||||
@@ -1020,6 +1025,10 @@
|
||||
"matrix": "MX",
|
||||
"telegram": "TG",
|
||||
"weixin": "WX",
|
||||
"wecom": "WC",
|
||||
"dingtalk": "DT",
|
||||
"web": "Web",
|
||||
"cli": "CLI",
|
||||
"local": "LC"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -989,6 +989,7 @@
|
||||
"feishuWebhookSecurityHint": "出于安全考虑,Webhook 模式必须配置 Encrypt Key 或 Verification Token 之一;未受保护的回调地址不应直接暴露在公网上。",
|
||||
"feishuWebhookSecretRequired": "出于安全考虑,请至少配置 Encrypt Key 或 Verification Token 之一。",
|
||||
"noAvailableTypes": "所有平台类型均已配置",
|
||||
"platformKey": "平台标识:{key}",
|
||||
"weixinQr": {
|
||||
"title": "扫码登录",
|
||||
"description": "使用微信扫描二维码以连接微信账号。",
|
||||
@@ -1007,6 +1008,10 @@
|
||||
"matrix": "Matrix",
|
||||
"telegram": "Telegram",
|
||||
"weixin": "微信",
|
||||
"wecom": "企业微信",
|
||||
"dingtalk": "钉钉",
|
||||
"web": "Web",
|
||||
"cli": "本地 CLI",
|
||||
"local": "本地"
|
||||
},
|
||||
"typesShort": {
|
||||
@@ -1016,6 +1021,10 @@
|
||||
"matrix": "MX",
|
||||
"telegram": "TG",
|
||||
"weixin": "WX",
|
||||
"wecom": "企微",
|
||||
"dingtalk": "钉",
|
||||
"web": "Web",
|
||||
"cli": "CLI",
|
||||
"local": "本地"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</span>
|
||||
<div class="flex-1 text-left">
|
||||
<div class="font-medium">
|
||||
{{ item.meta.display_name }}
|
||||
{{ channelTitle(item.meta) }}
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
<span
|
||||
@@ -107,7 +107,7 @@
|
||||
size="1em"
|
||||
/>
|
||||
</span>
|
||||
<span>{{ item.meta.display_name }}</span>
|
||||
<span>{{ channelTitle(item.meta) }}</span>
|
||||
</button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
@@ -136,6 +136,7 @@
|
||||
<script setup lang="ts">
|
||||
import { LoaderCircle, Plus } from 'lucide-vue-next'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
@@ -148,6 +149,7 @@ import { getChannels, getBotsByIdChannelByPlatform } from '@memohai/sdk'
|
||||
import type { HandlersChannelMeta, ChannelChannelConfig } from '@memohai/sdk'
|
||||
import ChannelSettingsPanel from './channel-settings-panel.vue'
|
||||
import ChannelIcon from '@/components/channel-icon/index.vue'
|
||||
import { channelTypeDisplayName } from '@/utils/channel-type-label'
|
||||
|
||||
export interface BotChannelItem {
|
||||
meta: HandlersChannelMeta
|
||||
@@ -159,6 +161,12 @@ const props = defineProps<{
|
||||
botId: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
function channelTitle(meta: HandlersChannelMeta) {
|
||||
return channelTypeDisplayName(t, meta.type, meta.display_name)
|
||||
}
|
||||
|
||||
const botIdRef = computed(() => props.botId)
|
||||
|
||||
const { data: channels, isLoading, refetch } = useQuery({
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
</span>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold">
|
||||
{{ channelItem.meta.display_name }}
|
||||
{{ channelTitle }}
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ channelItem.meta.type }}
|
||||
{{ platformKeyLine }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -273,6 +273,7 @@ import { client } from '@memohai/sdk/client'
|
||||
import ConfirmPopover from '@/components/confirm-popover/index.vue'
|
||||
import ChannelIcon from '@/components/channel-icon/index.vue'
|
||||
import WeixinQrLogin from './weixin-qr-login.vue'
|
||||
import { channelTypeDisplayName } from '@/utils/channel-type-label'
|
||||
|
||||
interface BotChannelItem {
|
||||
meta: HandlersChannelMeta
|
||||
@@ -292,6 +293,13 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n()
|
||||
const botIdRef = computed(() => props.botId)
|
||||
const platformType = computed(() => String(props.channelItem.meta.type || '').trim())
|
||||
|
||||
const channelTitle = computed(() =>
|
||||
channelTypeDisplayName(t, props.channelItem.meta.type, props.channelItem.meta.display_name),
|
||||
)
|
||||
const platformKeyLine = computed(() =>
|
||||
t('bots.channels.platformKey', { key: platformType.value }),
|
||||
)
|
||||
const queryCache = useQueryCache()
|
||||
const { mutateAsync: upsertChannel, isLoading } = useMutation({
|
||||
mutation: async ({ platform, data }: { platform: string; data: ChannelUpsertConfigRequest }) => {
|
||||
|
||||
@@ -228,6 +228,7 @@ import { resolveApiErrorMessage } from '@/utils/api-error'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { useAvatarInitials } from '@/composables/useAvatarInitials'
|
||||
import { channelTypeDisplayName } from '@/utils/channel-type-label'
|
||||
|
||||
interface IssueBindCodeResponse {
|
||||
token: string
|
||||
@@ -287,10 +288,7 @@ const avatarFallback = useAvatarInitials(() => displayTitle.value, 'U')
|
||||
|
||||
function platformLabel(platformKey: string): string {
|
||||
if (!platformKey?.trim()) return platformKey ?? ''
|
||||
const key = platformKey.trim().toLowerCase()
|
||||
const i18nKey = `bots.channels.types.${key}`
|
||||
const out = t(i18nKey)
|
||||
return out !== i18nKey ? out : platformKey
|
||||
return channelTypeDisplayName(t, platformKey, null) || platformKey
|
||||
}
|
||||
|
||||
const platformOptions = computed(() => {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Localized channel platform title for UI.
|
||||
* Prefer bots.channels.types.{type}; fall back to server display_name, then raw type.
|
||||
*/
|
||||
export function channelTypeDisplayName(
|
||||
t: (key: string, ...args: unknown[]) => string,
|
||||
channelType: string | undefined | null,
|
||||
serverDisplayName?: string | null,
|
||||
): string {
|
||||
const raw = (channelType ?? '').trim().toLowerCase()
|
||||
if (!raw) {
|
||||
return (serverDisplayName ?? '').trim() || ''
|
||||
}
|
||||
const i18nKey = `bots.channels.types.${raw}`
|
||||
const out = t(i18nKey)
|
||||
if (out !== i18nKey) return out
|
||||
const fb = (serverDisplayName ?? '').trim()
|
||||
if (fb) return fb
|
||||
return raw.charAt(0).toUpperCase() + raw.slice(1)
|
||||
}
|
||||
Reference in New Issue
Block a user