feat(channels): add Shangwutong connector

This commit is contained in:
2026-08-03 10:13:40 +08:00
parent 96f5c796a6
commit 897b4e018f
126 changed files with 20207 additions and 272 deletions
@@ -4,6 +4,7 @@ import Facebook from './channels/Facebook.vue';
import Website from './channels/Website.vue';
import Twitter from './channels/Twitter.vue';
import Api from './channels/Api.vue';
import Shangwutong from './channels/Shangwutong.vue';
import Email from './channels/Email.vue';
import Sms from './channels/Sms.vue';
import Whatsapp from './channels/Whatsapp.vue';
@@ -18,6 +19,7 @@ const channelViewList = {
website: Website,
twitter: Twitter,
api: Api,
shangwutong: Shangwutong,
email: Email,
sms: Sms,
whatsapp: Whatsapp,
@@ -55,6 +55,14 @@ const channelList = computed(() => {
description: t('INBOX_MGMT.ADD.AUTH.CHANNEL.API.DESCRIPTION'),
icon: 'i-woot-api',
},
{
key: 'shangwutong',
title: t('INBOX_MGMT.ADD.AUTH.CHANNEL.SHANGWUTONG.TITLE'),
description: t(
'INBOX_MGMT.ADD.AUTH.CHANNEL.SHANGWUTONG.DESCRIPTION'
),
icon: 'i-ri-customer-service-2-line',
},
{
key: 'telegram',
title: t('INBOX_MGMT.ADD.AUTH.CHANNEL.TELEGRAM.TITLE'),
@@ -199,6 +199,7 @@ export default {
this.isATwilioChannel ||
this.isALineChannel ||
this.isAPIInbox ||
this.isShangwutongInbox ||
(this.isAnEmailChannel && !this.inbox.provider) ||
this.shouldShowWhatsAppConfiguration ||
this.isAWebWidgetInbox
@@ -292,6 +293,7 @@ export default {
this.isAWhatsAppChannel ||
this.isAFacebookInbox ||
this.isAPIInbox ||
this.isShangwutongInbox ||
this.isAnInstagramChannel ||
this.isALineChannel ||
this.isATiktokChannel ||
@@ -591,6 +593,9 @@ export default {
this.isInboundEmailEnabled && this.continuityViaEmail,
},
};
if (this.isShangwutongInbox) {
payload.channel = {};
}
if (this.avatarFile) {
payload.avatar = this.avatarFile;
}
@@ -0,0 +1,176 @@
<script>
import { mapGetters } from 'vuex';
import { useVuelidate } from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import { useAlert } from 'dashboard/composables';
import router from '../../../../index';
import PageHeader from '../../SettingsSubPageHeader.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
const validSessionId = value => /^[A-Za-z0-9]{11}$/.test(value || '');
const validWebhookUrl = value => {
try {
const parsed = new URL(value);
return (
['http:', 'https:'].includes(parsed.protocol) &&
!parsed.username &&
!parsed.password &&
!parsed.hash
);
} catch {
return false;
}
};
export default {
components: { PageHeader, NextButton },
setup() {
return { v$: useVuelidate() };
},
data() {
return {
channelName: '',
sessionId: '',
username: '',
password: '',
desiredPresence: 'online',
webhookUrl: '',
};
},
computed: {
...mapGetters({ uiFlags: 'inboxes/getUIFlags' }),
},
validations: {
channelName: { required },
sessionId: { required, validSessionId },
username: { required },
password: { required },
webhookUrl: { required, validWebhookUrl },
},
methods: {
async createChannel() {
this.v$.$touch();
if (this.v$.$invalid) return;
try {
const inbox = await this.$store.dispatch(
'inboxes/createChannel',
{
name: this.channelName.trim(),
channel: {
type: 'shangwutong',
session_id: this.sessionId.trim(),
username: this.username.trim(),
password: this.password,
desired_presence: this.desiredPresence,
webhook_url: this.webhookUrl.trim(),
},
}
);
router.replace({
name: 'settings_inboxes_add_agents',
params: { page: 'new', inbox_id: inbox.id },
});
} catch (error) {
useAlert(
error.message ||
this.$t(
'INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.API.ERROR_MESSAGE'
)
);
}
},
},
};
</script>
<template>
<div class="h-full w-full p-6 col-span-6">
<PageHeader
:header-title="$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.TITLE')"
:header-content="$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.DESC')"
/>
<form class="flex flex-col" @submit.prevent="createChannel">
<label :class="{ error: v$.channelName.$error }">
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.CHANNEL_NAME') }}
<input
v-model="channelName"
type="text"
@blur="v$.channelName.$touch"
/>
</label>
<label :class="{ error: v$.sessionId.$error }">
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.SESSION_ID') }}
<input
v-model="sessionId"
type="text"
maxlength="11"
autocomplete="off"
@blur="v$.sessionId.$touch"
/>
<span v-if="v$.sessionId.$error" class="message">
{{
$t(
'INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.SESSION_ID_ERROR'
)
}}
</span>
</label>
<label :class="{ error: v$.username.$error }">
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.USERNAME') }}
<input
v-model="username"
type="text"
maxlength="255"
autocomplete="username"
@blur="v$.username.$touch"
/>
</label>
<label :class="{ error: v$.password.$error }">
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PASSWORD') }}
<input
v-model="password"
type="password"
maxlength="4096"
autocomplete="new-password"
@blur="v$.password.$touch"
/>
</label>
<label>
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.DESIRED_PRESENCE') }}
<select v-model="desiredPresence">
<option value="online">online</option>
<option value="busy">busy</option>
<option value="away">away</option>
<option value="offline">offline</option>
</select>
</label>
<label :class="{ error: v$.webhookUrl.$error }">
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.WEBHOOK_URL') }}
<input
v-model="webhookUrl"
type="url"
placeholder="http://shangwutong-connector:9100/webhooks/gochat/v1"
@blur="v$.webhookUrl.$touch"
/>
</label>
<div class="w-full mt-4">
<NextButton
type="submit"
solid
blue
:is-loading="uiFlags.isCreating"
:label="
$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.SUBMIT_BUTTON')
"
/>
</div>
</form>
</div>
</template>
@@ -0,0 +1,194 @@
<script>
import { useAlert } from 'dashboard/composables';
import InboxHealthAPI from 'dashboard/api/inboxHealth';
import SettingsFieldSection from 'dashboard/components-next/Settings/SettingsFieldSection.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: { SettingsFieldSection, NextButton },
props: {
inbox: { type: Object, required: true },
},
data() {
return {
desiredPresence: 'online',
password: '',
webhookUrl: '',
health: null,
isSaving: false,
isLoadingHealth: false,
};
},
watch: {
inbox: {
immediate: true,
handler(value) {
this.desiredPresence = value.desired_presence || 'online';
this.webhookUrl = value.webhook_url || '';
},
},
},
mounted() {
this.fetchHealth();
},
methods: {
async fetchHealth() {
this.isLoadingHealth = true;
try {
const response = await InboxHealthAPI.getHealthStatus(
this.inbox.id
);
this.health = response.data;
} catch (error) {
useAlert(
error.message ||
this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')
);
} finally {
this.isLoadingHealth = false;
}
},
async save() {
this.isSaving = true;
const channel = {
desired_presence: this.desiredPresence,
webhook_url: this.webhookUrl.trim(),
};
if (this.password) channel.password = this.password;
try {
await this.$store.dispatch('inboxes/updateInbox', {
id: this.inbox.id,
formData: false,
channel,
});
this.password = '';
useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
await this.fetchHealth();
} catch (error) {
useAlert(
error.message ||
this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')
);
} finally {
this.isSaving = false;
}
},
},
};
</script>
<template>
<div class="space-y-6">
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.RUNTIME_STATUS')"
:help-text="
$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.RUNTIME_STATUS_HELP')
"
>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div class="rounded-lg border border-n-weak p-3">
<p class="text-label-small text-n-slate-10">
connection_status
</p>
<p class="text-body-main text-n-slate-12">
{{
health?.connection_status ||
inbox.connection_status ||
'pending'
}}
</p>
</div>
<div class="rounded-lg border border-n-weak p-3">
<p class="text-label-small text-n-slate-10">
actual_presence
</p>
<p class="text-body-main text-n-slate-12">
{{
health?.actual_presence ||
inbox.actual_presence ||
'offline'
}}
</p>
</div>
<div class="rounded-lg border border-n-weak p-3">
<p class="text-label-small text-n-slate-10">
credential_status
</p>
<p class="text-body-main text-n-slate-12">
{{
health?.credential_status ||
inbox.credential_status ||
'pending'
}}
</p>
</div>
<div class="rounded-lg border border-n-weak p-3">
<p class="text-label-small text-n-slate-10">
last_heartbeat_at
</p>
<p class="text-body-main text-n-slate-12">
{{
health?.last_heartbeat_at ||
inbox.last_heartbeat_at ||
'—'
}}
</p>
</div>
</div>
<p
v-if="health?.last_error_code"
class="mt-3 text-sm text-n-ruby-11"
>
{{ health.last_error_code }}
</p>
<NextButton
class="mt-3"
:is-loading="isLoadingHealth"
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.REFRESH')"
@click="fetchHealth"
/>
</SettingsFieldSection>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.DESIRED_PRESENCE')"
:help-text="
$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.DESIRED_PRESENCE_HELP')
"
>
<select v-model="desiredPresence" class="w-full">
<option value="online">online</option>
<option value="busy">busy</option>
<option value="away">away</option>
<option value="offline">offline</option>
</select>
</SettingsFieldSection>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PASSWORD')"
:help-text="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PASSWORD_HELP')"
>
<input
v-model="password"
type="password"
maxlength="4096"
autocomplete="new-password"
class="w-full"
/>
</SettingsFieldSection>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.WEBHOOK_URL')"
:help-text="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.WEBHOOK_URL_HELP')"
>
<input v-model="webhookUrl" type="url" class="w-full" />
</SettingsFieldSection>
<div class="flex justify-end">
<NextButton
:is-loading="isSaving"
:label="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
@click="save"
/>
</div>
</div>
</template>
@@ -32,6 +32,7 @@ const i18nMap = {
'Channel::Telegram': 'TELEGRAM',
'Channel::Line': 'LINE',
'Channel::Api': 'API',
'Channel::Shangwutong': 'SHANGWUTONG',
'Channel::Instagram': 'INSTAGRAM',
'Channel::Tiktok': 'TIKTOK',
};
@@ -11,6 +11,7 @@ import { required } from '@vuelidate/validators';
import NextButton from 'dashboard/components-next/button/Button.vue';
import TextArea from 'next/textarea/TextArea.vue';
import WhatsappReauthorize from '../channels/whatsapp/Reauthorize.vue';
import ShangwutongConfiguration from '../channels/ShangwutongConfiguration.vue';
import { sanitizeAllowedDomains } from 'dashboard/helper/URLHelper';
export default {
@@ -23,6 +24,7 @@ export default {
NextButton,
TextArea,
WhatsappReauthorize,
ShangwutongConfiguration,
},
mixins: [inboxMixin],
props: {
@@ -189,6 +191,7 @@ export default {
</script>
<template>
<ShangwutongConfiguration v-if="isShangwutongInbox" :inbox="inbox" />
<div v-if="isATwilioChannel">
<SettingsFieldSection
:label="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')"