阵容推荐网站地址:https://lol.qq.com/tft/#/index

数据查询网站地址:https://tactics.tools/zh

上大学的时候,特别喜欢玩英雄联盟,现在随着年龄越来越大,手速和反应慢了很多,白银局打的都费劲,也就不想玩了。后面英雄联盟出了一个云顶之弈,这个模式不需要手速和反应,我偶尔玩一下。

我段位很低,常年在铂金段位。因为工作和家庭的原因,没有时间去研究阵容,凭着自己的理解乱玩。

有次在看直播的时候,看见主播在一个网站查询数据。某个英雄带哪个装备胜率最高。我得知有这么一个网站,然后我自己玩的时候,不知道给英雄什么装备,或不知道选什么强化的时候,就跑到这个网站查询。但是这个网站很卡,打开一次要很久,查询方式对新人不太友好,所以我打算用coze做一个辅助工具,只需要用户简单的输入,就能获取到自己想要的信息。

云顶之弈助手做完后,我自己体验了一下,轻轻松松上到了一区砖石,最高砖一44胜点,本来差一点上大师,那把之后运气就变差了,D不到自己想要的牌,连续几把没吃分,开始摆烂了,现在掉到了砖三了。

下面和大家分享一下这个助手,感兴趣的可以去体验一下。

概述

扣子是新一代 AI 应用开发平台。无论你是否有编程基础,都可以在扣子上快速搭建基于大模型的各类 Bot,并将 Bot 发布到各个社交平台、通讯软件或部署到网站等其他渠道。

功能与优势

无限拓展的能力集

扣子集成了丰富的插件工具,可以极大地拓展 Bot 的能力边界。

丰富的数据源

扣子提供了简单易用的知识库功能来管理和存储数据,支持 Bot 与你自己的数据进行交互。无论是内容量巨大的本地文件还是某个网站的实时信息,都可以上传到知识库中。这样,Bot 就可以使用知识库中的内容回答问题了。

持久化的记忆能力

扣子提供了方便 AI 交互的数据库记忆能力,可持久记住用户对话的重要参数或内容。

例如,创建一个数据库来记录阅读笔记,包括书名、阅读进度和个人注释。有了数据库,Bot 就可以通过查询数据库中的数据来提供更准确的答案。

灵活的工作流设计

扣子的工作流功能可以用来处理逻辑复杂,且有较高稳定性要求的任务流。扣子提供了大量灵活可组合的节点包括大语言模型 LLM、自定义代码、判断逻辑等,无论你是否有编程基础,都可以通过拖拉拽的方式快速搭建一个工作流,例如:

使用体验

在做插件的过程中,遇到了很多问题,感觉Coze还有很长的路要走,后面会和大家分享一下我遇到的坑。

阵容推荐

当用户输入阵容推荐或与阵容相关的内容时,会为用户推荐最热门的阵容。这里的热门阵容本来打算使用tactics网站里的胜率高的阵容,但是tactics里的数据都是国外的,国内环境和国外环境不一样,所以我这里使用的是官网的推荐阵容。

image.png

点击查看详情按钮,可以查看阵容详情。

数据查询

输入棋子名称或装备名称或强化名称会根据吃鸡率、前4率、平均排名为你推荐与之最搭配的强化、阵容、装备、羁绊。

image.png

image.png

image.png

这里输入的名称,可以少输一些字,但是不能有错字。

image.png

大家可以在bot商店里搜索**「云顶之弈助手」**体验插件。

image.png

人设与回复逻辑

image.png

告诉coze根据用户不同的输入调用不同的工作流。

这里我写了两个工作流,一个是查询热门阵容,另外一个用来查询棋子、装备、强化信息。

热门阵容

热门阵容工作流

image.png

热门阵容工作流里主要调用了云顶热门阵容插件(yunding_hot_list),然后把插件里返回的数据返回出去,这里有个需要注意的地方,我被卡了一段时间,还是群里的大佬帮助我解决的,结束节点这里,如果你想使用卡片展示用户问题答案,结束节点这里最好设置为**「使用设定内容指定回答」**,

image.png

如果使用默认回答模式,会输出乱七八糟的东西。

image.png

热门阵容插件

工作流里支持调用插件,我们写一个查询云顶热门阵容的插件在工作流中使用。

插件这里开发语言我选择nodejs,调用云顶官网接口查询数据,官网地址:https://lol.qq.com/tft/#/index。

