feat(channels): 社交渠道 OAuth 改进 + 集成应用重命名 + AI 流程文档
- Facebook/Instagram/TikTok 渠道 OAuth 授权流程改进 - 新增 oauth/credentialstore 包 - 集成应用 OpenAI 重命名为 OpenAI 兼容 (migration 061) - 消息生命周期与 AI 流程架构文档 - inbox 管理界面 i18n 更新
This commit is contained in:
@@ -15,18 +15,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['channelItemClick']);
|
||||
|
||||
const hasFbConfigured = computed(() => {
|
||||
return window.chatwootConfig?.fbAppId;
|
||||
});
|
||||
|
||||
const hasInstagramConfigured = computed(() => {
|
||||
return window.chatwootConfig?.instagramAppId;
|
||||
});
|
||||
|
||||
const hasTiktokConfigured = computed(() => {
|
||||
return window.chatwootConfig?.tiktokAppId;
|
||||
});
|
||||
|
||||
const isActive = computed(() => {
|
||||
const { key } = props.channel;
|
||||
if (Object.keys(props.enabledFeatures).length === 0) {
|
||||
@@ -35,39 +23,18 @@ const isActive = computed(() => {
|
||||
if (key === 'website') {
|
||||
return props.enabledFeatures.channel_website;
|
||||
}
|
||||
if (key === 'facebook') {
|
||||
return props.enabledFeatures.channel_facebook && hasFbConfigured.value;
|
||||
}
|
||||
if (key === 'email') {
|
||||
return props.enabledFeatures.channel_email;
|
||||
}
|
||||
|
||||
if (key === 'instagram') {
|
||||
return (
|
||||
props.enabledFeatures.channel_instagram && hasInstagramConfigured.value
|
||||
);
|
||||
}
|
||||
|
||||
if (key === 'tiktok') {
|
||||
return props.enabledFeatures.channel_tiktok && hasTiktokConfigured.value;
|
||||
}
|
||||
|
||||
if (key === 'voice') {
|
||||
return props.enabledFeatures.channel_voice;
|
||||
}
|
||||
|
||||
return [
|
||||
'website',
|
||||
'twilio',
|
||||
'api',
|
||||
'whatsapp',
|
||||
'sms',
|
||||
'telegram',
|
||||
'line',
|
||||
'instagram',
|
||||
'tiktok',
|
||||
'voice',
|
||||
].includes(key);
|
||||
// All other channels (facebook, instagram, tiktok, whatsapp, sms, api,
|
||||
// telegram, line, etc.) are always clickable. OAuth credentials (App ID,
|
||||
// API tokens, etc.) are collected per-inbox during the channel setup flow,
|
||||
// not gated by global config.
|
||||
return true;
|
||||
});
|
||||
|
||||
const isComingSoon = computed(() => {
|
||||
|
||||
@@ -49,7 +49,9 @@
|
||||
"ADD_NAME": "Add a name for your inbox",
|
||||
"PICK_NAME": "Pick a Name for your Inbox",
|
||||
"PICK_A_VALUE": "Pick a value",
|
||||
"CREATE_INBOX": "Create Inbox"
|
||||
"CREATE_INBOX": "Create Inbox",
|
||||
"LOGIN_WITH_FB": "Login with Facebook",
|
||||
"APP_CREDENTIALS_HELP": "Enter your Meta App credentials to connect. Each inbox can use a different Meta App."
|
||||
},
|
||||
"INSTAGRAM": {
|
||||
"CONTINUE_WITH_INSTAGRAM": "Continue with Instagram",
|
||||
|
||||
@@ -49,7 +49,9 @@
|
||||
"ADD_NAME": "为收件箱添加名称",
|
||||
"PICK_NAME": "为收件箱选择一个名称",
|
||||
"PICK_A_VALUE": "选择一个数值",
|
||||
"CREATE_INBOX": "新增收件箱"
|
||||
"CREATE_INBOX": "新增收件箱",
|
||||
"LOGIN_WITH_FB": "使用 Facebook 登录",
|
||||
"APP_CREDENTIALS_HELP": "输入您的 Meta App 凭据以进行连接。每个收件箱可以使用不同的 Meta App。"
|
||||
},
|
||||
"INSTAGRAM": {
|
||||
"CONTINUE_WITH_INSTAGRAM": "在 Instagram 中继续",
|
||||
|
||||
@@ -16,10 +16,6 @@ const globalConfig = useMapGetter('globalConfig/get');
|
||||
|
||||
const enabledFeatures = ref({});
|
||||
|
||||
const hasTiktokConfigured = computed(() => {
|
||||
return window.chatwootConfig?.tiktokAppId;
|
||||
});
|
||||
|
||||
const channelList = computed(() => {
|
||||
const { apiChannelName } = globalConfig.value;
|
||||
const channels = [
|
||||
@@ -79,14 +75,12 @@ const channelList = computed(() => {
|
||||
},
|
||||
];
|
||||
|
||||
if (hasTiktokConfigured.value) {
|
||||
channels.push({
|
||||
key: 'tiktok',
|
||||
title: t('INBOX_MGMT.ADD.AUTH.CHANNEL.TIKTOK.TITLE'),
|
||||
description: t('INBOX_MGMT.ADD.AUTH.CHANNEL.TIKTOK.DESCRIPTION'),
|
||||
icon: 'i-woot-tiktok',
|
||||
});
|
||||
}
|
||||
channels.push({
|
||||
key: 'tiktok',
|
||||
title: t('INBOX_MGMT.ADD.AUTH.CHANNEL.TIKTOK.TITLE'),
|
||||
description: t('INBOX_MGMT.ADD.AUTH.CHANNEL.TIKTOK.DESCRIPTION'),
|
||||
icon: 'i-woot-tiktok',
|
||||
});
|
||||
|
||||
channels.push({
|
||||
key: 'voice',
|
||||
|
||||
+61
-11
@@ -47,6 +47,10 @@ export default {
|
||||
errorStateMessage: '',
|
||||
errorStateDescription: '',
|
||||
hasLoginStarted: false,
|
||||
// Per-inbox Meta App credentials (collected during channel setup)
|
||||
fbAppId: '',
|
||||
fbAppSecret: '',
|
||||
fbApiVersion: 'v18.0',
|
||||
};
|
||||
},
|
||||
|
||||
@@ -122,9 +126,9 @@ export default {
|
||||
|
||||
runFBInit() {
|
||||
FB.init({
|
||||
appId: window.chatwootConfig.fbAppId,
|
||||
appId: this.fbAppId,
|
||||
xfbml: true,
|
||||
version: window.chatwootConfig.fbApiVersion,
|
||||
version: this.fbApiVersion,
|
||||
status: true,
|
||||
});
|
||||
window.fbSDKLoaded = true;
|
||||
@@ -195,6 +199,8 @@ export default {
|
||||
page_access_token: this.selectedPage.access_token,
|
||||
page_id: this.selectedPage.id,
|
||||
inbox_name: this.selectedPage.name?.trim(),
|
||||
fb_app_id: this.fbAppId,
|
||||
fb_app_secret: this.fbAppSecret,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -226,16 +232,60 @@ export default {
|
||||
v-if="!hasLoginStarted"
|
||||
class="flex flex-col items-center justify-center h-full text-center"
|
||||
>
|
||||
<a href="#" @click="startLogin()">
|
||||
<img
|
||||
class="w-auto h-10 rounded-md"
|
||||
src="~dashboard/assets/images/channels/facebook_login.png"
|
||||
alt="Facebook-logo"
|
||||
<div class="w-full max-w-md space-y-4">
|
||||
<div class="flex flex-col items-center mb-4">
|
||||
<a href="#" @click.prevent>
|
||||
<img
|
||||
class="w-auto h-10 rounded-md"
|
||||
src="~dashboard/assets/images/channels/facebook_login.png"
|
||||
alt="Facebook-logo"
|
||||
/>
|
||||
</a>
|
||||
<p class="py-4">
|
||||
{{ replaceInstallationName($t('INBOX_MGMT.ADD.FB.HELP')) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-left space-y-3">
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">
|
||||
Meta App ID
|
||||
</span>
|
||||
<input
|
||||
v-model="fbAppId"
|
||||
type="text"
|
||||
placeholder="1234567890123456"
|
||||
class="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">
|
||||
Meta App Secret
|
||||
</span>
|
||||
<input
|
||||
v-model="fbAppSecret"
|
||||
type="password"
|
||||
placeholder="App Secret"
|
||||
class="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">
|
||||
Graph API Version
|
||||
</span>
|
||||
<input
|
||||
v-model="fbApiVersion"
|
||||
type="text"
|
||||
placeholder="v18.0"
|
||||
class="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<NextButton
|
||||
:label="$t('INBOX_MGMT.ADD.FB.LOGIN_WITH_FB')"
|
||||
:disabled="!fbAppId || !fbAppSecret"
|
||||
@click="startLogin()"
|
||||
/>
|
||||
</a>
|
||||
<p class="py-6">
|
||||
{{ replaceInstallationName($t('INBOX_MGMT.ADD.FB.HELP')) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="hasError" class="max-w-lg mx-auto text-center">
|
||||
|
||||
+33
-2
@@ -11,6 +11,10 @@ const errorStateMessage = ref('');
|
||||
const errorStateDescription = ref('');
|
||||
const isRequestingAuthorization = ref(false);
|
||||
|
||||
// Per-inbox Meta App credentials (collected during channel setup)
|
||||
const igAppId = ref('');
|
||||
const igAppSecret = ref('');
|
||||
|
||||
onMounted(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
// TODO: Handle error type
|
||||
@@ -36,7 +40,10 @@ onMounted(() => {
|
||||
|
||||
const requestAuthorization = async () => {
|
||||
isRequestingAuthorization.value = true;
|
||||
const response = await instagramClient.generateAuthorization();
|
||||
const response = await instagramClient.generateAuthorization({
|
||||
app_id: igAppId.value,
|
||||
app_secret: igAppSecret.value,
|
||||
});
|
||||
const {
|
||||
data: { url },
|
||||
} = response;
|
||||
@@ -65,11 +72,35 @@ const requestAuthorization = async () => {
|
||||
<p class="py-6 text-sm text-n-slate-11">
|
||||
{{ $t('INBOX_MGMT.ADD.INSTAGRAM.HELP') }}
|
||||
</p>
|
||||
<div class="w-full max-w-sm space-y-3 text-left mb-6">
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">
|
||||
Instagram App ID
|
||||
</span>
|
||||
<input
|
||||
v-model="igAppId"
|
||||
type="text"
|
||||
placeholder="1234567890123456"
|
||||
class="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">
|
||||
Instagram App Secret
|
||||
</span>
|
||||
<input
|
||||
v-model="igAppSecret"
|
||||
type="password"
|
||||
placeholder="App Secret"
|
||||
class="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
class="text-white !rounded-full !px-6 bg-gradient-to-r from-[#833AB4] via-[#FD1D1D] to-[#FCAF45]"
|
||||
lg
|
||||
icon="i-ri-instagram-line"
|
||||
:disabled="isRequestingAuthorization"
|
||||
:disabled="isRequestingAuthorization || !igAppId || !igAppSecret"
|
||||
:is-loading="isRequestingAuthorization"
|
||||
:label="$t('INBOX_MGMT.ADD.INSTAGRAM.CONTINUE_WITH_INSTAGRAM')"
|
||||
@click="requestAuthorization()"
|
||||
|
||||
+33
-2
@@ -11,6 +11,10 @@ const errorStateMessage = ref('');
|
||||
const errorStateDescription = ref('');
|
||||
const isRequestingAuthorization = ref(false);
|
||||
|
||||
// Per-inbox TikTok App credentials (collected during channel setup)
|
||||
const ttAppId = ref('');
|
||||
const ttAppSecret = ref('');
|
||||
|
||||
onMounted(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
// TODO: Handle error type
|
||||
@@ -36,7 +40,10 @@ onMounted(() => {
|
||||
|
||||
const requestAuthorization = async () => {
|
||||
isRequestingAuthorization.value = true;
|
||||
const response = await tiktokClient.generateAuthorization();
|
||||
const response = await tiktokClient.generateAuthorization({
|
||||
app_id: ttAppId.value,
|
||||
app_secret: ttAppSecret.value,
|
||||
});
|
||||
const {
|
||||
data: { url },
|
||||
} = response;
|
||||
@@ -65,11 +72,35 @@ const requestAuthorization = async () => {
|
||||
<p class="py-6 text-sm text-n-slate-11">
|
||||
{{ $t('INBOX_MGMT.ADD.TIKTOK.HELP') }}
|
||||
</p>
|
||||
<div class="w-full max-w-sm space-y-3 text-left mb-6">
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">
|
||||
TikTok App ID
|
||||
</span>
|
||||
<input
|
||||
v-model="ttAppId"
|
||||
type="text"
|
||||
placeholder="1234567890123456"
|
||||
class="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
/>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">
|
||||
TikTok App Secret
|
||||
</span>
|
||||
<input
|
||||
v-model="ttAppSecret"
|
||||
type="password"
|
||||
placeholder="App Secret"
|
||||
class="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
class="text-white !rounded-full !px-6 bg-gradient-to-r from-[#00f2ea] via-[#ff0050] to-[#000000]"
|
||||
lg
|
||||
icon="i-ri-tiktok-line"
|
||||
:disabled="isRequestingAuthorization"
|
||||
:disabled="isRequestingAuthorization || !ttAppId || !ttAppSecret"
|
||||
:is-loading="isRequestingAuthorization"
|
||||
:label="$t('INBOX_MGMT.ADD.TIKTOK.CONTINUE_WITH_TIKTOK')"
|
||||
@click="requestAuthorization()"
|
||||
|
||||
+8
-2
@@ -20,6 +20,12 @@ export default {
|
||||
inboxId() {
|
||||
return this.inbox.id;
|
||||
},
|
||||
fbAppId() {
|
||||
return this.inbox.fb_app_id || '';
|
||||
},
|
||||
fbApiVersion() {
|
||||
return this.inbox.fb_api_version || 'v18.0';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.fbAsyncInit = this.runFBInit;
|
||||
@@ -27,9 +33,9 @@ export default {
|
||||
methods: {
|
||||
runFBInit() {
|
||||
FB.init({
|
||||
appId: window.chatwootConfig.fbAppId,
|
||||
appId: this.fbAppId,
|
||||
xfbml: true,
|
||||
version: window.chatwootConfig.fbApiVersion,
|
||||
version: this.fbApiVersion,
|
||||
status: true,
|
||||
});
|
||||
window.fbSDKLoaded = true;
|
||||
|
||||
Reference in New Issue
Block a user