1. 引言
最近深度体验了一下 Coze国内版(扣子) ,能耍的基本都玩过了,踩了不少坑,顺带写了两篇文章:
看到群里有人吐槽 国内版插件比国际版少太多,🤔 我突然萌生了一丝疑问:
目前国内版总共有多少个插件?都有什么插件?
是首页商店看到的这个 36个 吗?
是,它们叫 插件,但在 工作流 里,我们用到的是 插件工具!!!比如这个 文生图 的 插件:
它包含了三个 插件工具,前面讲过 插件和节点 的本质都是 API接口调用的封装,那么插件和插件工具的区别:
答:插件指定 →【BaseUrl + 默认请求头】,插件工具 指定 → 【调用接口、输入/输出参数】
🐶 所以,使用同一插件的不同插件工具,本质上就是调用 同一域名下的不同的接口。😄 好,修正下问题:
国内版总共有多少个插件工具?
😑 官方并没有说明,想知道得自己统计,在折腾工具流时,我发现 插件搜索 并没那么好用,比如,我想要用 文生图 的 插件,搜 "图":
明显是根据 插件名 来匹配的,支持文生图的 ByteArtist 插件就没显示出来。😳 另外,我之前好像看到官方的Bot的工作流用到了 隐藏插件,用户 没法直接搜到,但是可以通过 间接的方式来使用它:
打开官方Bot的工作流,点击右上角 创建副本 来拷贝工作流到自己的工作空间,就可以用它了。2333,之前云雀大模型抽风的时候,群里有人有人通过这样的方式拷贝了 更牛批的 Seed大模型。😄 不过现在不行了,默认只能用云雀了,如果想调其它大模型,可以在代码节点使用 request_async库 来调用第三方的API来实现。
😆 综上,笔者想做的事情就是:
- 统计能搜到插件工具,做下汇总输出,方便Bot玩家对内置插件工具进行检索,快速找到心仪的工具。
- 发掘下官方Bot的工作流中有没有隐藏的插件工具。
🤡 手动统计肯定是不可能的,爬虫走一波~
2. 爬取思路
😜 先调皮下,搭个Bot,看用 LinkReaderPlugin插件 能否直接提取,写下简易提示词:
试试:
🤣 哈哈哈,明显不行,需要登录,F12看下请求:
看到 msToken 这么长的加密参数,就不用想接口模拟了。数据量不大,浏览器自动化(selenium) + 请求拦截(BrowserMob Proxy) 走一波看看,随手写下爬取代码:
import os
import time
from browsermobproxy import Server
from selenium import webdriver
import cp_utils
coze_save_dir = os.path.join(os.getcwd(), "coze")
explore_list_file = os.path.join(coze_save_dir, "explore_list.txt")
def init_browser():
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('ignore-certificate-errors') # 无视证书验证
chrome_options.add_argument('--start-maximized') # 开始时直接最大屏幕
chrome_options.add_argument(r'--user-data-dir=D:\ChromeUserData') # 设置用户数据目录,避免每次登陆
chrome_options.add_argument('--proxy-server={0}'.format(proxy.proxy)) # 设置请求的代理
return webdriver.Chrome(options=chrome_options)
# 模拟登陆,只需调用一次,以后登陆失效了主动调用下就好
def login():
browser.get("https://www.coze.cn/home")
# 获取Bots列表
def get_explore_list():
proxy.new_har("get_explore_list", options={
'captureContent': True,
'captureHeaders': True
})
browser.get('https://www.coze.cn/explore')
time.sleep(5)
result = proxy.har
for entry in result['log']['entries']:
if entry['request']['url'].find('/explore/get_explore_list') != -1:
content = entry['response']['content']
print(content)
with open(explore_list_file, "w+", encoding='utf-8') as f:
f.write(content['text'])
print(explore_list_file, " 文件写入完毕...")
proxy.close()
browser.close()
if __name__ == '__main__':
cp_utils.is_dir_existed(coze_save_dir)
server = Server(os.path.join(os.getcwd(), r'browsermob-proxy-2.1.4\bin\browsermob-proxy'))
server.start()
proxy = server.create_proxy({'trustAllServers': True})
browser = init_browser()
# login()
get_explore_list()
先调login(),完成登录后关闭浏览器,注释掉,调用get_explore_list(),此时打开浏览器就处于登录态了,拦截包含 /explore/get_explore_list 字符串的URL,保存text到本地。运行后报错:
没有text,难道是 二进制数据?proxy.new_har的option参数再加个 'captureBinaryContent': True,运行:
🙃 淦,读是能读到了,但text这字符串明显就是 字符串加密 + Base64 啊,yue了,但打开 浏览器 数据确是正常的:
明显是执行某个解密js后加载出来的,破解就算了🙃,先不谈水平不够,浪费时间之余意义不大。
- 😑 那 自动化+Xpath抠页面节点 ?算了吧,又low又费劲,无计可施才会用这个方法。
- 🤔️感觉得搞个Chrome插件 做下接口监听了,但那是我的知识盲区...
- 🤨 说到Chrome插件,突然想到一个神级插件 → Tampermonkey(油猴/篡改猴)。会点js看下文档就能耍,简单瞄了下《油猴开发指南》,看了几个案例,加之这里的需求也比较简单:请求加载url、监听特定url的响应内容,保存数据到本地。
- 😏 感觉应该不复杂,而且有GPT加持,可以一试
- 😄 浏览器装下 油猴 插件,直接开始边写代码边测试~
2.1. 浏览器跳特定URL
直接设置下 window.location.href 的属性就好了,代码如下:
// ==UserScript==
// @name 扣子插件工具信息爬取
// @namespace http://tampermonkey.net/
// @version 2024-02-21
// @description try to take over the world!
// @author CoderPig
// @match https://www.coze.cn/*
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function() {
'use strict';
console.log('插件加载...')
// 浏览器跳转特定url
function locationUrl(url) {
window.location.href = url;
}
// 脚本加载后3s跳转掘金
setTimeout(function() {
locationUrl("https://juejin.cn/");
}, 3000);
}
运行后,控制台输出 插件加载中... 3s后自动跳转掘金。对了,如果是想发起一个请求不跳转,可以使用 Tampermonkey 提供的API → GM_xmlhttpRequest,用于发起跨域 HTTP(S) 请求。它的参数是一个对象,包含了 请求的各种设置。可选参数如下:
- method :HTTP 请求方法,如 "GET"、"POST" 等。
- url :请求的 URL。
- headers :一个包含 HTTP 请求头的对象,如:{ "Content-Type": "application/x-www-form-urlencoded" }。
- data :要发送的数据。只有在 method 为 "POST" 时才会使用。
- binary :如果为 true,responseText 将包含未处理的二进制数据。
- timeout :请求超时时间,单位为毫秒。如果在这段时间内没有收到响应,将触发 ontimeout 事件。
- context :一个可以在回调函数中使用的值,这个值会在调用回调函数时作为 this 参数传入。
- responseType :期望的响应类型。可能的值有 "arraybuffer"、"blob"、"json" 等。
- overrideMimeType :覆盖响应的 MIME 类型。
- anonymous :如果为 true,请求将不会发送 cookies 或 HTTP 认证信息。
然后是可选的 回调函数:
- onload:请求成功时调用的函数。
- onerror:请求失败时调用的函数。
- onreadystatechange:当 readyState 值改变时调用的函数。
- ontimeout:请求超时时调用的函数。
上述回调函数都会接收一个响应对象,包含下述属性:
- responseText:服务器的响应文本。
- responseXML:如果服务器的响应是一个 XML 文档,这个属性将包含该文档。
- response:服务器的响应。其类型由 responseType 参数决定。
- status:服务器的响应状态码。
- statusText:服务器的响应状态文本。
- readyState:请求的当前状态。可能的值有 0(未初始化)、1(已打开)、2(已发送头部)、3(正在接收响应)、4(已完成)。
- finalUrl:响应的最终 URL,考虑到可能的重定向。
一个发起请求,并在请求成功后,显示响应文本的简单代码示例如下:
// 浏览器发起请求
function sendRequest(url) {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
console.log(response.responseText);
}
});
}
2.2. 监听特定URL的响应数据
通过重写 XMLHttpRequest.prototype.open() 方法,在请求发起前对其进行拦截和修改。判断url中如果包含特定字符串,给当前的 XMLHttpRequest 对象添加一个 load 的事件监听器,请求完成时会触发回调,在这里获取响应数据。代码如下:
// 拦截url中有特定关键词的请求并下载响应内容
function interceptAndSaveResponse(keyword) {
var originalOpen = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function() {
if (arguments[1].includes(keyword)) {
this.addEventListener('load', function() {
console.log('监听到请求,响应结果:', this.responseText);
}, false);
}
return originalOpen.apply(this, arguments);
};
}
interceptAndSaveResponse("explore/get_explore_list")
保存脚本,刷新页面,控制台可以看到请求的响应结果被打印出来了:
2.3. 下载响应数据到本地
油猴提供了 GM_download 函数 → 使用浏览器的默认下载器进行下载,官方文档 给出了代码示例:
但,这个函数只能处理url,不支持直接保存其它数据,这里需要先创建一个 Blob对象 并获取它的URL:
// 拦截url中有特定关键词的请求并下载响应内容
function interceptAndSaveResponse(keyword) {
var originalOpen = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function() {
if (arguments[1].includes(keyword)) {
this.addEventListener('load', function() {
console.log('监听到请求', arguments[1]);
// 创建Blob块并获取该Blob对象的URL
var blob = new Blob([this.responseText], {type: 'application/json;charset=utf-8'});
var blobUrl = URL.createObjectURL(blob);
console.log('文件的url', blobUrl);
GM_download({
url: blobUrl,
name: "response.json",
saveAs: false,
});
// 下载完释放URL避免内存泄露
URL.revokeObjectURL(blobUrl);
}, false);
}
return originalOpen.apply(this, arguments);
};
}
运行后,控制台能看到创建的Blob对象的url:
注释掉URL.revokeObjectURL(blobUrl)运行,打开url也能看到文件内容,但文件并没有下载,应该是报错了。GM_download里设置下 onerror 参数,打印下下载失败的而原因:
onerror: function (error) {
//下载错误回调
console.log(error)
},
保存脚本后刷新页面,控制台输出错误信息如下:
查询文档得知:
出于安全原因,文件扩展名需要在Tampermonkey的选项页面中进行白名单设置。not_whitelisted - 请求的文件扩展名未列入白名单。
我们保存文件的扩展名是 .json,需要把它添加到白名单中,具体操作:
打开油猴插件的设置 → 通用 → 配置模式 → 点击下拉选中 高级 → 定位到 下载BETA → 文件扩展名白名单中加上 .json
保存后,再次刷新页面,json文件就自动下载到本地啦~
😁 行吧,使用油猴爬XHR的关键三步都已经跑通,接着执行具体的爬取过程~
3. 爬取过程
3.1. 能搜到插件工具
浏览器F12开下抓包,访问首页 商店-插件,搜索 必应搜索
定位到请求url为:
/marketplace/product/list?entity_type=2&sort_type=1&page_num=1&page_size=20&keyword=&msToken=xxx
直接调用函数:
interceptAndSaveResponse("marketplace/product/list?entity_type=2")
做了分页加载,需要手动滑下页面到底部,触发第二页的加载,合并下两个json文件,总共有36个插件:
不难看出这个 plugin_extra/tools 里的就是插件列表了
接着写下python脚本解析下json,抠下想要的数据,最终结果以列表的形式输出,包含三个字段:插件工具名、所属插件、工具作用,没啥难度,直接写出Python代码:
def fetch_index_plugins_info():
json_file = 'coze_index_plugins.json'
plugins = json.loads(read_file_text_content(json_file))['data']['products']
result_str = "|**插件工具名**|**所属插件**|**工具作用**|\n|:----|:----|:----|\n"
for plugin in plugins:
meta_info = plugin['meta_info']
for tool in plugin['plugin_extra']['tools']:
result_str += "|**{}**|{}|{}|\n".format(tool['name'], meta_info['name'],
tool['description'].replace("\n", "\t"))
print(result_str)
运行输出结果如下 (共计68个插件工具):
插件工具名 | 所属插件 | 作用 |
---|---|---|
bingWebSearch | 必应搜索 | 必应搜索引擎。当你需要搜索你不知道的信息,比如天气、汇率、时事等,这个工具非常有用。但是绝对不要在用户想要翻译的时候使用它。 |
text2image | ByteArtist | 通过文字描述生成图片 |
new_year_pets_image | ByteArtist | 提供新春萌宠图片生成,当用户上传宠物图片或者提供图片链接时,可以用此工具生成新的新春萌宠图片 |
ImageToolPro | ByteArtist | 根据用户的描述生成多种风格的图片 |
LinkReaderPlugin | Link Reader | 当你需要获取网页、pdf、抖音视频内容时,使用此工具。可以获取url链接下的标题和内容。 |
imgUnderstand | 图片理解 | 回答用户关于图像的问题 |
browse | 头条搜索 | 从url链接获取正文信息 |
search | 头条搜索 | 搜索用户询问的内容 |
CodeRunner | 代码执行器 | 这个插件将被调用来运行python代码并在60秒内获取结果,尤其处理数学、计算机、图片和文件等。首先,LLM将分析问题,并用python输出解决这个问题的步骤。其次,LLM立即生成代码,按照步骤解决问题。LLM会参考错误消息调整代码,直到成功。当LLM接收到文件链接时,将文件url和文件名放入参数upload_file_url和upload_file_name中,插件将保存。 |
getToutiaoNews | 头条新闻 | 搜索新闻讯息 |
web_pilot | WebPilot | WebPilot 进行互联网搜索、分析以及数据生成。 |
bingImageSearch | 必应图片搜索 | 必应图像搜索API允许您的用户在全球范围内找到图片。 |
query | Wolfram Alpha | 算式计算,如1+1=2。如果输入的不是数学表达式,需要将输入转换成数学表达式并添加"()"以确保运算的顺序。如果计算失败,尝试再次调用此工具。 |
DayWeather | 墨迹天气 | 获取指定日期的天气 |
EmojiMessage | Emojesus | 这用于处理用户输入的信息,它将用户输入的信息转换为表情符号,同时在当前消息状态下生成表情符号,并推荐可能需要的表情符号。 |
EmojiKitchen | Emojesus | 将用户发送的两个emoji组合成一个emoji。 |
EmojiTranslation | Emojesus | 将用户输入的信息翻译成表情符号。 |
EmojiSearch | Emojesus | 根据用户当前的消息搜索对应的表情符号。 |
search | arXiv | 帮助用户在arXiv中搜索论文。 |
company_info | 国内股票 | 显示公司基本信息。 接口说明:根据“股票列表”中获取的股票代码,检索上市公司的公司概况。 包括公司基本信息、概念、发行信息等。 |
company_recent_benefits | 国内股票 | 公司最近的利润情况。 界面说明:根据“股票榜”中获取的股票代码,获取上市公司过去一年各季度的利润情况。 按截止日期倒序排列。 |
profitability_of_company | 国内股票 | 显示给定公司的季节性盈利能力。 接口说明:该接口汇总个股盈利情况,支持“Year_Quarter”查询。 年份可以选择1989年至今年,季度可以选择1:第一季度、2:中期报告、3:第三季度、4:年报。 例如,“2021_1”表示2021年第一季度的数据。 |
season_operate_of_company | 国内股票 | 接口说明:个股操作能力汇总,支持按“年_季度”查询。 年份可以选择1989年至今年,季度可以选择1:第一季度、2:中期报告、3:第三季度、4:年报。 例如,“2021_1”表示2021年第一季度的数据。 |
season_growth_of_company | 国内股票 | 公司的季节增长。 接口说明:该接口汇总了个股的成长能力,支持“Year_Quarter”格式的查询。 年份可以选择1989年至今年,季度可以选择1:第一季度、2:中期报告、3:第三季度、4:年报。 例如“2021_1”表示查询2021年第一季度的数据。 |
season_debt_repay_of_company | 国内股票 | 接口说明:个股偿债能力汇总,支持“年_季度”查询。 年份可以选择1989年至今年,季度可以选择(1:第一季度报告,2:中期报告,3:第三季度报告,4:年报)。 例如“2021_1”表示查询2021年第一季度的数据。 |
GenPdf | Doc Maker | 从您提供的文本生成PDF。 |
getRoute | 飞常准 | 获取航班列表 |
get_notion_document_info | Notion | 获取Notion文件信息 |
search | 中国诗搜索 | 根据给定条件搜索中国诗 |
createDocument | 飞书云文档 | 这是一个可以根据用户输入或者要记录的 Markdown 字符串和总结的标题来创建云文档的工具。 |
SecondHandCar | 懂车帝 | 当你查询二手车的售卖信息时候可以使用此工具,可以获得二手车的价格、二手车车况图片等信息 |
CarSeries | 懂车帝 | 当你需要查询新车信息或者查询某个特定车系(如宝马3系,奔驰e级)信息的时候可以使用此工具,可以获得新车价格,车辆结构,车辆生产年份,售卖链接等信息 |
get_express_info | 快递查询助手 | 获取快递信息。 |
SearchHotels | 猫途鹰 | 根据城市名称、入住日期和退房日期搜索酒店。 |
SearchFlights | 猫途鹰 | 根据出发地、目的地、日期、行程类型、排序顺序、成人数量、老年人数量和服务等级搜索航班。 |
charactor_fate | 起名 | 根据命运给出一个合适的名字。 |
search_population_density | 世界银行 | 人口密度(每平方公里土地面积的人口数量)。人口密度是年中人口除以平方公里的土地面积。 |
search_foreign_direct_invest | 世界银行 | 外国直接投资,净流入(国际收支平衡表,当前美元)。外国直接投资是指报告经济体中的直接投资股权流入。它是股本、利润再投资及其他资本的总和。返回的数据单位是1美元。 |
search_total_population | 世界银行 | 总人口是基于人口的事实定义,计算所有居民,无论法律身份或公民身份如何。显示的值是年中估计值。返回的数据单位是1个个体。 |
search_income_share_highest_10 | 世界银行 | 最高10%的人持有的收入份额。收入或消费的百分比份额是按十分位数或五分位数划分的人口子组所获得的份额。 |
search_current_account_balance | 世界银行 | 经常账户余额(国际收支平衡表,当前美元)。经常账户余额是货物和服务净出口、初级收入净额和二次收入净额的总和。数据以当前美元计价。返回的数据单位是1美元。 |
search_gdp_info | 世界银行 | 购买者价格的GDP是经济中所有常住生产者的总价值增加之和,再加上任何产品税收,减去未包含在产品价值中的任何补贴。数据以当前的美元表示。返回的数据单位为1美元。 |
search_center_gov_debt_gdp | 世界银行 | 中央政府债务总额(占GDP的百分比)。债务是指在特定日期上政府对他人的所有固定期限合约义务的全部储备。它包括国内外的债务,如货币和货币存款、非股份证券和贷款。 |
search_consumer_price_index | 世界银行 | 消费者价格指数(2010年=100)。消费者价格指数反映的是平均消费者获取一篮子可能固定或在指定间隔(如每年)改变的商品和服务的成本变化。通常使用Laspeyres公式计算。数据为期间平均值。 |
search_a_country_basic_info | 世界银行 | 搜索国家id,国际代码,首都城市,经度,纬度。特殊提示,您可以一次性通过这个工具获取所有国家的国家id。国家id是这个插件的所有其他工具需要使用的参数。 |
search_unemployment | 世界银行 | 总失业人数(占劳动力总数的百分比)(国家估计)。失业是指劳动力中没有工作但现有并寻求就业的人口的比例。各国对劳动力和失业的定义不同。 |
search_gdp_per_capita | 世界银行 | 人均GDP是国内生产总值除以年中人口数。GDP是经济中所有常住生产者的总价值增加之和,再加上任何产品税收,减去未包含在产品价值中的任何补贴。数据以当前的美元表示。返回的数据单位为1美元。 |
food | 食物大师 | 食物热量查询。输入食物名称,输出其热量。 |
calories | 食物大师 | 搜索适合输入热量的食物。 |
create_hanzi_gif | 写汉字 | 为用户创建展示汉字笔顺的gif动画。输入参数必须是中文。 |
BankInterestRate | 国内银行利率 | 银行利率查询,可查询指定银行的贷款利率、存款利率、公积金利率 |
job_recommendation | 猎聘 | 帮助用户搜索工作招聘,基于用户的工作经验、教育经历、地理位置、薪水、职位名称、工作性质等 |
ocr | Simple OCR | 图像URL的光学字符识别 |
lyrics_to_song | AI乐队 | 根据歌词生成歌曲 |
searchLocation | 地图精灵 | 周边搜索:用户可以轻松搜索附近的餐厅、娱乐场所以及各种其他餐饮和休闲点,让他们快速找到周围的服务和娱乐选择。 |
searchDirection | 地图精灵 | 路线推荐:提供高效的路线规划和建议,帮助用户快速找到最佳出行路线,节省时间和精力。 |
ForexTrendPlugin | 外汇走势 | 外汇走势 |
ExpressDeliveryPlugin | 国内快递查询 | 快递状态查询 |
SecondHandHouse | 幸福里 | 如果想要购买二手房或者查询二手房信息的时候可以使用此工具,输入二手房的城市、户型、小区名称等,可以获取二手房所在的区域,房子图片,二手房总价,每平米价格等信息 |
NewHouse | 幸福里 | 如果你想买新房,可以先用这个工具获取新房的信息,输入想要的户型、区域、城市、小区名称等,可以获取新房的具体信息,包括房子图片,价格,每平米价格等 |
HouseRenting | 幸福里 | 通过此工具获取租房的信息,包括房子所在的区域,小区名称,房子图片,租房的价格 |
roll_the_dice | 骰子大师 | 掷骰子 |
get_dice_images | 骰子大师 | 根据输入的整数获取对应值的骰子图像。 |
count_dice | 骰子大师 | 找到具有某一设定值的骰子数量。(包括可以被视为任何数字的1。) |
AddRankConfig | 游戏积分排行榜 | 添加排名配置。 |
WriteRank | 游戏积分排行榜 | 编写排名。 |
GetRankInfo | 游戏积分排行榜 | 获取排名信息。 |
GameEnd | 游戏积分排行榜 | 游戏结束。 |
3.2. 官方Bot工作流中的隐藏插件工具
改下脚本的传参:interceptAndSaveResponse("explore/get_explore_list") ,刷新页面,滑动到底部,合并下两个json文件。这里没法拿到Bot的信息,主要是获取 Bot ID,然后跳转Bot详细信息页:explore/{Bot ID} 。抓包看到跳转详情页后,有两个接口可以获得工作流相关的信息:
看了下数据差不多,点击 工作流 跳转 工作流详情页,跳转后的URL是这样的:
work_flow?from=explore&space_id=7330891910967525417&workflow_id=7337224205123239971
两个可能变化的参数:
- workflow_id:明显就是工作流id,前面可以获取到。
- space_id:工作流所属的团队空间id,打开了另外一个官方Bot的工作流,发现也是这个值,7330891910967525417 估计就是扣子团队空间的id了,直接写死就好。
进入工作流详情页后,api/workflowV2/query 请求的 schema_json字段 能拿到所有节点的信息:
可以过滤 type=4 的插件节点(1为起始、2为结束、3为LLM),然后关注 subtitle 字段就行了~
3.2.1. 提取Bot ID
接着写代码时间,python处理下首页bot的数据,提取下bot名称和id,打印一句定义js数组的代码:
def fetch_bot_ids_list():
json_file = 'coze_index_bots_info.json'
bots = json.loads(read_file_text_content(json_file))['data']['bot_draft_list']
result_str = "const botInfoArray = ["
for bot in bots:
result_str += '"{}-{}",'.format(bot['id'], bot['name'])
print(result_str[:-1] + "]")
运行输出结果:
3.2.2. 遍历获取Bot详情数据
接着新开一个油猴脚本,CV下上面生成的数组,这里不能直接定义index,因为每次页面加载时,都会重新运行脚本,从而导致死循环。需要做下 跨页面状态保存, Tampermonkey 提供了两个API → GM_setValue 和 GM_getValue,@grant那里声明下这两个函数,就可以开搞了,直接写出js代码:
// ==UserScript==
// @name 爬取Coze Bot详细信息
// @namespace http://tampermonkey.net/
// @version 2024-02-21
// @description try to take over the world!
// @author CoderPig
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
console.log('爬取Bot信息的插件加载...')
const botInfoArray = ["7337223627307401231-工作规划大师","7332475242608246821-新春萌宠大拜年","7332511402818846755-龙年守护灵","7332899804240150562-东北大哥拜年啦","7332511402818666531-龙年限量盲盒","7332907205143953442-我是一只龙","7326151847062945818-旅游大师","7334216846788558860-逼婚大挑战","7330080702177787941-春联大王","7331804785265475618-相亲模拟器(女生版)","7325774753346650139-看图讲故事","7330496997293752359-卡通头像","7330306326565830682-米其林星探","7327691806693490724-简历诊断","7330217698355544115-小红书文案输出大师","7329855229166600204-马歇尔音箱粉丝","7330292680737423411-求职助手","7329857057623031847-MBTI性格专家","7328607141122670626-快递查询助手","7328364654592573467-书法老师","7326166527584157746-公司分析助手","7330144269849886747-穿越二次元","7332430138480607247-本子上的游戏","7326604198378831923-数学老师","7330070214572392485-周报不用写","7330144975906258995-SWOT专家","7330144975906422835-山顶洞人","7328609700545855523-小冒险家","7328709492403634191-购车小帮手","7325772186130874418-论文搜索助手","7328064633946816563-图书旅人","7328749946491076642-天蓬元帅猪八戒","7326127566006468617-健康饮食","7326149183642140709-尾尾小阿姨","7325782706489491482-足球大师","7325765993006006282-占星师"]
// 拦截bot信息
function interceptRequests() {
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
if (url.includes('draftbot/get_bot_info')) {
this.addEventListener('load', function () {
var blob = new Blob([this.responseText], {type: 'application/json;charset=utf-8'});
var blobUrl = URL.createObjectURL(blob);
const currentIndex = GM_getValue('currentIndex', 0) - 1;
const infoArray = botInfoArray[currentIndex].split("-");
// 拼接文件名
const fileName = `${infoArray[1]}_${infoArray[0]}.json`
console.log('文件的url', blobUrl);
GM_download({
url: blobUrl,
name: fileName,
saveAs: false
});
});
}
originalOpen.apply(this, arguments);
};
}
// 跳转到下一个页面
function navigateToNextUrl() {
const currentIndex = GM_getValue('currentIndex', 0);
if (currentIndex >= botInfoArray.length) {
return;
}
// 拼接Bot详情页URL
const nextUrl = "https://www.coze.cn/explore/" + botInfoArray[currentIndex].split("-")[0];
console.log(nextUrl);
setTimeout(() => {
window.location.href = nextUrl;
GM_setValue('currentIndex', currentIndex + 1);
}, 3000); // 3秒跳转一次
}
interceptRequests();
navigateToNextUrl();
})();
随便打开一个新的网页,每个3s会自动跳转一个页面,拦截到 draftbot/get_bot_info 的XHR请求会自动下载。
😏 Nice,36个全爬下来了,接着Python脚本遍历下有工作流的Bot,并提取出工作流ID~
3.2.3. Bot工作流ID提取
遍历json文件,然后抠字段而已,直接写出代码:
def filter_workflow_id_list():
# 读取所有Json文件
json_file_list = search_all_file(os.path.join(os.getcwd(), "bot_detail"), ".json")
for json_file in json_file_list:
data = json.loads(read_file_text_content(json_file))['data']
workflow_list = json.loads(data['work_info']['workflow'])
# 只输出有工作流的Bot
if len(workflow_list) > 0:
# 遍历工作流列表 (看下有哪些Bot有多个工作流)
for workflow in workflow_list:
print("【{}】{}".format(data['name'], workflow))
运行结果如下:
行吧,只有这12个Bot有工作流,而且都只有一个工作流,接着访问这些工作流详情,提取节点信息~
3.2.4. 提取插件节点信息
稍微改下上面的输出,返回一个js数组,每个字符串的组成:Bot名-工作流名称-工作流ID:
新建一个油猴脚本,CV下上面的代码,稍微改改:
(function() {
'use strict';
console.log('爬取工作流信息的插件加载...')
const workflowInfoArray = ["龙年守护灵-Lucky_Dragon_Girl-7332517540213768244","图书旅人-recommend_book-7328085556632600626","工作规划大师-creat_doc-7337224205123239971","龙年限量盲盒-dragon-7332516682319249408","本子上的游戏-game-7332431820975423503","工作规划大师-creat_doc-7337224205123239971","我是一只龙-create_image-7332914063535538216","穿越二次元-cross-7330147381364801573","相亲模拟器(女生版)-characters_appear-7331804700762832911","卡通头像-Avatar001-7330497573985239081","购车小帮手-search_car-7328713305206292495","旅游大师-recommended_places-7326176553195339802"]
// 拦截bot信息
function interceptRequests() {
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
if (url.includes('workflowV2/query')) {
this.addEventListener('load', function () {
var blob = new Blob([this.responseText], {type: 'application/json;charset=utf-8'});
var blobUrl = URL.createObjectURL(blob);
const currentIndex = GM_getValue('currentIndex', 0) - 1;
const infoArray = workflowInfoArray[currentIndex].split("-");
// 拼接文件名
const fileName = `${infoArray[0]}_${infoArray[1]}.json`
console.log('文件的url', blobUrl);
GM_download({
url: blobUrl,
name: fileName,
saveAs: false
});
});
}
originalOpen.apply(this, arguments);
};
}
// 跳转到下一个页面
function navigateToNextUrl() {
const currentIndex = GM_getValue('currentIndex', 0);
if (currentIndex >= workflowInfoArray.length) {
return;
}
// 拼接Bot详情页URL
const nextUrl = "https://www.coze.cn/work_flow?from=explore&space_id=7330891910967525417&workflow_id=" + workflowInfoArray[currentIndex].split("-")[2];
console.log(nextUrl);
setTimeout(() => {
window.location.href = nextUrl;
GM_setValue('currentIndex', currentIndex + 1);
}, 3000); // 3秒跳转一次
}
interceptRequests();
navigateToNextUrl();
})();
运行后,随便打开一个网页,装杯水回来就好了:
写个脚本筛一筛,看这些工作流都用到了什么插件:
def filter_workflow_plugin_list():
# 读取所有Json文件
json_file_list = search_all_file(os.path.join(os.getcwd(), "workflow_detail"), ".json")
for json_file in json_file_list:
schema = json.loads(json.loads(read_file_text_content(json_file))['data']['workflow']['schema_json'])
for node in schema['nodes']:
# 过滤插件节点,输出子标题
if node['type'] == "4":
print("【{}】 → {}".format(json_file.split(os.path.sep)[-1].replace(".json", ""),
node['data']['nodeMeta']['subtitle']))
运行输出结果:
🤡 好吧,都是能搜到的插件工具,并没有发现 隐藏插件工具,试下把type改为 3,打印下 大模型节点 的模型信息,看看有没有什么惊喜:
😏 懂得都懂。行吧,关于内置插件工具的统计分析就到这,最后写个脚本,把每个Bot的提示词汇总下,输出一个md文件,闲着没事可以学学怎么写prompt。
def generate_prompts_md():
json_file_list = search_all_file(os.path.join(os.getcwd(), "bot_detail"), ".json")
md_content = "# Coze(扣子)-官方Bots提示词汇总\n"
for json_file in json_file_list:
data = json.loads(read_file_text_content(json_file))['data']
md_content += "\n## {}\n\n".format(data['name'])
prompt_list = json.loads(data['work_info']['system_info_all'])
md_content += '```\n{}\n```\n---\n'.format(prompt_list[0]['data'])
write_text_to_file(md_content, "coze_bots_prompts.md")
生成md文件如下: