“榴莲自定义功能”的版本间差异
小 (→注意事项) |
|||
(未显示2个用户的23个中间版本) | |||
第46行: | 第46行: | ||
如果您好奇别人开发的功能是如何实现的,可以发送 <code>.view 功能名</code> 查看源代码。兼容句号前缀。 | 如果您好奇别人开发的功能是如何实现的,可以发送 <code>.view 功能名</code> 查看源代码。兼容句号前缀。 | ||
<references group="注释"/> | <span style="color:#aaa;"><references group="注释"/></span> | ||
== 进阶使用方法 == | == 进阶使用方法 == | ||
第77行: | 第77行: | ||
例如 <code>.jrrp</code> 等价于执行 <code>jrrp()</code> ;<code>。垃圾 玻璃</code> 等价于执行 <code>垃圾("玻璃")</code> 。 | 例如 <code>.jrrp</code> 等价于执行 <code>jrrp()</code> ;<code>。垃圾 玻璃</code> 等价于执行 <code>垃圾("玻璃")</code> 。 | ||
=== | === 示例一:Hello, world! 开发自己第一个可交互功能 === | ||
推荐使用 [https://code.visualstudio.com/ VSCode], [https://www.sublimetext.com/ Sublime] 或其它 IDE / 带语法高亮的文本编辑器进行开发,即使您熟练到可以使用记事本开发,能忍受非等宽字体的代码文本,QQ 针对特殊符号的弹出菜单依然非常烦人。 | |||
功能是通过函数实现的,如果您是从零基础开始学习,那建议教程至少看到函数这一章。在此之前,您可以使用本虚拟环境或浏览器控制台练习、测试基础语法。 | |||
本功能内置的 <code>help</code> 函数是为了方便用户去了解您撰写的功能的,您的函数第二行往后连续的以 <code>//</code> 开头的注释将被视为帮助信息,如果您希望您的功能被更多的用户理解和使用,请不要忽视帮助信息的重要性。 | |||
现在您可以尝试在群里发送如下的代码文字:<span style="color:#aaa;">(在本文的所有示例中控制台模式的 <code>></code> 符号均不给出,因为这不是标准 JavaScript 的一部分,请自行添加此符号)</span><syntaxhighlight lang="javascript"> | |||
function hello() { | |||
// 发送“.hello”向机器人打招呼! | |||
return '你好,' + qqName() + '!'; // ① | |||
} | |||
</syntaxhighlight>定义完毕后,其它用户发送 <code>.hello</code> 就可以收到来自机器人的回应了。<blockquote><span style="color:#aaa;">代码中 ① 部分可以使用 EMCAScript 2015 引入的模板字面量 <code>`你好,${qqName()}!`</code> ,笔者也推荐这种写法,不过为了教程的简单性,本文多以 <code>+</code> 拼接字符串。<br> | |||
定义函数也可以使用赋值定义 <code>hello = function () { ... }</code> ,也可以用箭头函数 <code>hello = () => { ... }</code> ,都是 JavaScript 合法表达式。注意定义不能使用声明关键字 <code>let</code> 和 <code>const</code> ,可以在最外层使用 <code>var</code>,这是由于需要把定义的变量绑定到全局对象中,<code>let</code> 和 <code>const</code> 声明的变量只在局部作用域有效,详见[https://zh.javascript.info/var 旧时的 "var"]。</span></blockquote> | |||
=== 示例二:你太强了!彩虹屁功能 === | |||
参考《关于 Chuck Norris 100 个广为人知的事实》,这里选取部分事实来生成夸群友的彩虹屁功能。<syntaxhighlight lang="javascript"> | |||
var tqlList = [ | |||
"%name 的眼泪能治癌,可惜的是他从来不哭。", | |||
"%name 从一数到了无穷。两次", | |||
"精装版吉尼斯世界纪录大全的最后一页上特别注明,所有的吉尼斯世界纪录其实都是由 %name 保持的,这本书上记载的只不过是那些最接近他的人而已。", | |||
"世界上其实没有进化论。只有一张“%name 允许活下来的生物列表”", | |||
"%name 是世界上唯一的在网球比赛里打赢了一堵墙的人。", | |||
"没有什么大规模杀伤性武器,只有 %name。", | |||
"当 %name 掉到水里的时候,%name 没有被弄湿:水被弄“%name”了。", | |||
"%name 的房子没有门,只有他直接穿过的墙。", | |||
"%name 有12颗卫星,其中之一是地球。", | |||
"%name 在汉堡王要了一个巨无霸,并且要到了。", | |||
"%name 能关上一扇旋转门。", | |||
"%name 能被零整除。", | |||
]; | |||
function tql(name = qqName()) { | |||
// 发送“.tql 某人”来夸赞他 | |||
const item = tqlList[Math.floor(Math.random() * tqlList.length)]; | |||
return item.replace(/%name/g, name); | |||
} | |||
</syntaxhighlight>在这里 <code>tqlList</code> 作为全局变量暴露在外面,您也可以写为隐藏在函数中的局部变量,只是全局变量更好维护一些,不用每次修改的时候都重新输入代码。如果您希望每次同一个名字的输出相同,请考虑使用带种随机数,参考后面的示例。 | |||
=== 编写实用型基础功能 === | |||
在开发新功能的过程中,您可能注意到了一些基础的代码会反复地写来写去,例如从数组里随机挑选一个元素,总是要写 <code>arr[Math.floor(Math.random() * arr.length)]</code> ,遇到这种情况时,推荐把这种类型的功能单独封装为一个独立的函数,以后就可以依赖这些函数进行快捷、明晰的开发了。优秀的实用函数会不定期罗列到本文[[榴莲自定义功能#.E5.AE.9E.E7.94.A8.E7.9A.84.E8.87.AA.E5.AE.9A.E4.B9.89.E5.87.BD.E6.95.B0|实用的自定义函数]]一节。 | |||
在这里举几个实用函数的的例子。<syntaxhighlight lang="javascript"> | |||
function pick(array, seed = null) { | |||
// 从 array 当中随机挑选一个元素,若提供 seed,则在 array 相同的情况下每次返回同一个随机元素 | |||
if (!(array instanceof Array)) throw new TypeError('第一个参数仅支持数组。'); | |||
const random = seed ? seedRandom(seed) : Math.random; | |||
return array[Math.floor(random() * array.length)]; | |||
} | |||
</syntaxhighlight>有了这样一个随机挑选元素的函数,示例二当中的代码就可以简化为 <code>pick(tqlList)</code> 。<syntaxhighlight lang="javascript"> | |||
function shuffle(array, seed = null) { | |||
// 随机打乱 array,即洗牌,若提供 seed,则在 array 相同的情况下洗牌的顺序一致 | |||
// 此函数会改变原数组 | |||
if (!(array instanceof Array)) throw new TypeError('第一个参数仅支持数组。'); | |||
const random = seed ? seedRandom(seed) : Math.random; | |||
for (let i = array.length; i > 1;) { | |||
const j = Math.floor(random() * (i--)); | |||
[array[i], array[j]] = [array[j], array[i]]; | |||
} | |||
return array; | |||
} | |||
</syntaxhighlight>有了洗牌函数,以后一些娱乐功能(例如发牌)就可以直接调用了。<syntaxhighlight lang="javascript"> | |||
function range(start, stop = null, step = 1) { | |||
// Python 风格的 range 函数,返回一个与 Python 中 list(range(start, stop, step)) 相同的数组 | |||
if (stop === null) [start, stop] = [0, start]; | |||
if (typeof start !== 'number' || typeof stop !== 'number' || typeof step !== 'number') | |||
throw new TypeError('错误的参数类型'); | |||
if (step === 0) throw new RangeError('步长不能为零'); | |||
const result = []; | |||
const cmp = step > 0 ? (a, b) => a < b : (a, b) => a > b; | |||
for (let i = start; cmp(i, stop); i += step) result.push(i); | |||
return result; | |||
} | |||
</syntaxhighlight>习惯了 Python 内置的 <code>range</code>,在 JavaScript 里面也实现一个方便使用也不错。 | |||
=== 示例三:你的超能力是什么?试试利用实用函数开发新功能 === | |||
有了实用函数后,看看这个例子,是不是发现编写功能似乎简单了不少呢?<syntaxhighlight lang="javascript"> | |||
var superpowerList = ['每次考试满分', '会隐身', '超高校级的运气', '心灵感应']; | |||
var superpowerCostList = [ | |||
'不能出门', '必须大声喊出超长的咒语', '一辈子与金钱无缘', | |||
'得一种会在你死后以你的名字命名的怪病' | |||
]; | |||
function 超能力() { | |||
// 来看看你今天的超能力是什么?代价又是什么? | |||
// 你也可以发送“.添加超能力 能力描述”和“.添加代价 代价描述”来扩充更多的可能性 | |||
return qqName() + ' 今天的超能力是\n' + | |||
pick(superpowerList) + '\n' + | |||
'但是代价是\n' + | |||
pick(superpowerCostList); | |||
} | |||
function 添加超能力(superpower) { | |||
if (superpower) superpowerList.push(superpower); | |||
} | |||
function 添加代价(superpowerCost) { | |||
if (superpowerCost) superpowerCostList.push(superpowerCost); | |||
} | |||
</syntaxhighlight> | |||
=== 示例四:你是什么垃圾?利用免费 API 实现的功能 === | |||
上面两个示例您可能发现了,很多功能的开发需要大量的数据支持才能变得更加实用。由用户提供数据是一个思路,而另一个思路则是从广袤的互联网当中获得海量数据。本示例和下个示例将给出两个简单的基于 <code>get</code> 从网上获取数据的功能。 | |||
网上有大量的免费或付费的 API,用于提供、处理、转发各种不同的数据。由于 API 设计就是给开发者提供数据的,很多是以 JSON 格式返回结构化的数据,因此相比于下个示例来说,利用 API 实现功能非常简单。下面以[https://api.66mz8.com/docs-garbage.html 垃圾分类查询](请点击此链接查看 API 文档)为例,注意利用 API 开发功能一定要仔细阅读 API 提供者撰写的文档。<syntaxhighlight lang="javascript"> | |||
function 垃圾(garbageName) { | |||
// 发送“.垃圾 垃圾名”查询垃圾分类 | |||
if (!garbageName) return; | |||
const url = 'https://api.66mz8.com/api/garbage.php?name=' + | |||
encodeURI(garbageName); | |||
get(url).then(json => { | |||
const res = JSON.parse(json); | |||
if (res.name && res.data) alert(res.name + '是' + res.data); | |||
else if (res.msg) alert(res.msg); | |||
}); | |||
} | |||
</syntaxhighlight>这里使用了 Promise 风格的处理方式,您也可以按 async / await 方式使用,参见示例五和示例六。 | |||
=== 示例五:重口味今日菜品,利用爬虫实现的功能 === | |||
API 提供的功能受限于服务提供商,而爬虫为获得数据提供了更多的可能性。然而,这一节的内容需要您有 HTML, CSS 和 Web API 的一些基础知识以及对爬虫的一些粗略的理解才能看明白,遇到的问题也相对会更加棘手。 | |||
'''重要提示:'''很多网站服务器是不欢迎爬虫 / 自动化抓取信息机器人的,他们可能会采取限制 headers、需要登录状态 / token、需要验证码、限制访问频率、图片反爬、字体反爬等各式手段对抗爬虫。笔者认为采取反爬手段的网站最好就尊重他们的意愿,因此 <code>get</code> 暂时只提供了基础的请求能力。如果您有确切的理由需要自定义 headers 的 GET 请求,或者 POST 请求等,欢迎向开发者联系。 | |||
各式各样的 wiki 是结构化最好的数据来源之一,这个示例实现了一个从[https://dontstarve.fandom.com/zh/wiki/%E9%A3%9F%E7%89%A9 饥荒 wiki 的食物条目]中获取食物类型,并每天推荐一个“今日菜品”的功能。为了实现这个示例,我们需要打开 wiki 条目,通过控制台(<code>F12</code> / <code>Ctrl + Shift + I</code>)或源代码(<code>Ctrl + U</code>)定位到要获得的信息位置,分析网页的 HTML 结构。在这个示例中,要获取的信息在第一个表格(<code>table</code> 标签)中,除了第一行表头外,存在每一行(<code>tr</code> 标签)的每个单元格(<code>td</code> 标签)内。另外这个 wiki 使用了异步加载图片的技术,每个图片的真实 URL 保存在 <code>img</code> 标签的 <code>data-src</code> 属性中。分析完毕后,就可以针对性的写出如下代码。<syntaxhighlight lang="javascript"> | |||
var jrcp = async () => { | |||
// 今日菜品 | |||
// 由于需要异步加载网站数据,在加载期间有可能环境变量发生变化 | |||
// 因此在这里提前保存所需的环境变量 | |||
const user_id = qq(); | |||
const user_name = qqName(); | |||
// wiki 数据不常变动,加载一次,缓存到全局变量中即可 | |||
if (!globalThis.dontStarveFoodList) { | |||
// 防止两次加载,提前给 dontStarveFoodList 一个真值 | |||
dontStarveFoodList = []; | |||
// 使用 parse 解析 HTML,详见其它内置函数: parse | |||
const document = parse(await get( | |||
'https://dontstarve.fandom.com/zh/wiki/%E9%A3%9F%E7%89%A9' | |||
)); | |||
// 获得第一个表格元素,这里同时选择了 wikitable 类和 sortable 类 | |||
const foodTable = document.querySelector('table.wikitable.sortable'); | |||
// 因为 wiki 的数据中,血量、饱食度、SAN 值有的数字前面没有符号 | |||
// 这里构建了一个函数,使每一个 buff 数值都有一个“+/-”符号 | |||
const normalizeBuffNumber = (numberText) => | |||
/^[-+]/.test(numberText) ? numberText : '+' + numberText; | |||
// 循环:对于表格的每一行 | |||
for (const tr of foodTable.querySelectorAll('tr')) { | |||
// 获得这一行中每一个单元格 | |||
const cells = tr.querySelectorAll('td'); | |||
// 过滤表头 | |||
if (cells.length < 6) continue; | |||
// 添加一个食物项目 | |||
dontStarveFoodList.push({ | |||
name: cells[1].querySelector('a').innerText, | |||
image: cells[0].querySelector('img').getAttribute('data-src'), | |||
HP: normalizeBuffNumber(cells[3].innerText), | |||
HV: normalizeBuffNumber(cells[4].innerText), | |||
SAN: normalizeBuffNumber(cells[5].innerText), | |||
}); | |||
} | |||
} | |||
// 防止加载结束前出现第二个 jrcp 请求 | |||
if (dontStarveFoodList.length === 0) return; | |||
// 按种子随机一个食物项目 | |||
const item = pick( | |||
dontStarveFoodList, | |||
user_id + new Date().toLocaleDateString(), | |||
); | |||
// 输出这个食物项目 | |||
alert([ | |||
user_name, ' 今天的菜品是 ', item.name, img(item.image), | |||
'HP', item.HP, ' HV', item.HV, ' SAN', item.SAN, | |||
].join('')); | |||
}; | |||
</syntaxhighlight> | |||
=== 示例六:挑出最胖的群友?多轮交互式功能 === | |||
有这么一个古老的问题:<blockquote>柏拉图有一天问老师苏格拉底是什么是爱情,苏格拉底叫他到麦田走一次,要不回头地走,在途中要摘一株最大最好的麦穗,'''但只可以摘一次'''。柏拉图觉得很容易,充满信心地出去,谁知过了半天他仍没有回去。最后,他垂头丧气地出现在老师跟前诉说空手而回的原因:“很难得看见一株不错的,却不知道是不是最好的,因为只可以摘一株,只好放弃,再往前走看看有没有更好的。到发现已经走到尽头时,才发觉手上一株麦穗也没有”。</blockquote>换做是你能够找到最大的那一株吗? | |||
现在把这个问题变成挑选最胖群友的小游戏试试吧。<syntaxhighlight lang="javascript"> | |||
var 吃群友 = async () => { | |||
// 古有麦田拾穗,今有娜米吃人,来看看你能不能献祭出最胖的群友! | |||
// 现在有十位群友排队测体重,发送“要”或“不要”来挑选。 | |||
// 不要的群友就逃了,无法回头再逮回来,小心错失良机! | |||
/* 注意环境变量会随着群内发送消息而变化, | |||
一定要保存好需要的环境变量 */ | |||
const user_id = qq(); | |||
const N = 10; | |||
const order = shuffle(range(N)); // 胖瘦顺序,9 最胖,0 最瘦 | |||
const weightList = []; // 重量列表,按需计算 | |||
let i; | |||
for (i = 0; i < N - 1; i++) { | |||
let lower = 80, upper = 210; | |||
for (let k = 0; k < i; k++) { | |||
if (order[i] < order[k]) | |||
upper = Math.min(upper, weightList[k]); | |||
else | |||
lower = Math.max(lower, weightList[k]); | |||
} | |||
const currentWeight = (lower + upper) / 2; | |||
weightList.push(currentWeight); | |||
alert(`${at(user_id)} 第 ${i + 1} 位群友体重为 ${currentWeight.toFixed(1)} 斤,你要吗?`); | |||
let response; | |||
for (let _ = 0; _ < 3; _++) { // 等待 3 次有效输入 | |||
response = await input(user_id); | |||
response = response.trim(); | |||
if (['要', '不要'].includes(response)) break; | |||
} | |||
if (response === '不要') continue; | |||
else if (response === '要') break; | |||
else { alert('游戏超时自动结束。'); return; } | |||
} | |||
alert(i === N - 1 ? | |||
`你不得不选择最后一位群友。他是 ${N} 位群友中第 ${N - order[i]} 胖的。` : | |||
`这位群友是 ${N} 位群友中第 ${N - order[i]} 胖的。`); | |||
}; | |||
</syntaxhighlight> | |||
=== 示例七:某函数 === | |||
> TODO -- 二喵留 | |||
== 内置 API == | == 内置 API == | ||
第94行: | 第320行: | ||
|- | |- | ||
|<code>qq(qq)</code> | |<code>qq(qq)</code> | ||
| | |?string | ||
|number | |number | ||
|返回 QQ 号,默认为发送消息者 | |返回 QQ 号,默认为发送消息者 | ||
第149行: | 第375行: | ||
|- | |- | ||
|<code>qqHead(qq)</code> | |<code>qqHead(qq)</code> | ||
|<nowiki> | |<nowiki>?(number | string)</nowiki> | ||
|string | |string | ||
|生成 QQ 头像消息片段,默认发送消息者 | |生成 QQ 头像消息片段,默认发送消息者 | ||
|- | |- | ||
|<code>at(qq)</code> | |<code>at(qq)</code> | ||
|<nowiki> | |<nowiki>?(number | string)</nowiki> | ||
|string | |string | ||
|生成 at 消息片段,默认发送消息者 | |生成 at 消息片段,默认发送消息者 | ||
第167行: | 第393行: | ||
|object | |object | ||
|消息环境变量,一般以上函数足够使用,不需要手动处理环境变量 | |消息环境变量,一般以上函数足够使用,不需要手动处理环境变量 | ||
|- | |||
|''async'' <code>input(qq)</code> | |||
|?number | |||
|Promise<string> | |||
|当对应用户(默认全体)发送普通模式消息时,该消息作为字符串返回 | |||
|} | |} | ||
{| class="wikitable" | {| class="wikitable" | ||
第201行: | 第432行: | ||
|- | |- | ||
|<code>alert(message)</code> | |<code>alert(message)</code> | ||
| | |any | ||
|void | |void | ||
|向群里发送消息 | |向群里发送消息 | ||
第219行: | 第450行: | ||
!备注 | !备注 | ||
|- | |- | ||
| | |<code>pick(array, seed=null)</code> | ||
| | |T[], ?any | ||
| | |T | ||
| | |从数组内随机挑选一个元素 | ||
|- | |- | ||
| | |<code>shuffle(array, seed=null)</code> | ||
| | |T[], ?any | ||
| | |T[] | ||
| | |对数组进行随机打乱(洗牌) | ||
|- | |- | ||
| | |<code>range(stop)</code><br><code>range(start, stop, step=1)</code> | ||
| | |number | ||
| | |number[] | ||
| | |Python 风格的 <code>range</code> 函数 | ||
|} | |} | ||
第247行: | 第478行: | ||
* 不反对出于研究、学习目的的攻击,发现 bug 后请向开发者反馈,切勿反复、恶意攻击。 | * 不反对出于研究、学习目的的攻击,发现 bug 后请向开发者反馈,切勿反复、恶意攻击。 | ||
== | == 开发计划(二喵的) == | ||
# Web 端开发界面 | |||
# 个人存档机制,防止被覆盖 | |||
# 历史快照机制,防止被覆盖 | |||
# HTTP API,允许自由定制开发其它前端 |
2021年3月14日 (日) 15:36的最新版本
关于
该功能为基于 JavaScript 虚拟执行环境的被动响应式可编程功能,仅在附属群机器人研究室内开启内测。如果您不熟悉编程也没关系,可以来尝试由大家开发的最新小功能,或者对机器人进行简单的“调教”;如果您熟悉 JavaScript,非常欢迎来发散您的创意;如果您有意愿学习 JavaScript 编程语言,那同样欢迎您加入来利用虚拟环境练习、测试、开发属于自己的专属功能。
由于该功能向群内完全开放了自定义能力,因此严禁出现包括但不限于有关政治、色情、暴力、恐怖等敏感话题或违规话题,严禁任何违反 QQ 软件许可及服务协议 8.2 条款的功能开发或调用,如果
- 您开发违反以上内容的功能,例如从色情网站上搬运照片等,则您会被拉黑,该功能会被直接删除
- 您利用他人开发的中立性功能有意调用导致出现违反以上内容的情况,例如利用他人开发的维基百科搬运功能搜索政治人物事件等,则您会被拉黑
另外,由于在群内所有群友共享一个 JavaScript 执行上下文,因此请不要恶意攻击虚拟执行环境,不要恶意覆盖、删除他人定义的变量或函数。屡犯不改的群友同样可能会被拉黑。
基本使用方法
本节列举了无需编程知识就可以使用的功能列表,其中一些进阶的说明会用注释罗列到本节的最后。
计算器
此功能可以当作一个简单的计算器来用,支持 +, -, *, /, % (模), ** (幂)
等运算[注释 1]和 sin, cos, exp, log
等数学函数[注释 2]。
发送 | 回应 |
---|---|
1 + 1
|
2
|
sqrt(3 ** 2 + 4 ** 2)
|
5
|
PI * 10 ** 2
|
314.1592653589793
|
(100 + 200 + 300) / 12
|
50
|
定义自动回复
此功能可以当作另一个”调教榴莲“的方式使用,并且支持图片和表情。语法为 触发语句='回复语句'
或 触发语句="回复语句"
或 触发语句=`回复语句`
,只有最后一种表达式支持多行定义[注释 3],触发语句不能包含标点、空格或其它特殊字符。如果需要删除定义的自动回复,发送 delete 触发语句
即可。
查找功能
如果您想查找别人定义的功能,发送 .find 关键词
可以进行搜索,兼容句号前缀。注意,名称是大小写敏感的,错误的大小写无法得到预期的结果。
查看功能帮助
如果您想查看某个功能的帮助信息,发送 .help 功能名
即可,兼容句号前缀。具体的帮助信息由开发此功能的人员提供。
调用功能
发送 .功能名
可以调用该功能。如果这个功能需要参数,可以用空格分隔功能名称和参数,例如 .功能名 参数1 参数2
。兼容句号前缀。
查看源码
如果您好奇别人开发的功能是如何实现的,可以发送 .view 功能名
查看源代码。兼容句号前缀。
进阶使用方法
如果您不打算学习任何与编程有关的内容,您可以看到这里为止。如果您对撰写自己的专属功能或对编程感兴趣的话,非常欢迎来这里尝试一下。
本节不会过多地涉及 JavaScript 基础知识,如果您对 JavaScript 不熟悉,请参阅
放心,JavaScript 入门相比 C++、Java 等其它语言来说简单得多,而且这是一门迄今为止最受欢迎的语言之一,生态圈十分活跃,在网页端、后端、甚至桌面端、移动端开发、数值计算等领域均可胜任。这么热门的语言,不来学习一下试试吗?
处理消息方式说明
这里介绍一下针对群聊消息处理为合适的输入代码和对应输出的方式,注意这不是 JavaScript 处理代码的方式,切勿与 JavaScript 语法混淆。
普通模式
群内所有的信息都会被当作 JavaScript 代码尝试执行,不是以 .
、。
、>
开头的消息都会在普通模式执行,普通模式的特点为:
- 仅输出字符串、数值型、布尔型、BigInt 类型;错误、数组对象、函数对象等其它类型的输出都会静默隐藏;
- 为了减少输入法切换次数,符合国人输入习惯,以下符号在执行前会被静默转换为半角符号:
。!?,;()“”‘’《》
注意:标点静默转换可能会产生意外的结果,例如打算作为输出的字符串内的逗号句号也会变为半角,更严重的情况包括字符串内的引号变为半角导致出现语法错误而执行失败,此时由于错误的静默隐藏无法第一时间得知,因此强烈推荐开发功能使用控制台模式。
控制台模式
任何以半角符号 >
开头的信息会在控制台模式下执行,控制台模式不会静默转换标点,返回值会以检查 (inspect) 模式输出,错误信息也会显示。
命令行模式
任何以 .
或 。
开头的信息会以命令行形式运行,此模式是方便大家调用而设计的。函数名和参数间以空格分隔,参数会以字符串形式传入,因为空格作为分隔符,传入的字符串不会包含空格,而是多个位置参数,开发带参数的功能时请注意这一点。此外,命令行模式与普通模式一样会静默转换标点、静默隐藏错误和对象。
例如 .jrrp
等价于执行 jrrp()
;。垃圾 玻璃
等价于执行 垃圾("玻璃")
。
示例一:Hello, world! 开发自己第一个可交互功能
推荐使用 VSCode, Sublime 或其它 IDE / 带语法高亮的文本编辑器进行开发,即使您熟练到可以使用记事本开发,能忍受非等宽字体的代码文本,QQ 针对特殊符号的弹出菜单依然非常烦人。
功能是通过函数实现的,如果您是从零基础开始学习,那建议教程至少看到函数这一章。在此之前,您可以使用本虚拟环境或浏览器控制台练习、测试基础语法。
本功能内置的 help
函数是为了方便用户去了解您撰写的功能的,您的函数第二行往后连续的以 //
开头的注释将被视为帮助信息,如果您希望您的功能被更多的用户理解和使用,请不要忽视帮助信息的重要性。
现在您可以尝试在群里发送如下的代码文字:(在本文的所有示例中控制台模式的 >
符号均不给出,因为这不是标准 JavaScript 的一部分,请自行添加此符号)
function hello() {
// 发送“.hello”向机器人打招呼!
return '你好,' + qqName() + '!'; // ①
}
定义完毕后,其它用户发送 .hello
就可以收到来自机器人的回应了。
代码中 ① 部分可以使用 EMCAScript 2015 引入的模板字面量
`你好,${qqName()}!`
,笔者也推荐这种写法,不过为了教程的简单性,本文多以+
拼接字符串。
定义函数也可以使用赋值定义hello = function () { ... }
,也可以用箭头函数hello = () => { ... }
,都是 JavaScript 合法表达式。注意定义不能使用声明关键字let
和const
,可以在最外层使用var
,这是由于需要把定义的变量绑定到全局对象中,let
和const
声明的变量只在局部作用域有效,详见旧时的 "var"。
示例二:你太强了!彩虹屁功能
参考《关于 Chuck Norris 100 个广为人知的事实》,这里选取部分事实来生成夸群友的彩虹屁功能。
var tqlList = [
"%name 的眼泪能治癌,可惜的是他从来不哭。",
"%name 从一数到了无穷。两次",
"精装版吉尼斯世界纪录大全的最后一页上特别注明,所有的吉尼斯世界纪录其实都是由 %name 保持的,这本书上记载的只不过是那些最接近他的人而已。",
"世界上其实没有进化论。只有一张“%name 允许活下来的生物列表”",
"%name 是世界上唯一的在网球比赛里打赢了一堵墙的人。",
"没有什么大规模杀伤性武器,只有 %name。",
"当 %name 掉到水里的时候,%name 没有被弄湿:水被弄“%name”了。",
"%name 的房子没有门,只有他直接穿过的墙。",
"%name 有12颗卫星,其中之一是地球。",
"%name 在汉堡王要了一个巨无霸,并且要到了。",
"%name 能关上一扇旋转门。",
"%name 能被零整除。",
];
function tql(name = qqName()) {
// 发送“.tql 某人”来夸赞他
const item = tqlList[Math.floor(Math.random() * tqlList.length)];
return item.replace(/%name/g, name);
}
在这里 tqlList
作为全局变量暴露在外面,您也可以写为隐藏在函数中的局部变量,只是全局变量更好维护一些,不用每次修改的时候都重新输入代码。如果您希望每次同一个名字的输出相同,请考虑使用带种随机数,参考后面的示例。
编写实用型基础功能
在开发新功能的过程中,您可能注意到了一些基础的代码会反复地写来写去,例如从数组里随机挑选一个元素,总是要写 arr[Math.floor(Math.random() * arr.length)]
,遇到这种情况时,推荐把这种类型的功能单独封装为一个独立的函数,以后就可以依赖这些函数进行快捷、明晰的开发了。优秀的实用函数会不定期罗列到本文实用的自定义函数一节。
在这里举几个实用函数的的例子。
function pick(array, seed = null) {
// 从 array 当中随机挑选一个元素,若提供 seed,则在 array 相同的情况下每次返回同一个随机元素
if (!(array instanceof Array)) throw new TypeError('第一个参数仅支持数组。');
const random = seed ? seedRandom(seed) : Math.random;
return array[Math.floor(random() * array.length)];
}
有了这样一个随机挑选元素的函数,示例二当中的代码就可以简化为 pick(tqlList)
。
function shuffle(array, seed = null) {
// 随机打乱 array,即洗牌,若提供 seed,则在 array 相同的情况下洗牌的顺序一致
// 此函数会改变原数组
if (!(array instanceof Array)) throw new TypeError('第一个参数仅支持数组。');
const random = seed ? seedRandom(seed) : Math.random;
for (let i = array.length; i > 1;) {
const j = Math.floor(random() * (i--));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
有了洗牌函数,以后一些娱乐功能(例如发牌)就可以直接调用了。
function range(start, stop = null, step = 1) {
// Python 风格的 range 函数,返回一个与 Python 中 list(range(start, stop, step)) 相同的数组
if (stop === null) [start, stop] = [0, start];
if (typeof start !== 'number' || typeof stop !== 'number' || typeof step !== 'number')
throw new TypeError('错误的参数类型');
if (step === 0) throw new RangeError('步长不能为零');
const result = [];
const cmp = step > 0 ? (a, b) => a < b : (a, b) => a > b;
for (let i = start; cmp(i, stop); i += step) result.push(i);
return result;
}
习惯了 Python 内置的 range
,在 JavaScript 里面也实现一个方便使用也不错。
示例三:你的超能力是什么?试试利用实用函数开发新功能
有了实用函数后,看看这个例子,是不是发现编写功能似乎简单了不少呢?
var superpowerList = ['每次考试满分', '会隐身', '超高校级的运气', '心灵感应'];
var superpowerCostList = [
'不能出门', '必须大声喊出超长的咒语', '一辈子与金钱无缘',
'得一种会在你死后以你的名字命名的怪病'
];
function 超能力() {
// 来看看你今天的超能力是什么?代价又是什么?
// 你也可以发送“.添加超能力 能力描述”和“.添加代价 代价描述”来扩充更多的可能性
return qqName() + ' 今天的超能力是\n' +
pick(superpowerList) + '\n' +
'但是代价是\n' +
pick(superpowerCostList);
}
function 添加超能力(superpower) {
if (superpower) superpowerList.push(superpower);
}
function 添加代价(superpowerCost) {
if (superpowerCost) superpowerCostList.push(superpowerCost);
}
示例四:你是什么垃圾?利用免费 API 实现的功能
上面两个示例您可能发现了,很多功能的开发需要大量的数据支持才能变得更加实用。由用户提供数据是一个思路,而另一个思路则是从广袤的互联网当中获得海量数据。本示例和下个示例将给出两个简单的基于 get
从网上获取数据的功能。
网上有大量的免费或付费的 API,用于提供、处理、转发各种不同的数据。由于 API 设计就是给开发者提供数据的,很多是以 JSON 格式返回结构化的数据,因此相比于下个示例来说,利用 API 实现功能非常简单。下面以垃圾分类查询(请点击此链接查看 API 文档)为例,注意利用 API 开发功能一定要仔细阅读 API 提供者撰写的文档。
function 垃圾(garbageName) {
// 发送“.垃圾 垃圾名”查询垃圾分类
if (!garbageName) return;
const url = 'https://api.66mz8.com/api/garbage.php?name=' +
encodeURI(garbageName);
get(url).then(json => {
const res = JSON.parse(json);
if (res.name && res.data) alert(res.name + '是' + res.data);
else if (res.msg) alert(res.msg);
});
}
这里使用了 Promise 风格的处理方式,您也可以按 async / await 方式使用,参见示例五和示例六。
示例五:重口味今日菜品,利用爬虫实现的功能
API 提供的功能受限于服务提供商,而爬虫为获得数据提供了更多的可能性。然而,这一节的内容需要您有 HTML, CSS 和 Web API 的一些基础知识以及对爬虫的一些粗略的理解才能看明白,遇到的问题也相对会更加棘手。
重要提示:很多网站服务器是不欢迎爬虫 / 自动化抓取信息机器人的,他们可能会采取限制 headers、需要登录状态 / token、需要验证码、限制访问频率、图片反爬、字体反爬等各式手段对抗爬虫。笔者认为采取反爬手段的网站最好就尊重他们的意愿,因此 get
暂时只提供了基础的请求能力。如果您有确切的理由需要自定义 headers 的 GET 请求,或者 POST 请求等,欢迎向开发者联系。
各式各样的 wiki 是结构化最好的数据来源之一,这个示例实现了一个从饥荒 wiki 的食物条目中获取食物类型,并每天推荐一个“今日菜品”的功能。为了实现这个示例,我们需要打开 wiki 条目,通过控制台(F12
/ Ctrl + Shift + I
)或源代码(Ctrl + U
)定位到要获得的信息位置,分析网页的 HTML 结构。在这个示例中,要获取的信息在第一个表格(table
标签)中,除了第一行表头外,存在每一行(tr
标签)的每个单元格(td
标签)内。另外这个 wiki 使用了异步加载图片的技术,每个图片的真实 URL 保存在 img
标签的 data-src
属性中。分析完毕后,就可以针对性的写出如下代码。
var jrcp = async () => {
// 今日菜品
// 由于需要异步加载网站数据,在加载期间有可能环境变量发生变化
// 因此在这里提前保存所需的环境变量
const user_id = qq();
const user_name = qqName();
// wiki 数据不常变动,加载一次,缓存到全局变量中即可
if (!globalThis.dontStarveFoodList) {
// 防止两次加载,提前给 dontStarveFoodList 一个真值
dontStarveFoodList = [];
// 使用 parse 解析 HTML,详见其它内置函数: parse
const document = parse(await get(
'https://dontstarve.fandom.com/zh/wiki/%E9%A3%9F%E7%89%A9'
));
// 获得第一个表格元素,这里同时选择了 wikitable 类和 sortable 类
const foodTable = document.querySelector('table.wikitable.sortable');
// 因为 wiki 的数据中,血量、饱食度、SAN 值有的数字前面没有符号
// 这里构建了一个函数,使每一个 buff 数值都有一个“+/-”符号
const normalizeBuffNumber = (numberText) =>
/^[-+]/.test(numberText) ? numberText : '+' + numberText;
// 循环:对于表格的每一行
for (const tr of foodTable.querySelectorAll('tr')) {
// 获得这一行中每一个单元格
const cells = tr.querySelectorAll('td');
// 过滤表头
if (cells.length < 6) continue;
// 添加一个食物项目
dontStarveFoodList.push({
name: cells[1].querySelector('a').innerText,
image: cells[0].querySelector('img').getAttribute('data-src'),
HP: normalizeBuffNumber(cells[3].innerText),
HV: normalizeBuffNumber(cells[4].innerText),
SAN: normalizeBuffNumber(cells[5].innerText),
});
}
}
// 防止加载结束前出现第二个 jrcp 请求
if (dontStarveFoodList.length === 0) return;
// 按种子随机一个食物项目
const item = pick(
dontStarveFoodList,
user_id + new Date().toLocaleDateString(),
);
// 输出这个食物项目
alert([
user_name, ' 今天的菜品是 ', item.name, img(item.image),
'HP', item.HP, ' HV', item.HV, ' SAN', item.SAN,
].join(''));
};
示例六:挑出最胖的群友?多轮交互式功能
有这么一个古老的问题:
柏拉图有一天问老师苏格拉底是什么是爱情,苏格拉底叫他到麦田走一次,要不回头地走,在途中要摘一株最大最好的麦穗,但只可以摘一次。柏拉图觉得很容易,充满信心地出去,谁知过了半天他仍没有回去。最后,他垂头丧气地出现在老师跟前诉说空手而回的原因:“很难得看见一株不错的,却不知道是不是最好的,因为只可以摘一株,只好放弃,再往前走看看有没有更好的。到发现已经走到尽头时,才发觉手上一株麦穗也没有”。
换做是你能够找到最大的那一株吗? 现在把这个问题变成挑选最胖群友的小游戏试试吧。
var 吃群友 = async () => {
// 古有麦田拾穗,今有娜米吃人,来看看你能不能献祭出最胖的群友!
// 现在有十位群友排队测体重,发送“要”或“不要”来挑选。
// 不要的群友就逃了,无法回头再逮回来,小心错失良机!
/* 注意环境变量会随着群内发送消息而变化,
一定要保存好需要的环境变量 */
const user_id = qq();
const N = 10;
const order = shuffle(range(N)); // 胖瘦顺序,9 最胖,0 最瘦
const weightList = []; // 重量列表,按需计算
let i;
for (i = 0; i < N - 1; i++) {
let lower = 80, upper = 210;
for (let k = 0; k < i; k++) {
if (order[i] < order[k])
upper = Math.min(upper, weightList[k]);
else
lower = Math.max(lower, weightList[k]);
}
const currentWeight = (lower + upper) / 2;
weightList.push(currentWeight);
alert(`${at(user_id)} 第 ${i + 1} 位群友体重为 ${currentWeight.toFixed(1)} 斤,你要吗?`);
let response;
for (let _ = 0; _ < 3; _++) { // 等待 3 次有效输入
response = await input(user_id);
response = response.trim();
if (['要', '不要'].includes(response)) break;
}
if (response === '不要') continue;
else if (response === '要') break;
else { alert('游戏超时自动结束。'); return; }
}
alert(i === N - 1 ?
`你不得不选择最后一位群友。他是 ${N} 位群友中第 ${N - order[i]} 胖的。` :
`这位群友是 ${N} 位群友中第 ${N - order[i]} 胖的。`);
};
示例七:某函数
> TODO -- 二喵留
内置 API
原生对象
该功能是纯 JavaScript V8 执行环境,因此不像 Node.js 一样包含
等方法,也不像 Chrome 一样包含 require, process
等对象,只有标准 ECMAScript 6 内置的对象。具体的内置对象及对应的使用方法请参考 JavaScript 标准内置对象。
window, document
内置函数
本节包括针对 QQ 环境注入的函数和方便开发功能注入的函数。这部分内容会随着开发频繁更改。如果您需要什么其它方便的函数请尽管提出。
定义 | 参数类型 | 返回值类型 | 备注 |
---|---|---|---|
qq(qq)
|
?string | number | 返回 QQ 号,默认为发送消息者 |
messageTime()
|
无 | number | 消息的秒级时间戳 |
groupID()
|
无 | number | 消息所在的群号 |
groupName()
|
无 | string | 消息所在的群名称 |
qqName()
|
无 | string | 名称 |
qqSex()
|
无 | string | 性别 |
qqArea()
|
无 | string | 地区 |
qqAge()
|
无 | number | 年龄 |
qqLevel()
|
无 | number | 等级 |
qqRole()
|
无 | string | 群员类型 |
qqTitle()
|
无 | string | 头衔 |
qqHead(qq)
|
?(number | string) | string | 生成 QQ 头像消息片段,默认发送消息者 |
at(qq)
|
?(number | string) | string | 生成 at 消息片段,默认发送消息者 |
img(imgURL)
|
string | string | 生成图片消息片段 |
env()
|
无 | object | 消息环境变量,一般以上函数足够使用,不需要手动处理环境变量 |
async input(qq)
|
?number | Promise<string> | 当对应用户(默认全体)发送普通模式消息时,该消息作为字符串返回 |
定义 | 参数类型 | 返回值类型 | 备注 |
---|---|---|---|
Buffer 对象
|
buffer 包提供的类似 Node.js Buffer 对象 | ||
seedRandom(seed)
|
any | () => number | seed-random 包提供的带种伪随机数 |
parse(html)
|
string | object | node-html-parser 包提供的 HTML 解析器,风格与 Web API 相近 |
setTimeout
|
function, number, any |
number | 定时执行函数,延迟不能小于 5000 ms |
clearTimeout(timeout)
|
number | void | 取消定时执行的函数 |
alert(message)
|
any | void | 向群里发送消息 |
async get(url)
|
string | Promise<string> | 向指定 URL 发送 GET 请求,返回 UTF-8 编码的结果 |
实用的自定义函数
这里列举一些非内置的实用函数,由群友开发,请务必不要随意更改、删除这些函数。
定义 | 参数类型 | 返回值类型 | 备注 |
---|---|---|---|
pick(array, seed=null)
|
T[], ?any | T | 从数组内随机挑选一个元素 |
shuffle(array, seed=null)
|
T[], ?any | T[] | 对数组进行随机打乱(洗牌) |
range(stop) range(start, stop, step=1)
|
number | number[] | Python 风格的 range 函数
|
注意事项
- 所有原生对象、内置函数均设置为不可写、不可配置、不可枚举,所有原生对象的原型已被冻结。
- 由于各种原因虚拟环境可能会重启,以下是重启会带来的副作用:
- JavaScript 的非原始对象(non-primitive)是按引用赋值的,但是数据经过重启带来的序列化和反序列化之后会解除引用,变为多个独立的对象。
- 目前能够序列化保存的对象仅限:String, Number, Boolean, null, undefined, Infinity, Date, Map, Set, Function, RegExp, BigInt,以及存储可序列化内容的 Array 和 Object。其它对象可能会被丢弃或错误地序列化。包含循环引用的对象会被直接丢弃。
- 反序列化后所有对象的原型会指向 Object.prototype,函数的原型会指向 Function.prototype,除 Object 外,保存在其它非原始对象中的额外数据可能会丢失。
- 闭包内的信息会丢失。
- 鉴于以上原因,不推荐撰写需要 class 或者复杂闭包的功能。
- 注意标点的隐式转换问题,推荐使用控制台模式开发新功能。详细说明
- 不反对出于研究、学习目的的攻击,发现 bug 后请向开发者反馈,切勿反复、恶意攻击。
开发计划(二喵的)
- Web 端开发界面
- 个人存档机制,防止被覆盖
- 历史快照机制,防止被覆盖
- HTTP API,允许自由定制开发其它前端