菜单搜索,用扣子API试试

1,090 阅读7分钟

背景

最近写了一篇文章突然上了热门,让我感到挺意外。针对那个46万行的超级系统,文章很多技术细节被我轻描淡写了,其实只是个人,系统治理的经验分享。

前面提到了,系统有几百个菜单,通过肌肉记忆,是很难记住的,如果你对业务不了解,找菜单也会显得笨手笨脚,因为下拉框的过滤需要精准匹配。今天借助于扣子API来实现找菜单功能,输入一句话:帮我打开订单列表,直接理解语义,打开订单列表页面。

这篇文章只是突发奇想,能否落地值得商榷,望大侠原谅。

方案

常规实现

使用vite+vue3简单搭建一个页面,在顶部增加搜索功能,把菜单数据组装成下拉数组,开启下拉组件的过滤属性。

image.png

image.png

这种方式有一个弊端,当菜单很多时,如果你输入的名称没有精准包含进去,就无法过滤出来,比如:订单列表写成订单页面,数组就会变成空,因为它其实通过indexOf来过滤,当菜单有几百个时,只能凭记忆来搜索名称。

image.png

右侧下拉框是常规的数据过滤,左侧下拉框是通过remote调用扣子API实现。

扣子实现

扣子平台创建一个Bot,添加知识库,发布,最后通过Bot API来调用获取菜单路径,从而实现菜单跳转。

API调用

1. 打开扣子平台。
2. 创建Bot
3. 进入扣子编排页面。

image.png

4. 添加文本知识库。
  • 点击【知识】下面的文本右侧的加号。
  • 在选择知识库弹窗里面,点击【创建知识库】。
  • 选择文本格式、导入类型选择自定义。

image.png

  • 填写菜单名称和菜单路径
名称:工作台  
路径:/workplace  
  
名称:数据看板  
路径:/dashboard  
  
名称:用户列表  
路径:/userList  
  
名称:字典列表  
路径:/dictList  
  
名称:图表1  
路径:/chart1  
  
名称:图表2  
路径:/chart2  
  
名称:部门列表  
路径:/deptList  
  
名称:待办列表  
路径:/todoList  
  
名称:库存列表  
路径:/stockList  
  
名称:订单列表  
路径:/orderList  
  
名称:城市列表  
路径:/cityList

上面为Demo数据,其实没有固定格式,你也可以用表格、txt等各种格式的数据,只要能达到最终目的即可。

  • 最终应该是这样的:

image.png

5. 添加Bot人设和回复逻辑
你是一名数据查找助手,根据用户输入的名称,找到对应的路径,比如:
1.  打开用户管理,返回:/userList。
2. 用户xxx管理,返回:/userList。
3. 我要打开用户管理菜单,返回:/userList。
4. 我要找用户管理页面,返回:/userList。
5. 打开xxx页面、找到xxx页面、打开xxx菜单、找到xxx菜单等,其实就是为了查找名称为xxx的路径。比如:打开用户页面,就是查找名称等于用户列表或者用户管理对应的路径,/userList。
6. xxx菜单,其实就是查找名称为xxx的路径,比如:看板菜单,对应的就是:/dashboard。

# 要求
1. 如果没有找到,直接返回:暂无数据。
2. 如果找到多个,则直接返回第一个。
3. 回复的时候,请不要说:xxx对应的路径是:/xxx。 请直接回复确定的答案,比如:/userList。

大家可以自己控制机器人的回复逻辑。

6. 在右侧进行调试
  • 比如:输入打开订单页面

image.png

  • 语义很模糊

image.png

  • 当查找到多条数据时,返回第一条

image.png

7. 点击右上角的发布
8. 发布时,勾选最下面的Bot APIWeb SDK

image.png

Bot as API 需要点击配置,创建一个【个人访问令牌】,生成的密钥要保存,因为密钥无法二次查看。

9. 发布成功后,会进入到下面这个界面

image.png

10. API 集成

设置下拉框为远程搜索,根据文档,先调用创建对话接口,再轮询调用详情接口。

image.png

  • 局部代码:
<el-select
  v-model="search"
  placeholder="请选择菜单"
  filterable
  remote
  :remote-method="fetchData"
  :loading="loading"
  style="width: 240px"
  @change="handleChange"
>
  <el-option
    v-for="item in options"
    :key="item.value"
    :label="item.name"
    :value="item.link"
  />
</el-select>
<script setup>
import { ref } from "vue";
const search = ref("");
const options = ref([]);
const T = ref(null);
const fetchData = (query) => {
  if (!query) return;
  if (T.value) {
    clearTimeout(T.value);
    T.value = setTimeout(() => {
      // TODO 
    }, 500);
  } else {
    T.value = setTimeout(() => {
      // TODO 
    }, 500);
  }
};
</script>

