develop.md 14 KB

  • 傻妞 是一个 支持Node、Python 机器人框架,开发需要有一定的编程基础,以下主要对Node进行讲解,其他语言类似。

开发环境搭建

通过 vscode 开发

  • 打开 plugins 目录
  • 运行傻妞 ./sillyGirl -t,在这里你可以直接输入信息调试、以及观察 console.log 输出信息、收发消息情况等信息
  • 开发插件非常方便~ 1677296097624 本人开发插件示例

文件目录说明

  • 插件开发

    • plugins 下只有二级目录下的 index.js 作为入口文件才会被尝试载入,根目录以及 3 级以下不会加载,示例 :

      plugins/hello/index.js  //会被当做插件载入
      plugins/index.js  //忽略
      
    • 重载:plugins 下的所有文件都是保存既重载

    • 默认识别为 Node 开发语言,否则请在插件目录加上 py_php_ 前缀

    • 内置的 JS 引擎仅支持在后台管理开发面板中进行开发

  • language

    • 存放语言支持的一些依赖,请不要动它。当然目前只有nodepython

模块方法介绍

async sleep

Node 休眠(阻塞运行),传 number,注意单位是毫秒

//内置js
time.sleep(5000);
//node
await sleep(5000);
#python
await asyncio.sleep(5)

utils 工具包

buildCQTag 拼接 CQ 码

本项目消息机遇字符串,图片和视频等通过 CQ 码实现,因此提供了拼接方法

/**
 * @author 猫咪
 * @origin 傻妞官方
 * @version v1.0.0
 * @create_at 2022-09-08 07:31:42
 * @title 二维码
 * @rule 二维码 ?
 * @description 🐒生成指定内容的一个二维码。
 * @public true
 * @icon https://bpic.51yuansu.com/pic3/cover/02/70/77/5a1878243d237_610.jpg
 * @class 图片
 */
//内置js
let url = `https://api.pwmqr.com/qrcode/create/?url=${encodeURIComponent(s.param(1))}`;
s.reply(strings.buildCQTag("image", { url }));
//等价写法
s.reply(image(url));
//node
const {
  sender: s,
  utils: { buildCQTag, image },
} = require("sillygirl");

(async () => {
  let url = `https://api.pwmqr.com/qrcode/create/?url=${encodeURIComponent(
    await s.param(1)
  )}`;
  await s.reply(buildCQTag("image", { url }));
  //等价写法
  await s.reply(image(url));
})();
#python
from sillygirl import sender as s, utils
from urllib.parse import quote
import asyncio

async def main():
  url = f"https://api.pwmqr.com/qrcode/create/?url={quote(await s.param(1))}"
  await s.reply(utils.buildCQTag("image", { url }))
  #等价写法
  await s.reply(utils.image(url))

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

parseCQText 解析 CQ 码

/**
 * @title 钉钉机器人
 * @create_at 2023-07-03 08:07:43
 * @description 🐔钉钉机器人适配器。
 * @author 佚名
 * @version v1.0.3
 * @public true
 * @class 机器人
 * @service true
 */

const {
  utils: { parseCQText },
} = require("sillygirl");

(async () => {
  let adapter = new Adapter({
    platform: "dingtalk",
    bot_id: wsdata.msg.user.id,
    replyHandler: async ({ user_id, chat_id, content }) => {
      for (let item of parseCQText(content)) {
        if (typeof item == "string") {
          // item 此时是文本,执行发送文本消息逻辑
        }
        if (item.type == "image") {
          let url = item.params.url;
          // url 是图片链接,执行发送图片消息逻辑
        }
        if (item.type == "video") {
          let url = item.params.url;
          // url 是视频链接,执行发送视频消息逻辑
        }
      }
      return "${message_id}";
    },
  });
})();

utils.npmInstall 安装 npm 包 (待实现)

await utils.npmInstall("request"); //会返回执行信息String
await utils.npmInstall("request", { outConsole: true }); // 将会在控制台实时打印安装情况,返回结果为null

utils.testModule 测试 npm 包是否存在 (待实现)

await utils.testModule(["telegram", "input"]); //将只测试,返回结果
await utils.testModule(["telegram", "input"], { install: true }); //发现少模块自动安装

Adapter 适配器构造类

  • 具体使用方法参见适配器开发

Bucket 数据库构造类

系统数据库为轻量型内嵌式 KV 数据库。

开发插件时,你可以使用任意你喜欢的数据库来存放数据 (不建议,会破坏用户使用体验)

有关数据库操作,你可以在官方插件中找到全部用法示例

Linux 存放于 /etc/sillyplus

Windows 存放于 C:\ProgramData\sillyplus

Darwin 存放于 .sillyplus

建议定期备份

get()获取数据

