feat(channels): 社交渠道 OAuth 改进 + 集成应用重命名 + AI 流程文档

- Facebook/Instagram/TikTok 渠道 OAuth 授权流程改进
- 新增 oauth/credentialstore 包
- 集成应用 OpenAI 重命名为 OpenAI 兼容 (migration 061)
- 消息生命周期与 AI 流程架构文档
- inbox 管理界面 i18n 更新
This commit is contained in:
Rogee
2026-08-01 19:38:27 +08:00
parent 63df5b8b91
commit 1446cfe701
21 changed files with 897 additions and 99 deletions
@@ -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',
@@ -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">
@@ -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()"
@@ -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()"
@@ -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;