image.png

插件完整代码

``import { Output } from "@/typings/yunding_hot_list/yunding_hot_list";
import { unescape } from 'querystring';
/**
  * Each file needs to export a function named handler. This function is the entrance to the Tool.
  * @param {Object} args.input - input parameters, you can get test input value by input.xxx.
  * @param {Object} args.logger - logger instance used to print logs, injected by runtime
  * @returns {*} The return data of the function, which should match the declared output parameters.
  * 
  * Remember to fill in input/output in Metadata, it helps LLM to recognize and use tool.
  */
export async function handler(): Promise {

  // 获取棋子数据
  async function getChessMap() {
    const { data } = await fetch('https://game.gtimg.cn/images/lol/act/img/tft/js/chess.js').then(res => res.json());

    return data.reduce((prev, cur) => {
      prev[cur.chessId] = cur;
      return prev;
    }, {});
  }

  // 获取羁绊数据
  async function getHexMap() {
    const { data } = await fetch('https://game.gtimg.cn/images/lol/act/img/tft/js/hex.js').then(res => res.json());
    return Object.values(data).reduce((prev, cur: any) => {
      prev[cur.hexId] = cur;
      return prev;
    }, {});
  }

  const [chessMap, hexMap] = await Promise.all([
    getChessMap(),
    getHexMap(),
  ]);

  // 查询热门阵容
  let data: any = await fetch('https://game.gtimg.cn/images/lol/act/tftzlkauto/json/lineupJson/s11/6/lineup_detail_total.json')
    .then(res => res.text());

  data = JSON.parse(unescape(data.replace(/\n/g, '')));

  data.lineup_list.forEach(item => {
    item.detail = JSON.parse(item.detail);
  });

  // 格式化数据输出
  const formatedData = (data.lineup_list.map((item, index) => {
    return {
      rank: index + 1,
      name: item.detail.line_name,
      image: 'https://www.fluxyadmin.cn/file/yunding/1001.jpeg',
      chesses: item.detail.hero_location.map(item => chessMap[item.hero_id]?.displayName).filter(o => o).join(','),
      hexes: item.detail.hexbuff.recomm.split(',').map(item => hexMap[item]?.name).filter(o => o).join(','),
      races: item.detail.contact.toSorted((x, y) => y.num - x.num).map(item => ${item.num}${item.name}).filter(o => o).join(' '),
      hex_info: item.detail.hex_info,
      early_info: item.detail.early_info,
      d_time: item.detail.d_time,
      equipment_info: item.detail.equipment_info,
      location_info: item.detail.location_info,
      location_info_2: item.detail.location_info_2,
      enemy_info: item.detail.enemy_info,
      url: https://lol.qq.com/tft/#/lineupDetail/${item.id}/1/detail,
    }
  }))

  return {
    list: formatedData,
  };
};

``

设置插件的输入输出参数

image.png

自定义回答卡片

如果不想使用文字回答,可以使用自定义卡片形式展示答案。

给工作流绑定卡片

image.png

可以使用官方的卡片,也可以自定义,这里官方没有合适的,我们自定义一个。

先创建一个变量,默认值把插件返回的数据放进去。

image.png

image.png

剩下的就像一些低代码平台一样,可以通过拖拉拽配置卡片内容。

image.png

可以通过配置循环渲染,展示列表

image.png

文本内容可以绑定变量

image.png

为卡片绑定变量,绑定变量的我发现了个bug,好像只支持string类型的数据,其他类型数据即使工作流返回的数据类型和卡片需要的类型一样,也选不到。

image.png

效果展示

image.png

棋子、强化、装备信息查询

和上面热门阵容的配置差不多,也是在工作流中调用一个插件返回数据,然后自定义卡片展示。

这里主要介绍一下插件,最开始从网站获取数据在一块代码是写在插件里的,但是查询数据这几个接口,太慢了,所以我自己写了一个接口,给查询的数据做一个缓存,只要当前信息被查过一次,后面再查直接就从我们自己数据库中取就行了。

每天晚上12点,把数据给清掉,第二天所有数据重新查询,保证数据实时性。

数据来源网站:https://tactics.tools

对外提供接口的框架,使用的是midway node框架,我以前写过一篇midway入门文章。

因为棋子、装备、强化名称是固定的,所以我们先写一个脚本把这些数据做成静态文件,就不用每次都去请求这些数据了。

image.png

image.png