该方法接受二个参数

  • 需要读取的 key
  • 未读取到值默认返回值(可选)

    //内置js
    const test = new Bucket("test");
    //读取一个key的 value 值 如果没有该数据返回undefined
    consoloe.log(test.name1)// undefined
    consoloe.log(test["name1"])// undefined
    // 第二个参数作为未读取到数据的返回值
    test.get("name1", "阿明")// 阿明
    
    //node
    const test = new Bucket("test");
    await test.get("name1");
    await test.get("name1", "阿明");
    
    #python
    test = Bucket("test")
    await test.get("name1") #None
    await test.get("name1", "阿明")
    
    #支持同步,小心阻塞watch方法
    print(test.name1) #None
    test.name1 = "阿明"
    print(test.name1)
    

set()存储数据

该方法接受三个参数

  • 需要设置的 key
  • 需要设置的 value

    //创建一个测试数据库实例
    const test = new Bucket('test');
    
    const {
    changed // boolean 实际值是否变更
    message // string 修改回调消息,修改可能被变更或拦截
    error // string 修改失败时的错误消息
    } = await test.set('name', '阿明');
    

delete()删除数据

该方法接受一个参数

  • 需要删除的 key

返回结果同 set

getAll() 读取所有键值对

empty() 清空数据库

keys()读取所有 key 返回一个 Array

返回一个字符串数组

watch() 监听数据变更,可进行拦截、修改

# python
from sillygirl import Bucket
import asyncio

test = Bucket("test")

async def main():
    async def handle(old_value, new_value, key):
        print("Bucket value changed!")
        print("Key:", key)
        print("Old value:", old_value)
        print("New value:", new_value)
        new_value = f"new {new_value}"
        print("New New value:", new_value)
        return {
            "now": new_value,
            #"error": "具体错误,将会撤销修改"
            #"message": "携带的额外信息"
        }

    test.watch("*", handle)
    await asyncio.Queue().get()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

适配器开发

元信息配置

通过 jsdoc 注释的方式来定义元信息,service true 时,将随程序一起执行

/**
 * @service true
 */

初始化一个适配器

基本使用方法:

/**
 * @title 钉钉机器人
 * @create_at 2023-07-30 11:09:12
 * @description 🐒钉钉机器人适配器。
 * @author cdle
 * @version v1.0.0
 * @service true
 */
//node
const { Adapter, sleep } = require("sillygirl");

const bot_id = "";
const platform = "dingtalk";

const dingtalk = new Adapter({
  platform,
  bot_id,
  // 处理 Sender.reply 回复的消息
  replyHandler: ({
    user_id, //用户ID
    chat_id, //群聊ID,为什么不用group_id,因为本人觉得别扭、不对齐
    message_id, //消息ID
    content, //消息内容,其中图片、文本使用CQ码实现
    //... 自定义参数,来自 Adapter.receive
  }) => {
    //... 实际消息发送逻辑
    return "${message_id}"; //返回消息ID,用于消息撤回等处理
  },
  // 处理 Sender.doAction 发出的指令
  actionHandler: (
    {
      //... 系列参数,如删除消息:{ type: "delete_message", message_id }
    }
  ) => {
    return "${action_result}"; //返回结果
  },
});

//向框架内部发送信息
dingtalk.receive({
  user_id: "用户ID",
  chat_id: "群聊ID",
  message_id: "消息ID",
  content: "消息内容",
  //以下为可选
  user_name: "用户名", //用户名称
  chat_name: "群聊名名", //群聊名称
  //... 自定义参数
});

//向框架外部推送消息,无视 unreply
dingtalk.push({
  ser_id: "用户ID",
  chat_id: "群聊ID",
  message_id: "消息ID",
  content: "消息内容",
});

//获取 sender 实例
const ns = dingtalk.sender({
  user_id: "用户ID",
  chat_id: "群聊ID",
  message_id: "消息ID",
  content: "消息内容",
});

// 1秒后主动销毁适配器,会抛出异常注意捕获
// sleep(1000).then(() => {
//   dingtalk.destroy();
// });
#python
from sillygirl import Adapter, console
async def main():
    def replyHandler(message):
        console.log("message", message)
        return "ok"
    bot = Adapter(platform="terminal2", bot_id="default", replyHandler=replyHandler)
    ns = await bot.sender({"user_id": "user_id"})
    console.log(await ns.reply("hello"))
    # await bot.destroy()
    await asyncio.sleep(10)

插件开发

sender 方法合集

开发插件前,我们先了解一下 sender,该方法只有在插件中可用

导入 sender 为 s

/**
 * @rule Hello ?
 */

const { sender: s } = require("sillygirl");

sender.getUserId() 获取用户 id

//内置js
console.log(s.getUserId())
//node
s.getUserId().then((user_id) => console.log(`用户ID:${user_id}`));
#python
print(await s.getUserId())

sender.getChatId() 获取群聊 id

s.getChatId().then((chat_id) => console.log(`群聊ID:${chat_id}`));

sender.getMessageId() 获取消息 id