当输入内容时,加一个防抖函数,避免接口调用频繁。接着调用对应接口。

image.png

  • 调用创建对话接口
const chat = (query) => {
  let url = "https://api.coze.cn/v3/chat";
  loading.value = true;
  fetch(url, {
    headers: {
      Authorization:
        "Bearer xxx",
      "Content-Type": "application/json",
    },
    method: "POST",
    mode: "cors",
    body: JSON.stringify({
      bot_id: "7394754487971938354",
      user_id: "1024",
      stream: false,
      auto_save_history: true,
      additional_messages: [
        {
          role: "user",
          content: query,
          content_type: "text",
        },
      ],
    }),
  })
    .then((response) => {
      return response.json();
    })
    .then((res) => {
      if (res.data.status === "in_progress") {
        setTimeout(() => {
          chatDetail(res.data.conversation_id, res.data.id);
        }, 1000);
      }
    }).catch(()=>{
      loading.value = false;
    });
};

注意:Bearer 后面需要添加个人令牌,上面有提到过。如果返回的status字段为in_progress,需要继续调用对话详情接口,拿到最终结果。另外调用详情接口的延迟时间不要太短,可能会有坑。

  • 调用详情接口
const chatDetail = (conversation_id, chat_id) => {
  let url =
    "https://api.coze.cn/v3/chat/message/list?conversation_id=" +
    conversation_id +
    "&chat_id=" +
    chat_id;
  fetch(url, {
    headers: {
      Authorization:
        "Bearer pat_435qwjg0WC2t4UuU4kZxiIeII0wI79inv1pgiomJ1qMiM9GWlzPPTKRG7QorfS7o",
      "Content-Type": "application/json",
    },
    method: "POST",
    mode: "cors",
  })
    .then((response) => {
      return response.json();
    })
    .then((res) => {
      if (res.data) {
        console.log(res.data);
        const item = res.data.filter((item) => item.type === "answer")?.[0];
        if (item) {
          // this.searchResult = item.content;
          if (item.content.trim() != "暂无数据") {
            const path = item.content.trim();
            options.value = [{ name: path, link: path }];
          } else {
            options.value = [];
          }
          loading.value = false;
        } else {
          setTimeout(() => {
            chatDetail(conversation_id, chat_id);
          }, 1000);
        }
      } else {
        setTimeout(() => {
          chatDetail(conversation_id, chat_id);
        }, 1000);
      }
    });
};

注意:接收会话ID和chatId作为参数,对结果进行过滤,如果发现有type='answer'说明已经开始拿到返回结果了,如果没有answer,继续轮询调用,同样添加延迟时间。 这里面我发现有一个问题,有的情况下,即使拿到answer也不是最终结果,因为它其实是流式响应,我们其实并没有开启stream,所以建议把延迟时间放大,尽量拿到最终结果。

拿到以后,保存到options中,作为下拉框的数据源。

  • 测试验证

image.png

image.png

点击会直接打开工作台页面。

image.png

image.png

用自然的交流方式,同样可以找到订单列表页面,点击实现跳转。

SDK 接入

除了API调用方式,我们也可以选择SDK接入,但是SDK接入,一般的场景可能就是机器人客服、在线问答等,对于找菜单可能作用不是很明显,因为他返回的路径你没办法直接跳转,只能手动复制,然后输入到url后,才能打开对应页面。

  1. 在发布成功页面,点击Web SDK的安装按钮。

image.png

image.png

  1. 把复制的代码放到index.htmlbody中。
<body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
    <script src="https://lf-cdn.coze.cn/obj/unpkg/flow-platform/chat-app-sdk/0.1.0-beta.5/libs/cn/index.js"></script>
    <script>
      new CozeWebSDK.WebChatClient({
        config: {
          bot_id: "7394754487971938354",
        },
        componentProps: {
          title: "Coze",
        },
      });
    </script>
  </body>
  1. 刷新页面

image.png

右下角会生成一个按钮,点击打开聊天窗口。

  1. 输入:打开工作台

image.png

它会返回:/workplace ,但是无法跟页面交互,需要手动复制才能打开,所以聊天窗口不适合当前业务场景。

总结

其实菜单搜索,就用常规的下拉过滤就足够了,今天只是突发奇想,用扣子API来尝试实现,打开思路,会有不一样的收获。尽管这样做,我感觉意义不大,但是经过这样的尝试,让我对扣子的使用有了进一步的了解,如果不尝试,可能我还是一知半解。

之前看到前端大佬 前端小付 有一篇文章也是关于语义理解的,写的很不错,他用的是 typechat做的,因为我没有openai账号,所以,我就用扣子的知识库插件实现了。

感谢各位客官倾听,我是河畔一角,一名普通的前端。