然后写接口,数据库使用的是mongoosedb。

`import { Controller, Get, Inject, Query } from '@midwayjs/core';
import { DataService } from '../service/chess.service';
import chess from '../data/chess.json';
import items from '../data/items.json';
import augments from '../data/augments.json';
@controller('/api')
export class APIController {
  @Inject()
  dataService: DataService;

  @get('/data')
  async getData(@query('name') name: string) {

    // 判断用户输入的名称是棋子还是物品还是强化名称
    const chessName = Object.keys(chess).find(o => o.includes(name));

    if (chessName) {
      return this.dataService.getChessByName(chessName);
    }

    const itemName = Object.keys(items).find(o => o.includes(name));

    if (itemName) {
      return this.dataService.getItemByName(itemName);
    }

    const augmentName = Object.keys(augments).find(o => o.includes(name));

    if (augmentName) {
      return this.dataService.getAugmentByName(augmentName);
    }
  }
}

`

service中调用tactics网站接口查询数据,下面以查询棋子信息为例。

`` async getChessInfo(name: string) {
    const code = Chess[name];

    if (!code) return { name: 'error' };

    // 查询数据库,如果存在则直接返回
    let info: Data = await this.dataModel.findOne({ code });

    if (info) {
      return info;
    }

    const data = (await axios(
      https://d2.tft.tools/stats2/unit/1100/${code}/14101/1
    ).then(res => res.data)) as any;

    info = {
      icon: https://ap.tft.tools/img/face/${data.unitId}.jpg,
    } as Data;

    info.count =
      data.base.count > 1000
        ? (data.base.count / 1000).toFixed(2) + 'k'
        : data.base.count;

    info.rate = data.base.rate.toFixed(2) + '%';
    info.won = data.base.won + '%';
    info.top4 = data.base.top4 + '%';
    info.place = data.base.place;
    info.name = zh.s11Unitsi18n[data.unitId];

    // 计算平均排名,然后取前5名强化
    info.augments = this.getAverageRanking(data.augments)
      .slice(0, 5)
      .map(cur => {
        return ${zh.s11Augmentsi18n[cur.id]}.replace(/ /g, '');
      })
      .join(' ');

    // 计算平均排名,然后取前5名装备
    info.items = this.getAverageRanking(
      data.items.map(item => ({ ...item, id: item.items }))
    )
      .slice(0, 5)
      .map(item => {
        return ${zh.s11Itemsi18n[item.items]};
      })
      .join(' ');

    // 计算平均排名,然后取前8名单位
    info.units = [
      ...this.getAverageRanking(data.units)
        .slice(0, 8)
        .map(item => ${zh.s11Unitsi18n[item.id]}),
      name,
    ].join(' ');

    // 计算平均排名,然后取前10名羁绊
    info.traits = this.getAverageRanking(
      data.traits.map(item => ({ ...item, id: ${item.id[0]}-${item.id[1]} }))
    )
      .slice(0, 10)
      .map(item => {
        const [id] = item.id.split('-');
        return ${zh.s11Traitsi18n[id]};
      })
      .join(' ');

    info.code = code;
    info.type = '棋子';

    this.dataModel.create(info);

    return info;
  }

``

再写一个定时任务,每天晚上12点清除数据。

`import { Inject } from '@midwayjs/core';
import { Job, IJob } from '@midwayjs/cron';
import { DataService } from '../service/chess.service';

@job({
  cronTime: '0 0 0 * * *',
  start: true,
})
export class DataClearJob implements IJob {
  @Inject()
  dataService: DataService;

  async onTick() {
    this.dataService.clear();
  }
}

`

写完接口中,部署到服务器,然后在插件中调用接口。

image.png

image.png

本来我还做了一个功能,把所有棋子的信息爬下来,然后上传到coze的知识库,试了一下发现coze知识库回答的不稳定。

我准备的测试知识库数据

image.png

这是我的问题和它的回答,实际上大卖特卖的吃鸡率比小伙伴高。

image.png

可能是我用法不对,我再研究研究,作为后续计划吧。

截止到这篇文章发完,我还是没上大师,五一期间玩的有点多,影响了家庭和谐,后面就没再玩了,有点遗憾,没有圆大师梦。

希望这个插件能帮助到大家,让大家上上分。

阵容推荐网站地址:https://lol.qq.com/tft/#/index

数据查询网站地址:https://tactics.tools/zh

bot ID: 7355799683123609641