s.getMessageId().then((message_id) => console.log(`消息ID:${message_id}`));

sender.getContent() 获取消息内容

s.getContent().then((content) => console.log(`消息内容:${content}`));

sender.param() 获取参数,对应元消息中的 rule 中子匹配

s.param(1).then((param1) => console.log(`参数1:${param1}`));

sender.reply() 回复消息

s.reply("你好").then((message_id) => console.log(`消息ID:${message_id}`));

sender.listen() 监听消息

例 1

(async () => {
  await s.reply("请输入11位手机号:");
  let s2 = await s.listen({
    rules: ["^\\d{11}$"],
  });
  let phone = await s2.getContent();
  await s.reply(`你的手机号码是:${phone}`);
  //... 业务逻辑
})();

例 2

(async () => {
  await s.reply("请在10秒内输入11位手机号:");
  let s2 = await s.listen({
    rules: ["^\\d{11}$"],
    timeout: 10000,
  });
  if (!s2) {
    await s.reply("输入超时!");
  } else {
    let phone = await s2.getContent();
    await s.reply(`你的手机号码是:${phone}`);
    //... 业务逻辑
  }
})();

sender.holdOn() 继续监听消息

例 3

(async () => {
  await s.reply("请在10秒内输入11位手机号:");
  let phone = "";
  await s.listen({
    handle: async (s) => {
      phone = await s.getContent();
      if (phone.length != 11) {
        return s.holdOn("错误,请重新输入(直到正确):");
      } else {
        return "输入正确。";
      }
    },
  });
  await s.reply(`你的手机号码是:${phone}`);
  //... 业务逻辑
})();

sender.doAction() 发起指令

撤回消息

(async () => {
  const { sleep } = require("sillygirl");
  let message_id = await s.reply("本消息3秒后撤回!");
  await sleep(3000);
  await s.doAction({
    type: "delete_message",
    message_id,
  });
})();

sender.getEvent() 获取事件

sender.isAdmin() 是否管理员消息

sender.isAdmin().then((bool) => console.log(`${bool ? "是" : "不是"}管理员。`));

sender.getUserName() 获取用户昵称

s.getUserName().then((user_name) => console.log(user_name));

sender.getChatName() 获取群聊昵称

s.getChatName().then((v) => console.log(v));

sender.getPlatform() 获取消息平台

s.getPlatform().then((v) => console.log(v));

sender.getBotId() 获取机器人 id

s.getBotId().then((v) => console.log(v));

sender.getAdapter() 获取适配器

s.getAdapter().then((adapter) => console.log(adapter););

sender.continue() 消息继续向下传递

sender.setContent() 篡改消息内容

sender.destroy() 销毁会话,

脚本结束后自动调用,如果脚本长期不结束需要用手动调用避免内存泄漏

元信息配置

/**作者
 * @author Cdle
 * 插件名
 * @name 日常命令
 * 组织名
 * @origin 傻妞官方
 * 版本号
 * @version 1.0.5
 * 说明
 * @description 🐷日常命令,个人认为会比较便捷
 * 限制平台 不在该范围内的平台消息该插件不会被触发
 * @platform tg qq
 * 触发正则,^开头或raw 开头
 * @rule ^你好 ([^\n]+)$
 * @rule raw 你好
 * 直接用问号代替要匹配的内容
 * @rule 你好 ?
 * 使用方括号代替要匹配的内容,s.param("姓名") 取参
 * @rule 你好 [姓名]
 * 参数名加?表示为选值
 * @rule 你好 [姓名?]
 * // 是否管理员才能触发命令
 * @admin true
 * // 是否发布插件
 * @public false
 * // 插件优先级,越大优先级越高  如果两个插件正则一样,则优先级高的先被匹配
 * @priority 9999
 * // 是否禁用插件
 * @disable false
 * // 每5小时运行一次插件,触发时 platform 为 cron
 * @cron 0 0 *\/5 * * *
 * // 是否服务模块,会在系统启动时执行该插件内容,触发时 platform 为 *
 * @service false
 * // 给插件设置一个好看的头像
 * @icon https://wx.zsxq.com/dweb2/assets/images/favicon_32.ico

写一个简单的 hello world

/**作者
 * @author Cdle
 * @name Hello
 * @version 1.0.5
 * @description 你好,世界!
 * @rule [参数1:hello,你好] [参数2]
 * @admin false
 * @public false
 * @priority 1
 * @disable false
 */

(async (s) => {
  let name = await s.param("参数2");
  await s.reply(`你好,我是${name}`);
})();

插件加密(暂未实现)

为保护开发者知识产权,如果不想公开插件源码,可以用块级注释来包裹需要加密的内容 /*hidden*/ 开始,/*neddih*/结束,必须严格按照要求,多/少空格都不会被识别到

/*hidden*/
let key = 1234;
/*neddih*/

console.log("key");

到底啦~ 详细的开发还是要靠插件来说明,多看看官方插件吧~~