diff --git a/.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md b/.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md
index 73f5009ca..0358a5b27 100644
--- a/.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md
+++ b/.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md
@@ -17,6 +17,7 @@ assignees: ''
{
"name": "插件名",
"desc": "插件介绍",
+ "author": "作者名",
"repo": "插件仓库链接",
"tags": [],
"social_link": ""
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..be006de9a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+# Keep GitHub Actions up to date with GitHub's Dependabot...
+# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
+# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
+version: 2
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ groups:
+ github-actions:
+ patterns:
+ - "*" # Group all Actions updates into a single larger pull request
+ schedule:
+ interval: weekly
diff --git a/.github/workflows/auto_release.yml b/.github/workflows/auto_release.yml
index 854a69f16..46d914a9a 100644
--- a/.github/workflows/auto_release.yml
+++ b/.github/workflows/auto_release.yml
@@ -73,7 +73,7 @@ jobs:
uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: '3.10'
diff --git a/.github/workflows/coverage_test.yml b/.github/workflows/coverage_test.yml
index 30e9237ed..e9c94d679 100644
--- a/.github/workflows/coverage_test.yml
+++ b/.github/workflows/coverage_test.yml
@@ -1,6 +1,6 @@
name: Run tests and upload coverage
-on:
+on:
push:
branches:
- master
@@ -8,6 +8,7 @@ on:
- 'README.md'
- 'changelogs/**'
- 'dashboard/**'
+ pull_request:
workflow_dispatch:
jobs:
@@ -21,25 +22,24 @@ jobs:
fetch-depth: 0
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- pip install -r requirements.txt
- pip install pytest pytest-cov pytest-asyncio
+ pip install pytest pytest-asyncio pytest-cov
+ pip install --editable .
- name: Run tests
run: |
- mkdir data
- mkdir data/plugins
- mkdir data/config
- mkdir data/temp
+ mkdir -p data/plugins
+ mkdir -p data/config
+ mkdir -p data/temp
export TESTING=true
export ZHIPU_API_KEY=${{ secrets.OPENAI_API_KEY }}
- PYTHONPATH=./ pytest --cov=. tests/ -v -o log_cli=true -o log_level=DEBUG
+ pytest --cov=. -v -o log_cli=true -o log_level=DEBUG
- name: Upload results to Codecov
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@v5
with:
- token: ${{ secrets.CODECOV_TOKEN }}
\ No newline at end of file
+ token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index e0e235c09..c0610a3c1 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -12,7 +12,7 @@ jobs:
steps:
- name: Pull The Codes
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0 # Must be 0 so we can fetch tags
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 310e250b6..283e99989 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -18,7 +18,7 @@ jobs:
pull-requests: write
steps:
- - uses: actions/stale@v5
+ - uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Stale issue message'
diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py
index 4a75a11bc..b3982cb13 100644
--- a/astrbot/core/config/default.py
+++ b/astrbot/core/config/default.py
@@ -6,7 +6,7 @@ import os
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
-VERSION = "3.5.18"
+VERSION = "3.5.19"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db")
# 默认配置
@@ -64,7 +64,7 @@ DEFAULT_CONFIG = {
"streaming_response": False,
"show_tool_use_status": False,
"streaming_segmented": False,
- "separate_provider": False,
+ "separate_provider": True,
},
"provider_stt_settings": {
"enable": False,
@@ -724,16 +724,16 @@ CONFIG_METADATA_2 = {
"model": "deepseek-chat",
},
},
- "智谱 AI": {
- "id": "zhipu_default",
- "type": "zhipu_chat_completion",
+ "302.AI": {
+ "id": "302ai",
+ "type": "openai_chat_completion",
"provider_type": "chat_completion",
"enable": True,
"key": [],
+ "api_base": "https://api.302.ai/v1",
"timeout": 120,
- "api_base": "https://open.bigmodel.cn/api/paas/v4/",
"model_config": {
- "model": "glm-4-flash",
+ "model": "gpt-4.1-mini",
},
},
"硅基流动": {
@@ -748,6 +748,18 @@ CONFIG_METADATA_2 = {
"model": "deepseek-ai/DeepSeek-V3",
},
},
+ "PPIO派欧云": {
+ "id": "ppio",
+ "type": "openai_chat_completion",
+ "provider_type": "chat_completion",
+ "enable": True,
+ "key": [],
+ "api_base": "https://api.ppinfra.com/v3/openai",
+ "timeout": 120,
+ "model_config": {
+ "model": "deepseek/deepseek-r1",
+ },
+ },
"Kimi": {
"id": "moonshot",
"type": "openai_chat_completion",
@@ -760,16 +772,16 @@ CONFIG_METADATA_2 = {
"model": "moonshot-v1-8k",
},
},
- "PPIO派欧云": {
- "id": "ppio",
- "type": "openai_chat_completion",
+ "智谱 AI": {
+ "id": "zhipu_default",
+ "type": "zhipu_chat_completion",
"provider_type": "chat_completion",
"enable": True,
"key": [],
- "api_base": "https://api.ppinfra.com/v3/openai",
"timeout": 120,
+ "api_base": "https://open.bigmodel.cn/api/paas/v4/",
"model_config": {
- "model": "deepseek/deepseek-r1",
+ "model": "glm-4-flash",
},
},
"Dify": {
diff --git a/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py b/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py
index dcb390b2f..c2961ded5 100644
--- a/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py
+++ b/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py
@@ -106,7 +106,6 @@ class ToolLoopAgent(BaseAgentRunner):
# 处理 LLM 响应
llm_resp = llm_resp_result
- logger.debug(f"LLMResp: {llm_resp}")
if llm_resp.role == "err":
# 如果 LLM 响应错误,转换到错误状态
@@ -219,7 +218,9 @@ class ToolLoopAgent(BaseAgentRunner):
content="返回了图片(已直接发送给用户)",
)
)
- yield MessageChain().base64_image(res.content[0].data)
+ yield MessageChain(type="tool_direct_result").base64_image(
+ res.content[0].data
+ )
elif isinstance(res.content[0], EmbeddedResource):
resource = res.content[0].resource
if isinstance(resource, TextResourceContents):
@@ -243,7 +244,9 @@ class ToolLoopAgent(BaseAgentRunner):
content="返回了图片(已直接发送给用户)",
)
)
- yield MessageChain().base64_image(res.content[0].data)
+ yield MessageChain(type="tool_direct_result").base64_image(
+ res.content[0].data
+ )
else:
tool_call_result_blocks.append(
ToolCallMessageSegment(
@@ -276,7 +279,9 @@ class ToolLoopAgent(BaseAgentRunner):
self._transition_state(AgentState.DONE)
if res := self.event.get_result():
if res.chain:
- yield MessageChain(chain=res.chain)
+ yield MessageChain(
+ chain=res.chain, type="tool_direct_result"
+ )
self.event.clear_result()
except Exception as e:
diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py
index 5923e8246..b1d9310c6 100644
--- a/astrbot/core/pipeline/process_stage/method/llm_request.py
+++ b/astrbot/core/pipeline/process_stage/method/llm_request.py
@@ -177,7 +177,13 @@ class LLMRequestSubStage(Stage):
if event.is_stopped():
return
if resp.type == "tool_call_result":
- continue # 跳过工具调用结果
+ msg_chain = resp.data["chain"]
+ if msg_chain.type == "tool_direct_result":
+ # tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
+ resp.data["chain"].type = "tool_call_result"
+ await event.send(resp.data["chain"])
+ continue
+ # 对于其他情况,暂时先不处理
if resp.type == "tool_call":
if self.streaming_response:
# 用来标记流式响应需要分节
@@ -254,11 +260,13 @@ class LLMRequestSubStage(Stage):
# 异步处理 WebChat 特殊情况
if event.get_platform_name() == "webchat":
- asyncio.create_task(self._handle_webchat(event, req))
+ asyncio.create_task(self._handle_webchat(event, req, provider))
await self._save_to_history(event, req, tool_loop_agent.get_final_llm_resp())
- async def _handle_webchat(self, event: AstrMessageEvent, req: ProviderRequest):
+ async def _handle_webchat(
+ self, event: AstrMessageEvent, req: ProviderRequest, prov: Provider
+ ):
"""处理 WebChat 平台的特殊情况,包括第一次 LLM 对话时总结对话内容生成 title"""
conversation = await self.conv_manager.get_conversation(
event.unified_msg_origin, req.conversation.cid
@@ -268,10 +276,9 @@ class LLMRequestSubStage(Stage):
latest_pair = messages[-2:]
if not latest_pair:
return
- provider = self.ctx.plugin_manager.context.get_using_provider()
cleaned_text = "User: " + latest_pair[0].get("content", "").strip()
logger.debug(f"WebChat 对话标题生成请求,清理后的文本: {cleaned_text}")
- llm_resp = await provider.text_chat(
+ llm_resp = await prov.text_chat(
system_prompt="You are expert in summarizing user's query.",
prompt=(
f"Please summarize the following query of user:\n"
@@ -333,7 +340,6 @@ class LLMRequestSubStage(Stage):
await self.conv_manager.update_conversation(
event.unified_msg_origin, req.conversation.cid, history=messages
)
- logger.debug(f"messages persisted: {messages}")
def fix_messages(self, messages: list[dict]) -> list[dict]:
"""验证并且修复上下文"""
diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py
index 58e3c9b19..7d5984416 100644
--- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py
+++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py
@@ -210,6 +210,16 @@ class WeChatPadProAdapter(Platform):
logger.error(traceback.format_exc())
return False
+ def _extract_auth_key(self, data):
+ """Helper method to extract auth_key from response data."""
+ if isinstance(data, dict):
+ auth_keys = data.get("authKeys") # 新接口
+ if isinstance(auth_keys, list) and auth_keys:
+ return auth_keys[0]
+ elif isinstance(data, list) and data: # 旧接口
+ return data[0]
+ return None
+
async def generate_auth_key(self):
"""
生成授权码。
@@ -218,28 +228,26 @@ class WeChatPadProAdapter(Platform):
params = {"key": self.admin_key}
payload = {"Count": 1, "Days": 365} # 生成一个有效期365天的授权码
+ self.auth_key = None # Reset auth_key before generating a new one
+
async with aiohttp.ClientSession() as session:
try:
async with session.post(url, params=params, json=payload) as response:
+ if response.status != 200:
+ logger.error(f"生成授权码失败: {response.status}, {await response.text()}")
+ return
+
response_data = await response.json()
- # 修正成功判断条件和授权码提取路径
- if response.status == 200 and response_data.get("Code") == 200:
- # 授权码在 Data 字段的列表中
- if (
- response_data.get("Data")
- and isinstance(response_data["Data"], list)
- and len(response_data["Data"]) > 0
- ):
- self.auth_key = response_data["Data"][0]
- logger.info(f"成功获取授权码 {self.auth_key[:8]}...")
+ if response_data.get("Code") == 200:
+ if data := response_data.get("Data"):
+ self.auth_key = self._extract_auth_key(data)
+
+ if self.auth_key:
+ logger.info("成功获取授权码")
else:
- logger.error(
- f"生成授权码成功但未找到授权码: {response_data}"
- )
+ logger.error(f"生成授权码成功但未找到授权码: {response_data}")
else:
- logger.error(
- f"生成授权码失败: {response.status}, {response_data}"
- )
+ logger.error(f"生成授权码失败: {response_data}")
except aiohttp.ClientConnectorError as e:
logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
except Exception as e:
diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py
index 2abe59d65..05747c3ff 100644
--- a/astrbot/core/provider/manager.py
+++ b/astrbot/core/provider/manager.py
@@ -40,11 +40,13 @@ class ProviderManager:
begin_dialogs = []
user_turn = True
for dialog in begin_dialogs:
- bd_processed.append({
- "role": "user" if user_turn else "assistant",
- "content": dialog,
- "_no_save": None, # 不持久化到 db
- })
+ bd_processed.append(
+ {
+ "role": "user" if user_turn else "assistant",
+ "content": dialog,
+ "_no_save": None, # 不持久化到 db
+ }
+ )
user_turn = not user_turn
if mood_imitation_dialogs:
if len(mood_imitation_dialogs) % 2 != 0:
@@ -93,15 +95,15 @@ class ProviderManager:
"""加载的 Text To Speech Provider 的实例"""
self.embedding_provider_insts: List[Provider] = []
"""加载的 Embedding Provider 的实例"""
- self.inst_map = {}
+ self.inst_map: dict[str, Provider] = {}
"""Provider 实例映射. key: provider_id, value: Provider 实例"""
self.llm_tools = llm_tools
- self.curr_provider_inst: Provider = None
+ self.curr_provider_inst: Provider | None = None
"""默认的 Provider 实例"""
- self.curr_stt_provider_inst: STTProvider = None
+ self.curr_stt_provider_inst: STTProvider | None = None
"""默认的 Speech To Text Provider 实例"""
- self.curr_tts_provider_inst: TTSProvider = None
+ self.curr_tts_provider_inst: TTSProvider | None = None
"""默认的 Text To Speech Provider 实例"""
self.db_helper = db_helper
@@ -145,21 +147,24 @@ class ProviderManager:
await self.load_provider(provider_config)
# 设置默认提供商
- self.curr_provider_inst = self.inst_map.get(
- self.provider_settings.get("default_provider_id")
+ selected_provider_id = sp.get(
+ "curr_provider", self.provider_settings.get("default_provider_id")
)
+ selected_stt_provider_id = sp.get(
+ "curr_provider_stt", self.provider_stt_settings.get("provider_id")
+ )
+ selected_tts_provider_id = sp.get(
+ "curr_provider_tts", self.provider_tts_settings.get("provider_id")
+ )
+ self.curr_provider_inst = self.inst_map.get(selected_provider_id)
if not self.curr_provider_inst and self.provider_insts:
self.curr_provider_inst = self.provider_insts[0]
- self.curr_stt_provider_inst = self.inst_map.get(
- self.provider_stt_settings.get("provider_id")
- )
+ self.curr_stt_provider_inst = self.inst_map.get(selected_stt_provider_id)
if not self.curr_stt_provider_inst and self.stt_provider_insts:
self.curr_stt_provider_inst = self.stt_provider_insts[0]
- self.curr_tts_provider_inst = self.inst_map.get(
- self.provider_tts_settings.get("provider_id")
- )
+ self.curr_tts_provider_inst = self.inst_map.get(selected_tts_provider_id)
if not self.curr_tts_provider_inst and self.tts_provider_insts:
self.curr_tts_provider_inst = self.tts_provider_insts[0]
@@ -417,7 +422,7 @@ class ProviderManager:
self.curr_tts_provider_inst = None
if getattr(self.inst_map[provider_id], "terminate", None):
- await self.inst_map[provider_id].terminate()
+ await self.inst_map[provider_id].terminate() # type: ignore
logger.info(
f"{provider_id} 提供商适配器已终止({len(self.provider_insts)}, {len(self.stt_provider_insts)}, {len(self.tts_provider_insts)})"
@@ -427,6 +432,6 @@ class ProviderManager:
async def terminate(self):
for provider_inst in self.provider_insts:
if hasattr(provider_inst, "terminate"):
- await provider_inst.terminate()
+ await provider_inst.terminate() # type: ignore
# 清理 MCP Client 连接
await self.llm_tools.mcp_service_queue.put({"type": "terminate"})
diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py
index 36a8579a6..ec1624776 100644
--- a/astrbot/core/provider/sources/openai_source.py
+++ b/astrbot/core/provider/sources/openai_source.py
@@ -487,7 +487,7 @@ class ProviderOpenAIOfficial(Provider):
if flag:
flag = False # 删除 image 后,下一条(LLM 响应)也要删除
continue
- if isinstance(context["content"], list):
+ if "content" in context and isinstance(context["content"], list):
flag = True
# continue
new_content = []
diff --git a/astrbot/core/star/filter/platform_adapter_type.py b/astrbot/core/star/filter/platform_adapter_type.py
index 0926cc337..fffaf8553 100644
--- a/astrbot/core/star/filter/platform_adapter_type.py
+++ b/astrbot/core/star/filter/platform_adapter_type.py
@@ -8,22 +8,48 @@ from typing import Union
class PlatformAdapterType(enum.Flag):
AIOCQHTTP = enum.auto()
QQOFFICIAL = enum.auto()
- VCHAT = enum.auto()
GEWECHAT = enum.auto()
TELEGRAM = enum.auto()
WECOM = enum.auto()
LARK = enum.auto()
- ALL = AIOCQHTTP | QQOFFICIAL | VCHAT | GEWECHAT | TELEGRAM | WECOM | LARK
+ WECHATPADPRO = enum.auto()
+ DINGTALK = enum.auto()
+ DISCORD = enum.auto()
+ SLACK = enum.auto()
+ KOOK = enum.auto()
+ VOCECHAT = enum.auto()
+ WEIXIN_OFFICIAL_ACCOUNT = enum.auto()
+ ALL = (
+ AIOCQHTTP
+ | QQOFFICIAL
+ | GEWECHAT
+ | TELEGRAM
+ | WECOM
+ | LARK
+ | WECHATPADPRO
+ | DINGTALK
+ | DISCORD
+ | SLACK
+ | KOOK
+ | VOCECHAT
+ | WEIXIN_OFFICIAL_ACCOUNT
+ )
ADAPTER_NAME_2_TYPE = {
"aiocqhttp": PlatformAdapterType.AIOCQHTTP,
"qq_official": PlatformAdapterType.QQOFFICIAL,
- "vchat": PlatformAdapterType.VCHAT,
"gewechat": PlatformAdapterType.GEWECHAT,
"telegram": PlatformAdapterType.TELEGRAM,
"wecom": PlatformAdapterType.WECOM,
"lark": PlatformAdapterType.LARK,
+ "dingtalk": PlatformAdapterType.DINGTALK,
+ "discord": PlatformAdapterType.DISCORD,
+ "slack": PlatformAdapterType.SLACK,
+ "kook": PlatformAdapterType.KOOK,
+ "wechatpadpro": PlatformAdapterType.WECHATPADPRO,
+ "vocechat": PlatformAdapterType.VOCECHAT,
+ "weixin_official_account": PlatformAdapterType.WEIXIN_OFFICIAL_ACCOUNT,
}
diff --git a/astrbot/core/utils/tencent_record_helper.py b/astrbot/core/utils/tencent_record_helper.py
index 9d0552c1e..2c97a01ed 100644
--- a/astrbot/core/utils/tencent_record_helper.py
+++ b/astrbot/core/utils/tencent_record_helper.py
@@ -117,7 +117,7 @@ async def audio_to_tencent_silk_base64(audio_path: str) -> tuple[str, float]:
try:
import pilk
except ImportError as e:
- raise Exception("未安装 pysilk,请执行: pip install pysilk") from e
+ raise Exception("未安装 pilk: pip install pilk") from e
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
os.makedirs(temp_dir, exist_ok=True)
diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py
index e7b086cd1..b704a8888 100644
--- a/astrbot/dashboard/routes/chat.py
+++ b/astrbot/dashboard/routes/chat.py
@@ -166,13 +166,12 @@ class ChatRoute(Route):
type = result.get("type")
cid = result.get("cid")
streaming = result.get("streaming", False)
+ chain_type = result.get("chain_type")
yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n"
await asyncio.sleep(0.05)
if streaming and type != "end":
- continue
-
- if type == "update_title":
+ # If the result is still streaming, we continue to wait for more data
continue
if result_text:
@@ -189,7 +188,11 @@ class ChatRoute(Route):
self.db.update_conversation(
username, cid, history=json.dumps(history)
)
- break
+ if chain_type not in ["tool_call", "tool_call_result"]:
+ # If the result is not a tool call or tool call result,
+ # we can break the loop and end the stream
+ break
+
except BaseException as _:
logger.debug(f"用户 {username} 断开聊天长连接。")
return
diff --git a/changelogs/v3.5.19.md b/changelogs/v3.5.19.md
new file mode 100644
index 000000000..cb821cef6
--- /dev/null
+++ b/changelogs/v3.5.19.md
@@ -0,0 +1,10 @@
+# What's Changed
+
+1. 修复: 通过 provider 指令设置提供商,重启后失效
+2. 新增: WebChat 支持直接选择提供商和模型
+3. 优化: WebUI 视觉效果、WebChat 视觉效果
+4. 优化: WebUI 测试提供商功能
+5. 优化: 修复潜在的 README XSS 注入问题
+6. 修复: WechatPadPro 授权码提取逻辑以适配上游新版本,并提高安全性
+7. 修复: Gemini 下,多轮工具调用时可能报错的问题
+8. 其他修复与优化
\ No newline at end of file
diff --git a/dashboard/package.json b/dashboard/package.json
index 7a5dd44a5..4f7ca6753 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -26,6 +26,7 @@
"js-md5": "^0.8.3",
"lodash": "4.17.21",
"marked": "^15.0.7",
+ "markdown-it": "^14.1.0",
"pinia": "2.1.6",
"remixicon": "3.5.0",
"vee-validate": "4.11.3",
diff --git a/dashboard/src/components/shared/ExtensionCard.vue b/dashboard/src/components/shared/ExtensionCard.vue
index 35666b740..8a0075173 100644
--- a/dashboard/src/components/shared/ExtensionCard.vue
+++ b/dashboard/src/components/shared/ExtensionCard.vue
@@ -49,6 +49,11 @@ const reloadExtension = () => {
};
const $confirm = inject("$confirm");
+
+const installExtension = async () => {
+ emit('install', props.extension);
+};
+
const uninstallExtension = async () => {
if (typeof $confirm !== "function") {
console.error(tm("card.errors.confirmNotRegistered"));
@@ -117,6 +122,10 @@ const viewReadme = () => {