From f028bea0c5ec025ec01a8e8a9388b929e88fa975 Mon Sep 17 00:00:00 2001 From: "quemingyi.wudong" Date: Wed, 16 Apr 2025 20:47:16 +0800 Subject: [PATCH] feat: support coze bot & update comments & fix some ui bug. --- .eslintrc | 2 +- README.md | 30 +- Server/app.js | 9 +- package.json | 2 +- src/components/AISettings/index.module.less | 7 + src/components/AISettings/index.tsx | 301 ++++++++++++++------ src/components/AvatarCard/index.tsx | 17 +- src/config/common.ts | 8 +- src/config/config.ts | 33 ++- src/config/index.ts | 1 - src/store/slices/room.ts | 11 +- 11 files changed, 307 insertions(+), 114 deletions(-) diff --git a/.eslintrc b/.eslintrc index f13c040..80b12c9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -41,7 +41,7 @@ } ], "rules": { - "prettier/prettier": ["warn", { "trailingComma": "es5", "printWidth": 200 }], + "prettier/prettier": ["warn", { "trailingComma": "es5", "printWidth": 100 }], "linebreak-style": "off", "no-console": ["warn", { "allow": ["warn", "error", "log"] }], "no-case-declarations": 0, diff --git a/README.md b/README.md index 0e89c2d..f5913d5 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ ## 【必看】环境准备 - **Node 版本: 16.0+** 1. 需要准备两个 Terminal,分别启动服务端、前端页面。 -2. 开通 ASR、TTS、LLM、RTC 等服务,可通过 [无代码跑通实时对话式](https://console.volcengine.com/rtc/guide) 快速开通服务, 点击 **快速开始** 中的 **跑通 Demo** 进行服务开通。 +2. 开通 ASR、TTS、LLM、RTC 等服务,可参考 [开通服务](https://www.volcengine.com/docs/6348/1315561?s=g) 进行相关服务的授权与开通。 3. **根据你自定义的 RoomId、UserId 以及申请的 AppID、BusinessID(如有)、Token、ASR AppID、TTS AppID,修改 `src/config/config.ts` 文件中 `ConfigFactory` 中 `BaseConfig` 的配置信息**。 -4. 使用火山引擎控制台账号的 [AK、SK](https://console.volcengine.com/iam/keymanage?s=g)、[SessionToken](https://www.volcengine.com/docs/6348/1315561#sub?s=g)(临时token, 子账号才需要), 修改 `Server/app.js` 文件中的 `ACCOUNT_INFO`。 -5. 您需要在 [火山方舟-在线推理](https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint?config=%7B%7D&s=g) 中创建接入点, 并将模型对应的接入点 ID 填入 `src/config/common.ts` 文件中的 `ARK_V3_MODEL_ID`, 否则无法正常启动智能体。 +4. 使用火山引擎控制台账号的 [AK、SK](https://console.volcengine.com/iam/keymanage?s=g), 修改 `Server/app.js` 文件中的 `ACCOUNT_INFO`。 +5. 若您使用的是官方模型, 需要在 [火山方舟-在线推理](https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint?config=%7B%7D&s=g) 中创建接入点, 并将模型对应的接入点 ID 填入 `src/config/common.ts` 文件中的 `ARK_V3_MODEL_ID`, 否则无法正常启动智能体。 6. 如果您已经自行完成了服务端的逻辑,可以不依赖 Demo 中的 Server,直接修改前端代码文件 `src/config/index.ts` 中的 `AIGC_PROXY_HOST` 请求域名和接口,并在 `src/app/api.ts` 中修改接口的参数配置 `APIS_CONFIG`。 ## 快速开始 @@ -43,12 +43,12 @@ yarn dev ### 常见问题 | 问题 | 解决方案 | | :-- | :-- | -| **启动智能体之后, 对话无反馈,或者一直停留在 "AI 准备中, 请稍侯"** |
  • 可能因为控制台中相关权限没有正常授予,请参考[流程](https://www.volcengine.com/docs/6348/1315561?s=g)再次确认下是否完成相关操作。此问题的可能性较大,建议仔细对照是否已经将相应的权限开通。
  • 参数传递可能有问题, 例如参数大小写、类型等问题,请再次确认下这类型问题是否存在。
  • 相关资源可能未开通或者用量不足,请再次确认。
  • **请检查当前使用的模型 ID 等内容都是正确且可用的。**
  • | -| `Server/app.js` 中的 `sessionToken` 是什么,该怎么填,为什么要填 | `sessionToken` 是火山引擎子账号发起 OpenAPI 请求时所必须携带的临时 Token,获取方式可参考 [此文章末尾](https://www.volcengine.com/docs/6348/1315561?s=g)。 | -| **浏览器报了 `Uncaught (in promise) r: token_error` 错误** | 请检查您填在项目中的 RTC Token 是否合法,检测用于生成 Token 的 UserId、RoomId 是否与项目中填写的一致;或者 Token 可能过期, 可尝试重新生成下。 | +| 如何使用第三方模型、Coze Bot | 点击页面上的 "修改 AI 设定" 进入配置页,可切换 官方模型/Coze/第三方模型,填写对应参数即可,相关代码对应 `src/components/AISettings/index.tsx` 文件。 | +| **启动智能体之后, 对话无反馈,或者一直停留在 "AI 准备中, 请稍侯"** |
  • 可能因为控制台中相关权限没有正常授予,请参考[流程](https://www.volcengine.com/docs/6348/1315561?s=g)再次确认下是否完成相关操作。此问题的可能性较大,建议仔细对照是否已经将相应的权限开通。
  • 参数传递可能有问题, 例如参数大小写、类型等问题,请再次确认下这类型问题是否存在。
  • 相关资源可能未开通或者用量不足/欠费,请再次确认。
  • **请检查当前使用的模型 ID 等内容都是正确且可用的。**
  • | +| **浏览器报了 `Uncaught (in promise) r: token_error` 错误** | 请检查您填在项目中的 RTC Token 是否合法,检测用于生成 Token 的 UserId、RoomId 以及 Token 本身是否与项目中填写的一致;或者 Token 可能过期, 可尝试重新生成下。 | | **[StartVoiceChat]Failed(Reason: The task has been started. Please do not call the startup task interface repeatedly.)** 报错 | 由于目前设置的 RoomId、UserId 为固定值,重复调用 startAudioBot 会导致出错,只需先调用 stopAudioBot 后再重新 startAudioBot 即可。 | | 为什么我的麦克风正常、摄像头也正常,但是设备没有正常工作? | 可能是设备权限未授予,详情可参考 [Web 排查设备权限获取失败问题](https://www.volcengine.com/docs/6348/1356355?s=g)。 | -| 接口调用时, 返回 "Invalid 'Authorization' header, Pls check your authorization header" 错误 | `Server/app.js` 中的 AK/SK/SessionToken 不正确 | +| 接口调用时, 返回 "Invalid 'Authorization' header, Pls check your authorization header" 错误 | `Server/app.js` 中的 AK/SK 不正确 | | 什么是 RTC | **R**eal **T**ime **C**ommunication, RTC 的概念可参考[官网文档](https://www.volcengine.com/docs/6348/66812?s=g)。 | | 不清楚什么是主账号,什么是子账号 | 可以参考[官方概念](https://www.volcengine.com/docs/6257/64963?hyperlink_open_type=lark.open_in_browser&s=g) 。| @@ -61,7 +61,21 @@ yarn dev ## 更新日志 -### [1.5.0] - [2025-03-31] +### OpenAPI 更新 +参考 [OpenAPI 更新](https://www.volcengine.com/docs/6348/116363?s=g) 中与 实时对话式 AI 相关的更新内容。 + +### Demo 更新 +#### [1.6.0] - [2025-04-16] +- 支持 Coze Bot +- 更新部分注释和文档内容 +- 删除子账号的 SessionToken 配置, 子账号调用无须 SessionToken +- 修复通话前修改内容,在通话后配置消失的问题 + +#### [1.5.1] - [2025-04-11] +- 移除无用代码和依赖 +- 修复字幕逻辑 + +#### [1.5.0] - [2025-03-31] - 修复部分 UI 问题 - 追加屏幕共享能力 (视觉模型可用,**读屏助手** 人设下可使用) - 修改字幕逻辑,避免字幕回调中标点符号、大小写不一致引起的字幕重复问题 diff --git a/Server/app.js b/Server/app.js index edc0939..ec9df62 100644 --- a/Server/app.js +++ b/Server/app.js @@ -27,11 +27,6 @@ const ACCOUNT_INFO = { * @notes 必填, 在 https://console.volcengine.com/iam/keymanage/ 获取 */ secretKey: 'Your SK', - /** - * @notes 非必填, 主账号无须传入, 子账号须传, 获取方式可参考 - * https://www.volcengine.com/docs/6348/1315561 中的 步骤 4-使用子账号调用智能体接口 一节 - */ - // sessionToken: 'Your SessionToken', } app.use(bodyParser()); @@ -67,9 +62,7 @@ app.use(async ctx => { /** 参考 https://www.volcengine.com/docs/6348/69828 可获取更多 OpenAPI 的信息 */ const result = await fetch(`https://rtc.volcengineapi.com?Action=${Action}&Version=${Version}`, { method: 'POST', - headers: { - ...openApiRequestData.headers, - }, + headers: openApiRequestData.headers, body: JSON.stringify(body), }); const volcResponse = await result.json(); diff --git a/package.json b/package.json index 247ece8..5bb6654 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aigc", - "version": "1.5.0", + "version": "1.6.0", "license": "BSD-3-Clause", "private": true, "dependencies": { diff --git a/src/components/AISettings/index.module.less b/src/components/AISettings/index.module.less index 61d7ce5..a440e79 100644 --- a/src/components/AISettings/index.module.less +++ b/src/components/AISettings/index.module.less @@ -91,6 +91,12 @@ flex-direction: column; gap: 36px; + .ai-settings-radio { + display: flex; + flex-direction: row; + justify-content: flex-end; + } + .anchor { position: absolute; border-bottom: 12px solid white; @@ -104,6 +110,7 @@ width: 100%; display: flex; flex-direction: row; + margin-top: -16px; gap: 24px; .ai-settings-wrapper { diff --git a/src/components/AISettings/index.tsx b/src/components/AISettings/index.tsx index 1cbbf27..4babc80 100644 --- a/src/components/AISettings/index.tsx +++ b/src/components/AISettings/index.tsx @@ -3,10 +3,10 @@ * SPDX-license-identifier: BSD-3-Clause */ -import { Button, Drawer, Input, Message } from '@arco-design/web-react'; +import { Button, Drawer, Input, Message, Radio, Tooltip } from '@arco-design/web-react'; import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { IconSwap } from '@arco-design/web-react/icon'; +import { IconExclamationCircle } from '@arco-design/web-react/icon'; import { StreamIndex } from '@volcengine/rtc'; import CheckIcon from '../CheckIcon'; import Config, { @@ -18,7 +18,7 @@ import Config, { Voice, Model, AI_MODEL, - ModelSourceType, + MODEL_MODE, VOICE_INFO_MAP, VOICE_TYPE, isVisionMode, @@ -26,7 +26,7 @@ import Config, { import TitleCard from '../TitleCard'; import CheckBoxSelector from '@/components/CheckBoxSelector'; import RtcClient from '@/lib/RtcClient'; -import { clearHistoryMsg, updateAIConfig, updateScene } from '@/store/slices/room'; +import { clearHistoryMsg, updateAIConfig, updateModelMode, updateScene } from '@/store/slices/room'; import { RootState } from '@/store'; import utils from '@/utils/utils'; import { useDeviceState } from '@/lib/useCommon'; @@ -42,6 +42,8 @@ export interface IAISettingsProps { onCancel?: () => void; } +const RadioGroup = Radio.Group; + const SCENES = [ SCENE.INTELLIGENT_ASSISTANT, SCENE.SCREEN_READER, @@ -59,17 +61,19 @@ function AISettings({ open, onCancel, onOk }: IAISettingsProps) { useDeviceState(); const room = useSelector((state: RootState) => state.room); const [loading, setLoading] = useState(false); - const [use3Part, setUse3Part] = useState(false); + const [modelMode, setModelMode] = useState(room.modelMode); const [scene, setScene] = useState(room.scene); const [data, setData] = useState({ - prompt: Prompt[scene], - welcome: Welcome[scene], - voice: Voice[scene], - model: Model[scene], + prompt: Config.Prompt || Prompt[scene], + welcome: Config.WelcomeSpeech || Welcome[scene], + voice: Config.VoiceType || Voice[scene], + model: Config.Model || Model[scene], - Url: '', - APIKey: '', - customModelName: '', + Url: Config.Url || '', + APIKey: Config.APIKey || '', + customModelName: (Config.Model || '') as string, + + BotID: Config.BotID || '', }); const handleVoiceTypeChanged = (key: string) => { @@ -90,35 +94,55 @@ function AISettings({ open, onCancel, onOk }: IAISettingsProps) { })); }; - const handleUseThirdPart = () => { - setUse3Part(!use3Part); - Config.ModeSourceType = use3Part ? ModelSourceType.Custom : ModelSourceType.Available; + const handleUseThirdPart = (val: MODEL_MODE) => { + setModelMode(val); + Config.ModeSourceType = val; }; const handleUpdateConfig = async () => { dispatch(updateScene({ scene })); - if (use3Part) { - if (!data.Url) { - Message.error('请输入正确的第三方模型地址'); - return; - } - if (!data.Url.startsWith('http://') && !data.Url.startsWith('https://')) { - Message.error('第三方模型请求地址格式不正确, 请以 http:// 或 https:// 为开头'); - return; - } - Config.Url = data.Url; - Config.APIKey = data.APIKey; - Config.ModeSourceType = ModelSourceType.Custom; - } else { - Config.Url = undefined; - Config.APIKey = undefined; - Config.ModeSourceType = ModelSourceType.Available; + Config.ModeSourceType = modelMode; + switch (modelMode) { + case MODEL_MODE.ORIGINAL: + Config.Url = undefined; + Config.APIKey = undefined; + break; + case MODEL_MODE.COZE: + if (!data.APIKey) { + Message.error('访问令牌必填'); + return; + } + if (!data.BotID) { + Message.error('智能体 ID 必填'); + return; + } + Config.APIKey = data.APIKey; + Config.BotID = data.BotID; + break; + case MODEL_MODE.VENDOR: + if (!data.Url) { + Message.error('请输入正确的第三方模型地址'); + return; + } + if (!data.Url.startsWith('http://') && !data.Url.startsWith('https://')) { + Message.error('第三方模型请求地址格式不正确, 请以 http:// 或 https:// 为开头'); + return; + } + Config.Url = data.Url; + Config.APIKey = data.APIKey; + break; + default: + break; } setLoading(true); - Config.Model = use3Part ? (data.customModelName as AI_MODEL) : (data.model as AI_MODEL); + Config.Model = + modelMode === MODEL_MODE.VENDOR + ? (data.customModelName as AI_MODEL) + : (data.model as AI_MODEL); Config.Prompt = data.prompt; Config.VoiceType = data.voice; Config.WelcomeSpeech = data.welcome; + dispatch(updateModelMode(modelMode)); dispatch(updateAIConfig(Config.aigcConfig)); if (isVisionMode(data.model)) { @@ -218,32 +242,85 @@ function AISettings({ open, onCancel, onOk }: IAISettingsProps) { }} /> )} - - { - setData((prev) => ({ - ...prev, - prompt: val, - })); - }} - placeholder="请输入你需要的 Prompt 设定" - /> - - - { - setData((prev) => ({ - ...prev, - welcome: val, - })); - }} - placeholder="请输入欢迎语" - /> - + + Coze + + 访问令牌可参考{' '} + + 添加个人访问令牌 + {' '} + 获取。 +
    + 智能体 ID 可参考{' '} + + 发送请求 + {' '} + 获取。 +
    + 请注意智能体发布时须勾选 API 调用能力,否则无法成功对话。 + + } + > + +
    + + ), + }, + { + value: MODEL_MODE.VENDOR, + label: ( +
    + 第三方模型 + + 如第三方模型使用失败, 可前往{' '} + + 第三方模型接口验证工具 + {' '} + 下载工具定位原因。 +
    + } + > + + + + ), + }, + ]} + value={modelMode} + size="mini" + type="button" + defaultValue="Beijing" + className={styles['ai-settings-radio']} + onChange={handleUseThirdPart} + />
    - {use3Part ? ( + {modelMode === MODEL_MODE.ORIGINAL && ( + + ({ + key: AI_MODEL[type as keyof typeof AI_MODEL], + label: type.replaceAll('_', ' '), + icon: DoubaoModelSVG, + }))} + moreIcon={ModelChangeSVG} + moreText="更换模型" + placeHolder="请选择你需要的模型" + onChange={(key) => { + setData((prev) => ({ + ...prev, + model: key as AI_MODEL, + })); + }} + value={data.model} + /> + + )} + {modelMode === MODEL_MODE.VENDOR && ( <> - ) : ( - - ({ - key: AI_MODEL[type as keyof typeof AI_MODEL], - label: type.replaceAll('_', ' '), - icon: DoubaoModelSVG, - }))} - moreIcon={ModelChangeSVG} - moreText="更换模型" - placeHolder="请选择你需要的模型" - onChange={(key) => { - setData((prev) => ({ - ...prev, - model: key as AI_MODEL, - })); - }} - value={data.model} - /> - )} - - + {modelMode === MODEL_MODE.COZE && ( + <> + + + + + { + setData((prev) => ({ + ...prev, + APIKey: val, + })); + }} + placeholder="请输入访问令牌" + /> + + + { + setData((prev) => ({ + ...prev, + BotID: val, + })); + }} + placeholder="请输入智能体 ID" + /> + + + )}
    + + { + setData((prev) => ({ + ...prev, + prompt: val, + })); + }} + placeholder="请输入你需要的 Prompt 设定" + /> + + + { + setData((prev) => ({ + ...prev, + welcome: val, + })); + }} + placeholder="请输入欢迎语" + /> + ); diff --git a/src/components/AvatarCard/index.tsx b/src/components/AvatarCard/index.tsx index f30772a..f1b6f21 100644 --- a/src/components/AvatarCard/index.tsx +++ b/src/components/AvatarCard/index.tsx @@ -10,7 +10,7 @@ import AISettings from '../AISettings'; import style from './index.module.less'; import DouBaoAvatar from '@/assets/img/DoubaoAvatarGIF.webp'; import { RootState } from '@/store'; -import { Name, VOICE_TYPE } from '@/config'; +import { MODEL_MODE, Name, VOICE_TYPE } from '@/config'; interface IAvatarCardProps extends React.HTMLAttributes { avatar?: string; @@ -24,11 +24,16 @@ const ReversedVoiceType = Object.entries(VOICE_TYPE).reduce state.room); + const { scene, aiConfig, modelMode } = room; const [open, setOpen] = useState(false); - const scene = room.scene; - const { LLMConfig, TTSConfig } = room.aiConfig.Config || {}; + const { LLMConfig, TTSConfig } = aiConfig.Config || {}; const { avatar, className, ...rest } = props; const voice = TTSConfig.ProviderParams.audio.voice_type; @@ -51,7 +56,11 @@ function AvatarCard(props: IAvatarCardProps) {
    {Name[scene]}
    声源来自 {ReversedVoiceType[voice || '']}
    -
    模型 {LLMConfig.ModelName}
    +
    + {modelMode === MODEL_MODE.ORIGINAL + ? `模型 ${LLMConfig.ModelName}` + : `模型来源 ${SourceName[modelMode]}`} +