Kaynağa Gözat

📦 AI用量不足后支持用乡源果购买

快乐的梦鱼 1 ay önce
ebeveyn
işleme
392df8b90c

+ 0 - 6
1.md

@@ -1,6 +0,0 @@
-1. 首页:增加精选推荐接口精选文章、贴图
-2. 乡源页:增加一个接口:获取指定村落乡源人列表
-3. followVillage:增加我已加入的村社列表
-4. addVillageClaim: 加入现在用的这个接口,目前需要审核,普通应该不需要审核(加入后立即在列表中显示),
-或者标记一个未审核状态,这个状态不能进行挖掘采编,建议可拆分接口。
-4. village/village/getVillageList:同上,增加返回一个状态,审核通过后允许采编内容,否则调用addVillageClaim进行申请

+ 31 - 0
AI功能相关需要修改.md

@@ -0,0 +1,31 @@
+* 建村简介需要加接口,临时保存已生成过的AI简介(由前端上传):
+  * 保存简介:输入 area_code, desc
+  * 获取简介: 输入 area_code ,查询已有 desc
+    
+* 增加乡源果兑换ai对话次数功能
+
+  ![](https://xy.wenlvti.net/uploads/20260713/da53bc047f4cfd1a096046ebb9bce924.png)
+
+  提供给小程序 接口 **同升级/升级管理员** 需要有获取套餐和下单接口 ,不同的是购买成功的兑现需要调用 [AI接入微服务](https://update-server1.imengyu.top/api-docs) 增加余额:
+
+```php
+//订单支付成功后调用接口为指定用户增加余额
+$ch = curl_init("https://update-server1.imengyu.top/content/agent/cost-settings/external/add-ballance");
+curl_setopt($ch, CURLOPT_POST, true);
+curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json', 'Content-Type: application/json', 'Authorization: {"apiKey":"i7JRrzFrrCjcTQ87tTeh2Mte3rRMB8QM","apiSecret":"JKkESyfRNQFiwdmJT7ydse4rSZfYePYAhcMjdBZk5EZCDzbd7rYdT6XQjPmQXPiR"}']);
+curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
+  'externalUserId' => 1,//乡源用户ID
+  'ballance' => 1000 //增加的TOKEN数, 平均一轮对话1000token左右
+]));
+curl_exec($ch);
+curl_close($ch);
+```
+
+  * [加余额接口文档](https://update-server1.imengyu.top/api-docs#tag/%E5%86%85%E5%AE%B9aiai%E5%AF%B9%E8%AF%9D/POST/content/agent/cost-settings/external/add-ballance)
+  * [获取余额文档(若需要查询)](https://update-server1.imengyu.top/api-docs#tag/%E5%86%85%E5%AE%B9aiai%E5%AF%B9%E8%AF%9D/GET/content/agent/cost-settings/external/get-cost-settings)
+
+套餐建议设置:
+1. token: 10000 约10次对话 20乡源果
+2. token: 20000 约20次对话 30乡源果
+3. token: 50000 约50次对话 68乡源果
+4. token: 100000 约100次对话 99乡源果

+ 38 - 0
src/api/agent/AgentCostSettings.ts

@@ -0,0 +1,38 @@
+import { DataModel } from "@imengyu/js-request-transform";
+import { RestfulApi } from "../restful";
+
+/**
+ * AI 消耗设置
+ */
+export class AgentCostSettings extends DataModel<AgentCostSettings> {
+  constructor() {
+    super(AgentCostSettings, "AI消耗设置");
+    this._convertTable = {
+      enable: { clientSide: "boolean" },
+    };
+  }
+
+  id = 0;
+  userId = 0;
+  enable = false;
+  balance = 0;
+}
+
+export class AgentCostSettingsApi extends RestfulApi<AgentCostSettings> {
+  constructor() {
+    super("content/agent/cost-settings", AgentCostSettings);
+  }
+
+  /**
+   * 获取当前登录用户的余额设置
+   */
+  getMyCostSettings() {
+    return this.get<{ balance: number; enable: boolean }>(
+      "/content/agent/cost-settings/my-cost-settings",
+      "获取我的余额设置",
+    );
+  }
+}
+
+export default new AgentCostSettingsApi();
+

+ 1 - 1
src/api/agent/AgentWorks.ts

@@ -7,7 +7,7 @@ export class AgentWorkApi extends RestfulApi<DataModel> {
     super("content/agent", DataModel);
   }
 
-  private static readonly WORK_MINI_MODEL_NAME = 'hunyuan-2.0-instruct-20251111';
+  private static readonly WORK_MINI_MODEL_NAME = 'hy3';
 
   private static readonly QUESTION_RELATED_CACHE_TTL_MS = 5 * 60 * 1000;
   private static readonly QUESTION_RELATED_CACHE_MAX_SIZE = 200;

+ 91 - 0
src/api/light/OfficialApi.ts

@@ -224,6 +224,57 @@ export class StaffLevelItem extends DataModel<StaffLevelItem> {
   updatetime = '';
 }
 
+export class AiPackageItem extends DataModel<AiPackageItem> {
+  constructor() {
+    super(AiPackageItem, 'AI对话套餐');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      price: { clientSide: 'number', serverSide: 'string' },
+    };
+  }
+  id!: number;
+  /** 套餐名称 */
+  name = '';
+  /** 价格 */
+  price = 0;
+}
+export class AiPackageOrder extends DataModel<AiPackageOrder> {
+  constructor() {
+    super(AiPackageOrder, 'AI对话套餐订单');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      userId: { clientSide: 'number', serverSide: 'number' },
+      amount: { clientSide: 'number', serverSide: 'number' },
+      status: { clientSide: 'number', serverSide: 'number' },
+      orderType: { clientSide: 'number', serverSide: 'number' },
+      payType: { clientSide: 'number', serverSide: 'number' },
+      paytime: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  id!: number;
+  /** 订单号 */
+  orderNo = '';
+  /** 用户ID */
+  userId = 0;
+  /** 村社ID */
+  villageId = 0;
+  /** 支付金额 */
+  amount = 0;
+  /** 状态: -1=已取消, 0=未支付, 1=已支付, 2=待审核 */
+  status = 0;
+  /** 支付方式: 1=线上支付, 2=对公付款 */
+  payType = 0;
+  /** 支付时间 */
+  paytime = 0;
+  /** 创建时间 */
+  createtime = '';
+  /** 更新时间 */
+  updatetime = '';
+}
+
 export interface WxPayParams {
   appId: string;
   timeStamp: string;
@@ -405,6 +456,46 @@ export class OfficialApi extends AppServerRequestModule<DataModel> {
       pay: data.pay,
     } as StaffUpgradeResult;
   }
+
+  /**
+   * 获取AI对话套餐列表
+   */
+  async getAiPackageList(page: number, pageSize: number) {
+    const res = await this.post<{
+      data: KeyValue[],
+      total: number,
+      per_page: number,
+      current_page: number,
+      last_page: number,
+    }>('/village/ai/package', '获取个人升级套餐列表', {
+      page,
+      pageSize,
+    });
+    const data = res.requireData();
+    return {
+      list: transformArrayDataModel<AiPackageItem>(AiPackageItem, transformSomeToArray(data.data), 'AI对话套餐列表', true),
+      total: data.total,
+    };
+  }
+
+  /**
+   * 
+   */
+  async payAiPackage(villageId: number, packageId = 1, payType: 1 | 2 | 3 = 1) {
+    const res = await this.post<{
+      order: KeyValue,
+      pay: WxPayParams,
+    }>('/village/growth/staff', '升级村社管理', {
+      village_id: villageId,
+      package_id: packageId,
+      pay_type: payType,
+    });
+    const data = res.requireData();
+    return {
+      order: transformDataModel<AiPackageOrder>(AiPackageOrder, data.order),
+      pay: data.pay,
+    };
+  }
 }
 
 

+ 1 - 1
src/pages/home/chat/components/ChatFooter.vue

@@ -47,7 +47,7 @@
         >
         </textarea>
         <IconButton 
-          v-if="props.chatInterfaceManager.uploadAttachment"
+          v-if="props.chatInterfaceManager?.uploadAttachment"
           icon="picture"
           :loading="props.chatManager.isLoading.value"
           :innerStyle="{

+ 6 - 0
src/pages/home/chat/core/Chat.ts

@@ -82,6 +82,11 @@ export interface ChatConfig {
    */
   onGetSendOptions: () => ChatSendOptions;
 
+  /** 
+   * 使用额度受限
+   */
+  onUseageLimited?: () => void;
+
   /**
    * 新对话
    * @param isNewChat - 是否是新对话
@@ -435,6 +440,7 @@ export function useChat(options: {
       if (('' +error).includes('402')) {
         aiMessage.content = "您今日的对话次数已用完,请明日再来吧。";
         aiMessage.state = "error";
+        config.onUseageLimited?.();
       } else {
         aiMessage.content = "失败,请稍后重试。";
         aiMessage.setError("请求错误", ChatUtils.formatError(error));

+ 43 - 3
src/pages/home/chat/dependent/post/components/agent.vue

@@ -46,6 +46,17 @@
             />
           </template>
           <template #header>
+            <FlexRow 
+              v-if="isUseageLimited"
+              margin="margin.md" justify="space-between" align="center" padding="padding.md"
+              radius="radius.md" backgroundColor="background.primary"
+            >
+              <FlexRow gap="gap.md">
+                <Icon icon="prompt-filling" />
+                <Text text="您与AI的今日对话次数已经用完,请明天再来吧!也可以使用乡源果兑换AI对话次数。" fontConfig="secondText" />
+              </FlexRow>
+              <Button text="去兑换" type="primary" @click="emit('recharge')" />
+            </FlexRow>
             <scroll-view scroll-x>
               <FlexRow align="center" padding="padding.md" gap="gap.md">
                 <Button text="看图写作" @click="imageWriting" />
@@ -145,13 +156,15 @@ import ChatMulitSelectBar from '../../../components/Footer/ChatMulitSelectBar.vu
 import NavBar from '@/components/nav/NavBar.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
-import type { VillageListItem } from '@/api/light/LightVillageApi';
 import AgentApi from '@/api/agent/Agent';
 import CommonContent from '@/api/CommonContent';
 import Button from '@/components/basic/Button.vue';
 import Field from '@/components/form/Field.vue';
 import Popup from '@/components/dialog/Popup.vue';
 import Tags from '@/components/form/Tags.vue';
+import AgentCostSettingsApi from '@/api/agent/AgentCostSettings';
+import Icon from '@/components/basic/Icon.vue';
+import Text from '@/components/basic/Text.vue';
 
 const messages = ref<ChatMessageModel[]>([]) as Ref<ChatMessageModel[]>;
 const interfaceManager: ChatInterfaceManager = {
@@ -180,10 +193,20 @@ const emit = defineEmits([
   'update:images',
   'update:showAgentPopup',
   'close',
-  'upload'
+  'upload',
+  'recharge'
 ]);
 
 const showSessionSidebar = ref(false);
+const isUseageLimited = ref(false);
+
+function loadUseageLimitedStatus() {
+  return AgentCostSettingsApi.getMyCostSettings().then(res => {
+    isUseageLimited.value = res.requireData().balance <= 0;
+  }).catch(() => {
+    isUseageLimited.value = false;
+  }); 
+}
 
 //快速操作控制
 //=============================================
@@ -418,7 +441,7 @@ const chatManager = useChat({
         value: 'hunyuan-2.0-thinking-20251109',
         max_tokens: 10000,
       },
-      model: 'hunyuan-2.0-thinking-20251109',
+      model: 'hy3',
       chatOptions: {
         temperature: 0.5,
         top_p: 1,
@@ -427,6 +450,9 @@ const chatManager = useChat({
       },
       customSystemPrompt: '',
     }),
+    onUseageLimited: () => {
+      isUseageLimited.value = true;
+    },
     defaultSystemPrompt: `你是一个亮乡源的“乡村文化挖掘笔记/文章写作与改稿助手”,工作在一个笔记编辑器里。你需要在与用户对话的同时,能直接读取并修改当前文章(标题与正文)。
 
 ## 工作目标
@@ -477,11 +503,25 @@ const {
   isSelectMode,
 } = useChatSelection();
 
+export interface AgentRefreshStatInterface {
+  doRefreshLimitStats() : Promise<boolean>;
+  showAndContinue(): void;
+}
+
 defineExpose({
   openImageWriting: () => imageWriting(),
   openImageWritingDialogIfLastClicked,
+  loadUseageLimitedStatus,
+  doRefreshLimitStats: async () => {
+    await loadUseageLimitedStatus();
+    return !isUseageLimited.value;
+  },
+  showAndContinue: () => {
+    emit('update:showAgentPopup', true);
+  },
 })
 onMounted(() => {
+  loadUseageLimitedStatus();
   interfaceManager.scrollToBottom?.();
 });
 </script>

+ 35 - 0
src/pages/home/chat/dependent/post/composables/recharge.ts

@@ -0,0 +1,35 @@
+import { navTo } from "@/components/utils/PageAction"
+import type { Ref } from "vue"
+import type { AgentRefreshStatInterface } from "../components/agent.vue"
+import { confirm, alert } from "@/components/utils/DialogAction"
+
+export function useRecharge(agentRef: Ref<AgentRefreshStatInterface>) {
+
+  function handleRecharge() {
+    navTo('/pages/home/village/upgrade/buy-ai-token')
+  }
+  function handleRechargePaySuccess() {
+    agentRef.value?.doRefreshLimitStats().then((res) => {
+      if (res) {
+        confirm({
+          content: '感谢您的充值!可以与AI继续对话啦。对AI说“继续”可以继续之前未完成的对话。',
+          title: '提示',
+        }).then((res) => {
+          if (res) {
+            agentRef.value?.showAndContinue();
+          }
+        })
+      } else {
+        alert({
+          content: '充值似乎没有到账,请稍后再试。可以联系客服查询原因',
+          title: '提示',
+        })
+      }
+    });
+  }
+
+  return {
+    handleRecharge,
+    handleRechargePaySuccess,
+  }
+}

+ 16 - 1
src/pages/home/chat/dependent/post/editor.vue

@@ -47,6 +47,7 @@
         v-model:content="currentContent"
         v-model:images="currentImages"
         :extraInfoFoAi="extraAiInfo"
+        @recharge="handleRecharge"
         @upload="uploader?.pick()"
         @close="showAgentPopup = false" 
       />
@@ -58,10 +59,11 @@
 import { ref } from 'vue';
 import { useAuthStore } from '@/store/auth';
 import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
-import { confirm } from '@/components/utils/DialogAction';
+import { alert, confirm } from '@/components/utils/DialogAction';
 import { back, backAndCallOnPageBack } from '@/components/utils/PageAction';
 import { showError } from '@/common/composeabe/ErrorDisplay';
 import { useAliOssUploadCo } from '@/common/components/upload/AliOssUploadCo';
+import { useRecharge } from './composables/recharge';
 import spEditor from '../../components/sp-editor/components/sp-editor/sp-editor.vue';
 import XBarSpace from '@/components/layout/space/XBarSpace.vue';
 import Button from '@/components/basic/Button.vue';
@@ -100,6 +102,11 @@ const authStore = useAuthStore();
 const uploader = ref<UploaderInstance>();
 const agentRef = ref<InstanceType<typeof Agent>>();
 
+const { 
+  handleRecharge, 
+  handleRechargePaySuccess 
+} = useRecharge(agentRef as any);
+
 function cancel() {
   confirm({
     title: '提示',
@@ -202,4 +209,12 @@ function upinImage(tempFiles: any, editorCtx: any) {
     })
   }).catch((e) => showError(e, '上传图片失败'));
 }
+
+defineExpose({
+  onPageBack(name: string, data: any) {
+    if (name === 'paySuccessAndRefresh') {
+      handleRechargePaySuccess();
+    }
+  },
+});
 </script>

+ 16 - 0
src/pages/home/chat/dependent/post/publish.vue

@@ -64,6 +64,7 @@
         v-model:content="content"
         v-model:images="images"
         :extraInfoFoAi="extraInfoFoAi"
+        @recharge="handleRecharge"
         @upload="uploader?.pick()"
         @close="showAgentPopup = false" 
       />
@@ -81,6 +82,7 @@ import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLo
 import { envVersion, isDevEnv } from '@/common/config/AppCofig';
 import { back, backAndCallOnPageBack } from '@/components/utils/PageAction';
 import { confirm as uniConfirm } from '@/components/utils/DialogAction';
+import { useRecharge } from './composables/recharge';
 import type { UploaderAction, UploaderItem } from '@/components/form/Uploader';
 import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
 import Field from '@/components/form/Field.vue';
@@ -278,6 +280,20 @@ function onUpdateList(list: UploaderItem[]) {
   }));
 }
 
+
+const { 
+  handleRecharge, 
+  handleRechargePaySuccess 
+} = useRecharge(agentRef as any);
+
+defineExpose({
+  onPageBack(name: string, data: any) {
+    if (name === 'paySuccessAndRefresh') {
+      handleRechargePaySuccess();
+    }
+  },
+});
+
 onMounted(() => {
   setTimeout(() => {
     loadDraft();

+ 6 - 1
src/pages/home/village/introd/card.vue

@@ -291,7 +291,10 @@
         </FlexRow>
       </template>
     </HomeTitle>
-    <RoundTags v-model:active="listActiveTag" :tags="['广场', '老味道', '老手艺', '老物件', '老故事']" />
+    <RoundTags 
+      v-model:active="listActiveTag" 
+      :tags="listTags" 
+    />
   </FlexCol>
   <OfficialAccountPublishWrap
     :topic="recommendTagName" 
@@ -509,6 +512,8 @@ function handleCloseManageTip(goComp: boolean) {
   showManageTipTimeout.recordTime();
 }
 
+const listTags = ['广场', '美食', '美景', '故事', '老手艺', '老物件'];
+
 watch(rankActiveTag, () => {
   villageUserRankListLoader.reload();
 });

+ 163 - 0
src/pages/home/village/upgrade/buy-ai-token.vue

@@ -0,0 +1,163 @@
+
+
+<template>
+  <CommonTopBanner 
+    title="购买AI对话次数"
+    showNav
+    :customBack="true"
+    @backPressed="handleBack"
+  >
+    <FlexCol gap="gap.lg" padding="padding.md">
+      
+      <template v-if="step === 'choose'">
+        <FlexCol padding="padding.md" center>
+          <Text 
+            text="欢迎购买AI对话次数,购买后立即生效,请选择购买套餐"
+            fontConfig="contentText" :fontSize="30" textAlign="center" 
+          />
+        </FlexCol>
+
+        <BoxMid 
+          v-for="item in loader.content.value" :key="item.id"
+          direction="row" justify="space-between" align="center" gap="gap.md"
+        >
+          <FlexCol width="74%" gap="gap.md">
+            <FlexCol gap="gap.md">
+              <Text :text="item.name" fontConfig="lightImportantTitle" :fontSize="42" />
+              <Text :text="`${item.timeTypeText === '限时' ? `临时 ${item.days} 天管理员` : '永久'}`" :fontSize="32" :color="item.timeTypeText === '永久' ? 'warning' : 'success'" />
+            </FlexCol>
+          </FlexCol>
+          <FlexRow align="center" gap="gap.lg">
+            <Icon icon="https://xy.wenlvti.net/app_static/images/village/IconFruit.png" :size="40" />
+            <Text :text="moneyToFruit(item.price)" fontConfig="lightGoldTitle" :fontSize="40" />
+            <FrameButton primary text="选择" @click="handleChoose(item)" />
+          </FlexRow>
+        </BoxMid>
+
+      </template>
+      <template v-else-if="step === 'pay'">
+
+        <FlexCol padding="padding.md" center>
+          <Text 
+            text="请选择您要付款方式" 
+            fontConfig="contentText" :fontSize="30" textAlign="center" 
+          />
+        </FlexCol>
+
+        <BuyFruitInfo :price="choosedLevel?.price || 0" @pay="handleDirectPay(3)">
+          <template #prepend>
+            <BoxMid direction="row" justify="space-between" align="center" gap="gap.md">
+              <FlexCol width="74%">
+                <Text text="在线支付" fontConfig="lightImportantTitle" :fontSize="42" />
+                <Text text="若乡源果不足,还支持微信线支付方式,立即生效" fontConfig="lightGoldTitle" :fontSize="30" />
+              </FlexCol>
+              <FrameButton primary text="选择" @click="handleDirectPay(1)" />
+            </BoxMid>
+          </template>
+        </BuyFruitInfo>
+      </template>
+    </FlexCol>
+  </CommonTopBanner>
+</template>
+
+<script setup lang="ts">
+import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+import { useRequireLogin } from '@/common/composeabe/RequireLogin';
+import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
+import { ref } from 'vue';
+import { back, backAndCallOnPageBack } from '@/components/utils/PageAction';
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import BoxMid from '@/common/components/box/BoxMid.vue';
+import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+import Text from '@/components/basic/Text.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import OfficialApi, { type AiPackageItem } from '@/api/light/OfficialApi';
+import BuyFruitInfo from './components/BuyFruitInfo.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Icon from '@/components/basic/Icon.vue';
+import { useFruitConversion } from '../../composeabe/FruitConverit';
+import { waitTimeOut } from '@imengyu/imengyu-utils';
+
+const { requireLoginAsync } = useRequireLogin();
+const { moneyToFruit } = useFruitConversion();
+
+function handlePaySuccess() {
+  setTimeout(() => {
+    backAndCallOnPageBack('paySuccessAndRefresh', {});
+  }, 100);
+}
+
+const { querys } = useLoadQuerys({
+  villageId: 0,
+}, () => {
+
+});
+
+const step = ref<'choose' | 'pay'>('choose');
+const choosedLevel = ref<AiPackageItem|null>(null);
+const loader = useSimpleDataLoader(async () => {
+  return (await OfficialApi.getAiPackageList(1, 10)).list;
+})
+
+function handleBack() {
+  if (step.value === 'pay')
+    step.value = 'choose';
+  else 
+    back();
+}
+async function handleChoose(level: AiPackageItem) {
+  uni.showLoading({ title: '请稍后...' });
+  step.value = 'pay';
+  choosedLevel.value = level;
+  await waitTimeOut(1000);
+  uni.hideLoading();
+}
+async function handleDirectPay(payType: 1 | 3) {
+  if (!requireLoginAsync('登录后为村社升级,做出你的贡献哦'))
+    return;
+  if (!choosedLevel.value)
+    throw new Error('请选择购买套餐');
+  try {
+    uni.showLoading({ title: '创建订单中...' });
+    const { order: orderInfo, pay: payInfo } = await OfficialApi.payAiPackage(
+      querys.value.villageId,
+      choosedLevel.value.id,
+      payType,
+    );
+    if (payType === 1) {
+      if (!payInfo) 
+        throw new Error('支付信息不存在');
+      uni.requestPayment({
+        provider: 'wxpay',
+        appId: payInfo.appId,
+        timeStamp: payInfo.timeStamp,
+        nonceStr: payInfo.nonceStr,
+        package: payInfo.package,
+        signType: payInfo.signType,
+        paySign: payInfo.paySign,
+        success: () => {
+          handlePaySuccess();
+        },
+        fail: (err) => {
+          showError(`支付失败: ${err.errMsg}`);
+        },
+      });
+    } else if (payType === 3) { 
+      handlePaySuccess();
+    }
+  } catch (e) {
+    showError(e);
+  } finally {
+    uni.hideLoading();
+  }
+}
+
+defineExpose({
+  onPageBack(name: string, data: any) {
+    if (name === 'handlePaySuccess') {
+      handlePaySuccess();
+    }
+  },
+});
+</script>