From 9ed4c1abb147d59f77ad34b5d345d8ec1800c0f6 Mon Sep 17 00:00:00 2001 From: snowy Date: Wed, 21 Aug 2024 18:05:04 +0800 Subject: [PATCH 1/5] =?UTF-8?q?:memo:=20=E6=96=87=E6=A1=A3=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=BA=90=E4=BB=A3=E7=A0=81=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- liteyuki/mkdoc.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/liteyuki/mkdoc.py b/liteyuki/mkdoc.py index fb179473..7463eca2 100644 --- a/liteyuki/mkdoc.py +++ b/liteyuki/mkdoc.py @@ -209,13 +209,14 @@ def get_module_info_normal(file_path: str, ignore_private: bool = True) -> Modul return module_info -def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str: +def generate_markdown(module_info: ModuleInfo, front_matter=None, lang: str = "zh-CN") -> str: """ 生成模块的Markdown 你可在此自定义生成的Markdown格式 Args: module_info: 模块信息 front_matter: 自定义选项title, index, icon, category + lang: 语言 Returns: Markdown 字符串 """ @@ -261,7 +262,13 @@ def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str: method.docstring = method.docstring.replace("\n", "\n\n") content += f" {method.docstring}\n\n" # 函数源代码可展开区域 - content += f"
\n源代码\n\n```python\n{method.source_code}\n```\n
\n\n" + + if lang == "zh-CN": + TEXT_SOURCE_CODE = "源代码" + else: + TEXT_SOURCE_CODE = "Source Code" + + content += f"
\n{TEXT_SOURCE_CODE}\n\n```python\n{method.source_code}\n```\n
\n\n" for attr in cls.attributes: content += f"###   ***attr*** `{attr.name}: {attr.type}`\n\n" @@ -278,7 +285,7 @@ def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str: return content -def generate_docs(module_folder: str, output_dir: str, with_top: bool = False, ignored_paths=None): +def generate_docs(module_folder: str, output_dir: str, with_top: bool = False, lang: str = "zh-CN", ignored_paths=None): """ 生成文档 Args: @@ -286,6 +293,7 @@ def generate_docs(module_folder: str, output_dir: str, with_top: bool = False, i output_dir: 输出文件夹 with_top: 是否包含顶层文件夹 False时例如docs/api/module_a, docs/api/module_b, True时例如docs/api/module/module_a.md, docs/api/module/module_b.md ignored_paths: 忽略的路径 + lang: 语言 """ if ignored_paths is None: ignored_paths = [] @@ -345,5 +353,5 @@ def generate_docs(module_folder: str, output_dir: str, with_top: bool = False, i # 入口脚本 if __name__ == '__main__': # 这里填入你的模块路径 - generate_docs('liteyuki', 'docs/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"]) - generate_docs('liteyuki', 'docs/en/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"]) + generate_docs('liteyuki', 'docs/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"], lang="zh-CN") + generate_docs('liteyuki', 'docs/en/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"], lang="en") From a3a31a2c9465aaaa98b35dc6ba1f0d675fb83e82 Mon Sep 17 00:00:00 2001 From: snowy Date: Thu, 22 Aug 2024 07:23:44 +0800 Subject: [PATCH 2/5] =?UTF-8?q?:fire:=20=E7=A7=BB=E9=99=A4=E9=80=9A?= =?UTF-8?q?=E9=81=93=E7=9A=84=E9=83=A8=E5=88=86=E7=89=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- liteyuki/comm/channel.py | 10 ---------- src/nonebot_plugins/to_liteyuki.py | 6 +++++- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index ce2db2b4..2fc9d462 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,5 @@ dist doc -mkdoc2.py \ No newline at end of file +mkdoc2.py +result.json \ No newline at end of file diff --git a/liteyuki/comm/channel.py b/liteyuki/comm/channel.py index 3355be08..8adb487d 100644 --- a/liteyuki/comm/channel.py +++ b/liteyuki/comm/channel.py @@ -217,16 +217,6 @@ class Channel(Generic[T]): data = self.conn_recv.recv() self._run_on_sub_receive_funcs(data) - def __iter__(self): - return self - - def __next__(self) -> Any: - return self.receive() - - def __del__(self): - self.close() - logger.debug(f"Channel {self.name} deleted.") - """子进程可用的主动和被动通道""" active_channel: Optional["Channel"] = None diff --git a/src/nonebot_plugins/to_liteyuki.py b/src/nonebot_plugins/to_liteyuki.py index 8358db5c..ddf0c15d 100644 --- a/src/nonebot_plugins/to_liteyuki.py +++ b/src/nonebot_plugins/to_liteyuki.py @@ -8,6 +8,8 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved @File : to_liteyuki.py @Software: PyCharm """ +import asyncio + from nonebot import Bot, get_bot, on_message from nonebot.plugin import PluginMetadata from nonebot.adapters.onebot.v11 import MessageEvent, Bot @@ -37,6 +39,8 @@ async def _(bot: Bot, event: MessageEvent): @shared_memory.on_subscriber_receive("event_to_nonebot") -async def _(event: MessageEvent): +async def _(event: LiteyukiMessageEvent): bot: Bot = get_bot(event.bot_id) + print("A") await bot.send_msg(message_type=event.message_type, user_id=int(event.session_id), group_id=int(event.session_id), message=event.data["message"]) + print("B") \ No newline at end of file From 4bf8512a7dd85402baf0f2db99f4fccb7d9ade48 Mon Sep 17 00:00:00 2001 From: snowy Date: Thu, 22 Aug 2024 09:35:02 +0800 Subject: [PATCH 3/5] =?UTF-8?q?:sparkles:=20=E6=96=B0=E5=A2=9Eon=5Fkeyword?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- liteyuki/__init__.py | 4 ++- liteyuki/comm/channel.py | 2 +- liteyuki/comm/storage.py | 6 ++--- liteyuki/message/matcher.py | 13 +++++----- liteyuki/message/on.py | 11 ++++++-- liteyuki/message/rule.py | 21 ++++++++++++---- liteyuki/utils.py | 11 ++++++++ src/liteyuki_plugins/anti_dislink.py | 24 ++++++++++++++++++ src/liteyuki_plugins/hello_liteyuki.py | 4 +-- src/liteyuki_plugins/ts_chan_main.py | 26 +++++++++++++++++++ src/nonebot_plugins/to_liteyuki.py | 2 -- src/nonebot_plugins/ts_chan_sub.py | 35 ++++++++++++++++++++++++++ 12 files changed, 137 insertions(+), 22 deletions(-) create mode 100644 src/liteyuki_plugins/anti_dislink.py create mode 100644 src/liteyuki_plugins/ts_chan_main.py create mode 100644 src/nonebot_plugins/ts_chan_sub.py diff --git a/liteyuki/__init__.py b/liteyuki/__init__.py index 3bde5ff4..c96f3ed1 100644 --- a/liteyuki/__init__.py +++ b/liteyuki/__init__.py @@ -33,7 +33,9 @@ __all__ = [ "logger", ] -__version__ = "6.3.8" # 测试版本号 +__version__ = "6.3.9" # 测试版本号 +# 6.3.9 +# 更改了on语法 # 6.3.8 # 1. 初步添加对聊天的支持 diff --git a/liteyuki/comm/channel.py b/liteyuki/comm/channel.py index 8adb487d..6ab737b0 100644 --- a/liteyuki/comm/channel.py +++ b/liteyuki/comm/channel.py @@ -38,7 +38,7 @@ class Channel(Generic[T]): 有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器 """ - def __init__(self, _id: str, type_check: Optional[bool] = None): + def __init__(self, _id: str = "", type_check: Optional[bool] = None): """ 初始化通道 Args: diff --git a/liteyuki/comm/storage.py b/liteyuki/comm/storage.py index e3f34aa5..97c3de76 100644 --- a/liteyuki/comm/storage.py +++ b/liteyuki/comm/storage.py @@ -8,7 +8,7 @@ from typing import Any, Coroutine, Optional, TypeAlias, Callable from liteyuki.comm import channel from liteyuki.comm.channel import Channel, ON_RECEIVE_FUNC, ASYNC_ON_RECEIVE_FUNC -from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable, run_coroutine +from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable, run_coroutine, run_coroutine_in_thread if IS_MAIN_PROCESS: _locks = {} @@ -220,10 +220,10 @@ class KeyValueStore: """ if IS_MAIN_PROCESS: if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]: - run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]]) + run_coroutine_in_thread(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]]) else: if channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]: - run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]]) + run_coroutine_in_thread(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]]) def _start_receive_loop(self): """ diff --git a/liteyuki/message/matcher.py b/liteyuki/message/matcher.py index 5585fa98..d2add762 100644 --- a/liteyuki/message/matcher.py +++ b/liteyuki/message/matcher.py @@ -34,16 +34,17 @@ class Matcher: def __str__(self): return f"Matcher(rule={self.rule}, priority={self.priority}, block={self.block})" - def handle(self, handler: EventHandler) -> EventHandler: + def handle(self) -> Callable[[EventHandler], EventHandler]: """ 添加处理函数,装饰器 - Args: - handler: Returns: - EventHandler + 装饰器 handler """ - self.handlers.append(handler) - return handler + def decorator(handler: EventHandler) -> EventHandler: + self.handlers.append(handler) + return handler + + return decorator async def run(self, event: MessageEvent) -> None: """ diff --git a/liteyuki/message/on.py b/liteyuki/message/on.py index bbc161e4..a450a85f 100644 --- a/liteyuki/message/on.py +++ b/liteyuki/message/on.py @@ -15,7 +15,7 @@ from liteyuki.comm.storage import shared_memory from liteyuki.log import logger from liteyuki.message.event import MessageEvent from liteyuki.message.matcher import Matcher -from liteyuki.message.rule import Rule +from liteyuki.message.rule import Rule, empty_rule _matcher_list: list[Matcher] = [] _queue: Queue = Queue() @@ -34,7 +34,7 @@ async def _(event: MessageEvent): break -def on_message(rule: Rule = Rule(), priority: int = 0, block: bool = True) -> Matcher: +def on_message(rule: Rule = empty_rule, priority: int = 0, block: bool = False) -> Matcher: matcher = Matcher(rule, priority, block) # 按照优先级插入 for i, m in enumerate(_matcher_list): @@ -44,3 +44,10 @@ def on_message(rule: Rule = Rule(), priority: int = 0, block: bool = True) -> Ma else: _matcher_list.append(matcher) return matcher + + +def on_keywords(keywords: list[str], rule=empty_rule, priority: int = 0, block: bool = False) -> Matcher: + @Rule + async def on_keywords_rule(event: MessageEvent): + return any(keyword in event.raw_message for keyword in keywords) + return on_message(on_keywords_rule & rule, priority, block) diff --git a/liteyuki/message/rule.py b/liteyuki/message/rule.py index 31af299e..56c4c541 100644 --- a/liteyuki/message/rule.py +++ b/liteyuki/message/rule.py @@ -8,26 +8,37 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved @File : rule.py @Software: PyCharm """ - +import inspect from typing import Optional, TypeAlias, Callable, Coroutine from liteyuki.message.event import MessageEvent -RuleHandler: TypeAlias = Callable[[MessageEvent], Coroutine[None, None, bool]] +RuleHandlerFunc: TypeAlias = Callable[[MessageEvent], Coroutine[None, None, bool]] """规则函数签名""" class Rule: - def __init__(self, handler: Optional[RuleHandler] = None): + def __init__(self, handler: RuleHandlerFunc): self.handler = handler def __or__(self, other: "Rule") -> "Rule": - return Rule(lambda event: self.handler(event) or other.handler(event)) + async def combined_handler(event: MessageEvent) -> bool: + return await self.handler(event) or await other.handler(event) + + return Rule(combined_handler) def __and__(self, other: "Rule") -> "Rule": - return Rule(lambda event: self.handler(event) and other.handler(event)) + async def combined_handler(event: MessageEvent) -> bool: + return await self.handler(event) and await other.handler(event) + + return Rule(combined_handler) async def __call__(self, event: MessageEvent) -> bool: if self.handler is None: return True return await self.handler(event) + + +@Rule +async def empty_rule(event: MessageEvent) -> bool: + return True diff --git a/liteyuki/utils.py b/liteyuki/utils.py index 90a2048f..6eb646a1 100644 --- a/liteyuki/utils.py +++ b/liteyuki/utils.py @@ -5,6 +5,7 @@ import asyncio import inspect import multiprocessing +import threading from pathlib import Path from typing import Any, Callable, Coroutine @@ -61,6 +62,16 @@ def run_coroutine(*coro: Coroutine): # 捕获其他异常,防止协程被重复等待 logger.error(f"Exception occurred: {e}") +def run_coroutine_in_thread(*coro: Coroutine): + """ + 在新线程中运行协程 + Args: + coro: + + Returns: + + """ + threading.Thread(target=run_coroutine, args=coro, daemon=True).start() def path_to_module_name(path: Path) -> str: """ diff --git a/src/liteyuki_plugins/anti_dislink.py b/src/liteyuki_plugins/anti_dislink.py new file mode 100644 index 00000000..7ef8387d --- /dev/null +++ b/src/liteyuki_plugins/anti_dislink.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +""" +Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved + +@Time : 2024/8/22 上午9:06 +@Author : snowykami +@Email : snowykami@outlook.com +@File : anti_dislink.py +@Software: PyCharm +""" +import random +from liteyuki.plugin import PluginMetadata, PluginType + +from liteyuki.message.on import on_keywords + +__plugin_meta__ = PluginMetadata( + name="严禁断联化", + type=PluginType.APPLICATION +) + + +@on_keywords(["看看你的", "看看j", "给我看看"]).handle() +async def _(event): + event.reply(random.choice(["No dislink", "严禁断联化"])) diff --git a/src/liteyuki_plugins/hello_liteyuki.py b/src/liteyuki_plugins/hello_liteyuki.py index 72d1ac42..86314c34 100644 --- a/src/liteyuki_plugins/hello_liteyuki.py +++ b/src/liteyuki_plugins/hello_liteyuki.py @@ -14,11 +14,11 @@ from liteyuki.message.event import MessageEvent __plugin_meta__ = PluginMetadata( name="你好轻雪", - type=PluginType.TEST + type=PluginType.APPLICATION ) -@on_message().handle +@on_message().handle() async def _(event: MessageEvent): if str(event.raw_message) == "你好轻雪": event.reply("你好呀") diff --git a/src/liteyuki_plugins/ts_chan_main.py b/src/liteyuki_plugins/ts_chan_main.py new file mode 100644 index 00000000..4b5f3280 --- /dev/null +++ b/src/liteyuki_plugins/ts_chan_main.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved + +@Time : 2024/8/22 上午8:37 +@Author : snowykami +@Email : snowykami@outlook.com +@File : ts_chan_main.py +@Software: PyCharm +""" +import asyncio + +from liteyuki.comm import Channel, set_channel, get_channel +from liteyuki import get_bot + +set_channel("chan-main", Channel("chan-main")) +set_channel("chan-sub", Channel("chan-sub")) + +chan_main = get_channel("chan-main") + + +# @get_bot().on_after_start +# async def _(): +# while True: +# chan_main.send("Hello, World!") +# await asyncio.sleep(5) diff --git a/src/nonebot_plugins/to_liteyuki.py b/src/nonebot_plugins/to_liteyuki.py index ddf0c15d..cba15f22 100644 --- a/src/nonebot_plugins/to_liteyuki.py +++ b/src/nonebot_plugins/to_liteyuki.py @@ -41,6 +41,4 @@ async def _(bot: Bot, event: MessageEvent): @shared_memory.on_subscriber_receive("event_to_nonebot") async def _(event: LiteyukiMessageEvent): bot: Bot = get_bot(event.bot_id) - print("A") await bot.send_msg(message_type=event.message_type, user_id=int(event.session_id), group_id=int(event.session_id), message=event.data["message"]) - print("B") \ No newline at end of file diff --git a/src/nonebot_plugins/ts_chan_sub.py b/src/nonebot_plugins/ts_chan_sub.py new file mode 100644 index 00000000..3a2c6665 --- /dev/null +++ b/src/nonebot_plugins/ts_chan_sub.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved + +@Time : 2024/8/22 上午8:39 +@Author : snowykami +@Email : snowykami@outlook.com +@File : ts_chan_sub.py +@Software: PyCharm +""" +import asyncio + +from liteyuki.comm import Channel, get_channel +from nonebot import get_bot +from nonebot.adapters.onebot.v11 import Bot +chan_main = get_channel("chan-main") + + +# @chan_main.on_receive() +# async def _(data: str): +# print("Received data from chan-main:", data) +# try: +# bot: Bot = get_bot("2443429204") # type: ignore +# +# def send_msg(): +# +# bot.send_msg(message_type="private", user_id=2443429204, message=data) +# +# print("tsA") +# print("tsA1") +# await asyncio.ensure_future(c) +# print("tsB") +# except Exception as e: +# print(e) +# pass From aa2d182840298193780765440c75850206d2d8cb Mon Sep 17 00:00:00 2001 From: snowy Date: Thu, 22 Aug 2024 10:10:03 +0800 Subject: [PATCH 4/5] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E8=BD=BB?= =?UTF-8?q?=E9=9B=AA=E4=B8=8ENoneBot=E5=AF=B9=E6=8E=A5=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E7=9A=84=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/nonebot_plugins/to_liteyuki.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/nonebot_plugins/to_liteyuki.py b/src/nonebot_plugins/to_liteyuki.py index cba15f22..f8410385 100644 --- a/src/nonebot_plugins/to_liteyuki.py +++ b/src/nonebot_plugins/to_liteyuki.py @@ -41,4 +41,7 @@ async def _(bot: Bot, event: MessageEvent): @shared_memory.on_subscriber_receive("event_to_nonebot") async def _(event: LiteyukiMessageEvent): bot: Bot = get_bot(event.bot_id) - await bot.send_msg(message_type=event.message_type, user_id=int(event.session_id), group_id=int(event.session_id), message=event.data["message"]) + if event.message_type == "private": + await bot.send_private_msg(user_id=int(event.session_id), message=event.data["message"]) + elif event.message_type == "group": + await bot.send_group_msg(group_id=int(event.session_id), message=event.data["message"]) From 71476560e45d5af2c65db4777803cb66fdba35d6 Mon Sep 17 00:00:00 2001 From: snowy Date: Thu, 22 Aug 2024 10:41:41 +0800 Subject: [PATCH 5/5] =?UTF-8?q?:memo:=20=E5=95=86=E5=BA=97=E6=96=B0?= =?UTF-8?q?=E5=A2=9Eanti-dislink=E6=8F=92=E4=BB=B6=E5=92=8Ctag=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/components/PluginItemCard.vue | 26 ++++++++++++++++++++ docs/.vuepress/public/assets/plugins.json | 26 ++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/docs/.vuepress/components/PluginItemCard.vue b/docs/.vuepress/components/PluginItemCard.vue index bd8906c9..d74e88fb 100644 --- a/docs/.vuepress/components/PluginItemCard.vue +++ b/docs/.vuepress/components/PluginItemCard.vue @@ -2,6 +2,9 @@
{{ props.item.name }}
{{ props.item.desc }}
+
+ {{ tag.label }} +
@@ -45,6 +48,21 @@ const copyToClipboard = () => { }) } +const getTagStyle = (backgroundColor: string) => { + // 将颜色值转换为 RGB 格式 + const rgb = backgroundColor.replace(/^#/, ''); + const [r, g, b] = rgb.match(/.{2}/g).map(x => parseInt(x, 16)); + + // 计算亮度 + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + + // 根据亮度决定文字颜色 + return { + backgroundColor: backgroundColor, + color: brightness > 128 ? '#000' : '#fff' + }; +}; + // 复制到剪贴板的函数 @@ -123,4 +141,12 @@ button { justify-content: space-between; color: #00000055; } + +.tag { + display: inline-block; + padding: 2px 5px; + margin-right: 5px; + border-radius: 5px; + font-size: 12px; +} \ No newline at end of file diff --git a/docs/.vuepress/public/assets/plugins.json b/docs/.vuepress/public/assets/plugins.json index 946cccbb..d24deb4a 100644 --- a/docs/.vuepress/public/assets/plugins.json +++ b/docs/.vuepress/public/assets/plugins.json @@ -3,12 +3,12 @@ "module_name": "liteyukibot-plugin-nonebot", "project_link": "liteyukibot-plugin-nonebot", "name": "NoneBot插件", - "desc": "在轻雪中使用NoneBot,为NoneBot开发者提供了更多便捷功能(已内置)", + "desc": "在轻雪中使用NoneBot,内置轻雪--NoneBot会话控制器插件,为NoneBot开发者提供了更多便捷功能(已内置)", "author": "snowykami", "homepage": "https://github.com/LiteyukiStudio/liteyukibot-plugin-nonebot", "tags": [ { - "label": "server", + "label": "app", "color": "#aeeaa8" } ], @@ -16,5 +16,27 @@ "type": "application", "valid": true, "version": "rolling" + }, + { + "module_name": "liteyukibot-plugin-antidislink", + "project_link": "liteyukibot-plugin-antidislink", + "name": "防断联插件", + "desc": "防止你的群友断联化", + "author": "snowykami", + "homepage": "https://github.com/snowykami/liteyukibot-plugin-antidislink", + "tags": [ + { + "label": "app", + "color": "#aeeaa8" + }, + { + "label": "dislink", + "color": "#d0e9ff" + } + ], + "is_official": true, + "type": "application", + "valid": true, + "version": "rolling" } ]