Compare commits

...

28 Commits

Author SHA1 Message Date
Akarin~
dc6786deab
暗语初步支持 (#27) 2025-06-10 13:30:54 +08:00
6bfa2c39a1
Merge pull request #28 from LiteyukiStudio/snowykami-patch-3
📝 new page
2025-06-10 13:02:32 +08:00
2ce29e45e7
📝 new page 2025-06-10 12:58:52 +08:00
55f9c427b7 🗑️ 标记MarshoTools为弃用 2025-04-05 01:31:35 +08:00
Akarin~
5768b95b09
[WIP] 表情回应支持 (#26)
* 初步支持&utils重构

* 戳一戳支持流式请求

* 移除未使用import

* 解决类型问题
2025-04-04 23:01:01 +08:00
c9d2ef7885 异步化获取夸赞名单与昵称函数 2025-03-29 12:53:20 +08:00
Akarin~
ff6369c1a5
Update README_EN.md 2025-03-25 23:06:52 +08:00
Akarin~
c00cb19e9e
Update README.md 2025-03-25 23:05:58 +08:00
e4490334fa ️ 修改 SSL 问题修复方式 2025-03-23 22:58:10 +08:00
fce3152e17 修改文档url 2025-03-17 23:13:47 +08:00
9878114376 修复夸赞名单报错 2025-03-17 05:25:15 +08:00
21b695f2d4
Merge pull request #22 from LiteyukiStudio/snowykami-patch-2
📝 新增PR预览
2025-03-11 00:05:16 +08:00
02d465112f
📝 新增PR预览 2025-03-11 00:03:54 +08:00
d95928cab7 Merge branch 'main' of https://github.com/LiteyukiStudio/nonebot-plugin-marshoai 2025-03-10 23:57:16 +08:00
41cb287a84 修复流式请求思维链未包含在结构体问题 2025-03-10 23:56:13 +08:00
a0f2b52e59 📝 更新 GitHub Actions 工作流以支持推送和拉取请求 2025-03-10 23:38:42 +08:00
75d173bed7 修改引用链接 2025-03-10 23:24:19 +08:00
f39f5cc1be
Merge pull request #20 from LiteyukiStudio/snowykami-patch-1
📝 更新pages部署地址
2025-03-10 23:13:32 +08:00
70fd176904
📝 更新pages部署地址 2025-03-10 23:08:57 +08:00
57ea4fc10b 📝 引入神秘小js 2025-03-08 23:31:59 +08:00
a1ddf40610 Merge branch 'main' of https://github.com/LiteyukiStudio/nonebot-plugin-marshoai 2025-03-07 21:34:22 +08:00
dc294a257d 📝 禁用干净 URL 设置 2025-03-07 21:34:19 +08:00
Akarin~
6f085b36c6
流式调用[WIP] (#19)
* 流式调用 30%

* 流式调用 90%
2025-03-07 19:04:51 +08:00
8aff490aeb 📝 更新文档页脚信息,修改加速服务链接的显示文本 2025-03-07 17:46:10 +08:00
b713110bcf 📝 更新文档页脚信息,添加网站部署和加速服务的链接 2025-03-07 17:18:56 +08:00
b495aa9490 📝 更新 GitHub Actions 工作流程,将 VitePress 部署更改为使用 Liteyuki PaaS 2025-03-07 17:14:20 +08:00
pre-commit-ci[bot]
a61d13426e
[pre-commit.ci] pre-commit autoupdate (#18)
updates:
- [github.com/PyCQA/isort: 6.0.0 → 6.0.1](https://github.com/PyCQA/isort/compare/6.0.0...6.0.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-04 21:30:01 +08:00
cbafaaf151 📝 使用Liteyukiflare对GitHub Page文档进行亚太地区加速 2025-03-01 02:48:03 +08:00
24 changed files with 384 additions and 197 deletions

View File

@ -1,26 +1,18 @@
# 构建 VitePress 站点并将其部署到 GitHub Pages 的示例工作流程
#
name: Deploy VitePress site to Pages
name: Deploy VitePress site to Liteyuki PaaS
on:
# 在针对 `main` 分支的推送上运行。如果你
# 使用 `master` 分支作为默认分支,请将其更改为 `master`
push:
branches: [main]
on: ["push", "pull_request_target"]
# 允许你从 Actions 选项卡手动运行此工作流程
workflow_dispatch:
# 设置 GITHUB_TOKEN 的权限,以允许部署到 GitHub Pages
permissions:
contents: write
statuses: write
# 只允许同时进行一次部署,跳过正在运行和最新队列之间的运行队列
# 但是,不要取消正在进行的运行,因为我们希望允许这些生产部署完成
concurrency:
group: pages
cancel-in-progress: false
env:
MELI_SITE: f31e3b17-c4ea-4d9d-bdce-9417d67fd30e
jobs:
# 构建工作
build:
@ -30,12 +22,10 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0 # 如果未启用 lastUpdated则不需要
# - uses: pnpm/action-setup@v3 # 如果使用 pnpm请取消注释
# - uses: oven-sh/setup-bun@v1 # 如果使用 Bun请取消注释
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.11'
python-version: "3.11"
- name: Setup API markdown
run: |-
@ -59,9 +49,13 @@ jobs:
run: |-
pnpm run docs:build
- name: 部署文档
uses: JamesIves/github-pages-deploy-action@v4
with:
# 这是文档部署到的分支名称
branch: docs
folder: docs/.vitepress/dist
- name: "发布"
run: |
npx -p "@getmeli/cli" meli upload docs/.vitepress/dist \
--url "https://dash.apage.dev" \
--site "$MELI_SITE" \
--token "$MELI_TOKEN" \
--release "$GITHUB_SHA"
env:
MELI_TOKEN: ${{ secrets.MELI_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -15,7 +15,7 @@ repos:
args: [--config=./pyproject.toml]
- repo: https://github.com/PyCQA/isort
rev: 6.0.0
rev: 6.0.1
hooks:
- id: isort
args: ["--profile", "black"]

2
CNAME
View File

@ -1 +1 @@
marsho.liteyuki.icu
marshoai-docs.pages.liteyuki.icu

View File

@ -1,6 +1,6 @@
<!--suppress LongLine -->
<div align="center">
<a href="https://marsho.liteyuki.icu"><img src="https://marsho.liteyuki.icu/marsho-full.svg" width="800" height="430" alt="MarshoLogo"></a>
<a href="https://marshoai-docs.pages.liteyuki.icu"><img src="https://marshoai-docs.pages.liteyuki.icu/marsho-full.svg" width="800" height="430" alt="MarshoLogo"></a>
<br>
</div>
@ -48,7 +48,7 @@ _谁不喜欢回复消息快又可爱的猫娘呢_
## 😼 使用
请查看[使用文档](https://marsho.liteyuki.icu/start/use)
请查看[使用文档](https://marshoai-docs.pages.liteyuki.icu/start/use.html)
## ❤ 鸣谢&版权说明

View File

@ -1,6 +1,6 @@
<!--suppress LongLine -->
<div align="center">
<a href="https://marsho.liteyuki.icu"><img src="https://marsho.liteyuki.icu/marsho-full.svg" width="800" height="430" alt="MarshoLogo"></a>
<a href="https://marshoai-docs.pages.liteyuki.icu"><img src="https://marshoai-docs.pages.liteyuki.icu/marsho-full.svg" width="800" height="430" alt="MarshoLogo"></a>
<br>
</div>
@ -48,7 +48,7 @@ Plugin internally installed the catgirl character of Marsho, is able to have a c
- 🐾 Play! I like play with friends!
## 😼 Usage
Please read [Documentation](https://marsho.liteyuki.icu/start/install)
Please read [Documentation](https://marshoai-docs.pages.liteyuki.icu/start/use.html)
## ❤ Thanks&Copyright
This project uses the following code from other projects:

View File

@ -1,81 +1,87 @@
import { VitePressSidebarOptions } from "vitepress-sidebar/types"
import { VitePressSidebarOptions } from "vitepress-sidebar/types";
export const gitea = {
svg: '<svg t="1725391346807" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5067" width="256" height="256"><path d="M1004.692673 466.396616l-447.094409-447.073929c-25.743103-25.763582-67.501405-25.763582-93.264987 0l-103.873521 103.873521 78.171378 78.171378c12.533635-6.00058 26.562294-9.359266 41.389666-9.359266 53.02219 0 96.00928 42.98709 96.00928 96.00928 0 14.827372-3.358686 28.856031-9.359266 41.389666l127.97824 127.97824c12.533635-6.00058 26.562294-9.359266 41.389666-9.359266 53.02219 0 96.00928 42.98709 96.00928 96.00928s-42.98709 96.00928-96.00928 96.00928-96.00928-42.98709-96.00928-96.00928c0-14.827372 3.358686-28.856031 9.359266-41.389666l-127.97824-127.97824c-3.051489 1.454065-6.184898 2.744293-9.379746 3.870681l0 266.97461c37.273227 13.188988 63.99936 48.721433 63.99936 90.520695 0 53.02219-42.98709 96.00928-96.00928 96.00928s-96.00928-42.98709-96.00928-96.00928c0-41.799262 26.726133-77.331707 63.99936-90.520695l0-266.97461c-37.273227-13.188988-63.99936-48.721433-63.99936-90.520695 0-14.827372 3.358686-28.856031 9.359266-41.389666l-78.171378-78.171378-295.892081 295.871601c-25.743103 25.784062-25.743103 67.542365 0 93.285467l447.114889 447.073929c25.743103 25.743103 67.480925 25.743103 93.264987 0l445.00547-445.00547c25.763582-25.763582 25.763582-67.542365 0-93.285467z" fill="#a2d8f4" p-id="5068"></path></svg>'
}
svg: '<svg t="1725391346807" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5067" width="256" height="256"><path d="M1004.692673 466.396616l-447.094409-447.073929c-25.743103-25.763582-67.501405-25.763582-93.264987 0l-103.873521 103.873521 78.171378 78.171378c12.533635-6.00058 26.562294-9.359266 41.389666-9.359266 53.02219 0 96.00928 42.98709 96.00928 96.00928 0 14.827372-3.358686 28.856031-9.359266 41.389666l127.97824 127.97824c12.533635-6.00058 26.562294-9.359266 41.389666-9.359266 53.02219 0 96.00928 42.98709 96.00928 96.00928s-42.98709 96.00928-96.00928 96.00928-96.00928-42.98709-96.00928-96.00928c0-14.827372 3.358686-28.856031 9.359266-41.389666l-127.97824-127.97824c-3.051489 1.454065-6.184898 2.744293-9.379746 3.870681l0 266.97461c37.273227 13.188988 63.99936 48.721433 63.99936 90.520695 0 53.02219-42.98709 96.00928-96.00928 96.00928s-96.00928-42.98709-96.00928-96.00928c0-41.799262 26.726133-77.331707 63.99936-90.520695l0-266.97461c-37.273227-13.188988-63.99936-48.721433-63.99936-90.520695 0-14.827372 3.358686-28.856031 9.359266-41.389666l-78.171378-78.171378-295.892081 295.871601c-25.743103 25.784062-25.743103 67.542365 0 93.285467l447.114889 447.073929c25.743103 25.743103 67.480925 25.743103 93.264987 0l445.00547-445.00547c25.763582-25.763582 25.763582-67.542365 0-93.285467z" fill="#a2d8f4" p-id="5068"></path></svg>',
};
export const defaultLang = 'zh'
export const defaultLang = "zh";
const commonSidebarOptions: VitePressSidebarOptions = {
collapsed: true,
convertSameNameSubFileToGroupIndexPage: true,
useTitleFromFrontmatter: true,
useFolderTitleFromIndexFile: false,
useFolderLinkFromIndexFile: true,
useTitleFromFileHeading: true,
rootGroupText: 'MARSHOAI',
includeFolderIndexFile: true,
sortMenusByFrontmatterOrder: true,
}
collapsed: true,
convertSameNameSubFileToGroupIndexPage: true,
useTitleFromFrontmatter: true,
useFolderTitleFromIndexFile: false,
useFolderLinkFromIndexFile: true,
useTitleFromFileHeading: true,
rootGroupText: "MARSHOAI",
includeFolderIndexFile: true,
sortMenusByFrontmatterOrder: true,
};
export function generateSidebarConfig(): VitePressSidebarOptions[] {
let sections = ["dev", "start"]
let languages = ['zh', 'en']
let ret: VitePressSidebarOptions[] = []
for (let language of languages) {
for (let section of sections) {
if (language === defaultLang) {
ret.push({
basePath: `/${section}/`,
scanStartPath: `docs/${language}/${section}`,
resolvePath: `/${section}/`,
...commonSidebarOptions
})
} else {
ret.push({
basePath: `/${language}/${section}/`,
scanStartPath: `docs/${language}/${section}`,
resolvePath: `/${language}/${section}/`,
...commonSidebarOptions
})
}
}
let sections = ["dev", "start"];
let languages = ["zh", "en"];
let ret: VitePressSidebarOptions[] = [];
for (let language of languages) {
for (let section of sections) {
if (language === defaultLang) {
ret.push({
basePath: `/${section}/`,
scanStartPath: `docs/${language}/${section}`,
resolvePath: `/${section}/`,
...commonSidebarOptions,
});
} else {
ret.push({
basePath: `/${language}/${section}/`,
scanStartPath: `docs/${language}/${section}`,
resolvePath: `/${language}/${section}/`,
...commonSidebarOptions,
});
}
}
return ret
}
return ret;
}
export const ThemeConfig = {
getEditLink: (editPageText: string): { pattern: (params: { filePath: string; }) => string; text: string; } => {
return {
pattern: ({filePath}: { filePath: string; }): string => {
if (!filePath) {
throw new Error("filePath is undefined");
}
const regex = /^(dev\/api|[^\/]+\/dev\/api)/;
if (regex.test(filePath)) {
filePath = filePath.replace(regex, '')
.replace('index.md', '__init__.py')
.replace('.md', '.py');
const fileName = filePath.split('/').pop();
const parentFolder = filePath.split('/').slice(-2, -1)[0];
if (fileName && parentFolder && fileName.split('.')[0] === parentFolder) {
filePath = filePath.split('/').slice(0, -1).join('/') + '/__init__.py';
}
return `https://github.com/LiteyukiStudio/nonebot-plugin-marshoai/tree/main/nonebot_plugin_marshoai/${filePath}`;
} else {
return `https://github.com/LiteyukiStudio/nonebot-plugin-marshoai/tree/main/docs/${filePath}`;
}
},
text: editPageText
};
},
getEditLink: (
editPageText: string
): { pattern: (params: { filePath: string }) => string; text: string } => {
return {
pattern: ({ filePath }: { filePath: string }): string => {
if (!filePath) {
throw new Error("filePath is undefined");
}
const regex = /^(dev\/api|[^\/]+\/dev\/api)/;
if (regex.test(filePath)) {
filePath = filePath
.replace(regex, "")
.replace("index.md", "__init__.py")
.replace(".md", ".py");
const fileName = filePath.split("/").pop();
const parentFolder = filePath.split("/").slice(-2, -1)[0];
if (
fileName &&
parentFolder &&
fileName.split(".")[0] === parentFolder
) {
filePath =
filePath.split("/").slice(0, -1).join("/") + "/__init__.py";
}
return `https://github.com/LiteyukiStudio/nonebot-plugin-marshoai/tree/main/nonebot_plugin_marshoai/${filePath}`;
} else {
return `https://github.com/LiteyukiStudio/nonebot-plugin-marshoai/tree/main/docs/${filePath}`;
}
},
text: editPageText,
};
},
getOutLine: (label: string): { label: string; level: [number, number]; } => {
return {
label: label,
level: [2, 6]
};
},
copyright: 'Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved'
}
getOutLine: (label: string): { label: string; level: [number, number] } => {
return {
label: label,
level: [2, 6],
};
},
};

View File

@ -23,7 +23,7 @@ export const en = defineConfig({
lightModeSwitchTitle: 'Light',
darkModeSwitchTitle: 'Dark',
footer: {
message: "The document is being improved. Suggestions are welcome.",
message: "The document is being improved. Suggestions are welcome.<br>Webpage is deployed at <a href='https://meli.liteyuki.icu' target='_blank'>Liteyuki Meli</a> and accelerated by <a href='https://cdn.liteyuki.icu' target='_blank'>Liteyukiflare</a>.",
copyright: '© 2024 <a href="https://liteyuki.icu" target="_blank">Liteyuki Studio</a>',
}
},

View File

@ -8,12 +8,13 @@ import { generateSidebar } from 'vitepress-sidebar'
// https://vitepress.dev/reference/site-config
export default defineConfig({
head: [
["script", { src: "https://cdn.liteyuki.icu/js/liteyuki_footer.js" }],
['link', { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
],
rewrites: {
[`${defaultLang}/:rest*`]: ":rest*",
},
cleanUrls: true,
cleanUrls: false,
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
logo: {

View File

@ -23,7 +23,7 @@ export const ja = defineConfig({
lightModeSwitchTitle: 'ライト',
darkModeSwitchTitle: 'ダーク',
footer: {
message: "ドキュメントは改善中です。ご意見をお待ちしております。",
message: "ドキュメントは改善中です。ご意見をお待ちしております。<br>ウェブサイトは <a href='https://meli.liteyuki.icu' target='_blank'>Liteyuki Meli</a> によってデプロイされ、<a href='https://cdn.liteyuki.icu' target='_blank'>Liteyukiflare</a> によって加速されています。",
copyright: '© 2024 <a href="https://liteyuki.icu" target="_blank">Liteyuki Studio</a>',
}
},

View File

@ -23,7 +23,7 @@ export const zh = defineConfig({
lightModeSwitchTitle: '轻色模式',
darkModeSwitchTitle: '深色模式',
footer: {
message: "文档完善中,欢迎提出建议或帮助我们完善。",
message: "文档完善中,欢迎提出建议或帮助我们完善。<br>网站部署在 <a href='https://meli.liteyuki.icu' target='_blank'>Liteyuki Meli</a> 由 <a href='https://cdn.liteyuki.icu' target='_blank'>Liteyukiflare</a> 提供加速服务。",
copyright: '© 2024 <a href="https://liteyuki.icu" target="_blank">Liteyuki Studio</a>',
}
},

View File

@ -65,7 +65,7 @@ When nonebot linked to OneBot v11 adapter, can recieve double click and response
MarshoTools is a feature added in `v0.5.0`, support loading external function library to provide Function Call for Marsho.
## 🧩 Marsho Plugin
Marsho Plugin is a feature added in `v1.0.0`, replacing the old MarshoTools feature. [Documentation](https://marsho.liteyuki.icu/dev/extension)
Marsho Plugin is a feature added in `v1.0.0`, replacing the old MarshoTools feature. [Documentation](https://marshoai-docs.pages.liteyuki.icu/dev/extension)
## 👍 Praise list
@ -147,4 +147,5 @@ Add options in the `.env` file from the diagram below in nonebot2 project.
| MARSHOAI_ENABLE_RICHTEXT_PARSE | `bool` | `true` | Turn on auto parse rich text feature(including image, LaTeX equation) |
| MARSHOAI_SINGLE_LATEX_PARSE | `bool` | `false`| Render single-line equation or not |
| MARSHOAI_FIX_TOOLCALLS | `bool` | `true` | Fix tool calls or not |
| MARSHOAI_SEND_THINKING | `bool` | `true` | Send thinking chain or not |
| MARSHOAI_SEND_THINKING | `bool` | `true` | Send thinking chain or not |
| MARSHOAI_STREAM | `bool` | `false`| 是否通过流式方式请求 API **开启此项后暂无法使用函数调用,无法在 Bot 用户侧聊天界面呈现出流式效果** |

View File

@ -68,7 +68,7 @@ GitHub Models API 的限制较多,不建议使用,建议通过修改`MARSHOA
## 🧩 小棉插件
小棉插件是`v1.0.0`的新增功能,替代旧的小棉工具功能。[使用文档](https://marsho.liteyuki.icu/dev/extension)
小棉插件是`v1.0.0`的新增功能,替代旧的小棉工具功能。[使用文档](https://marshoai-docs.pages.liteyuki.icu/dev/extension)
## 👍 夸赞名单
@ -149,6 +149,8 @@ GitHub Models API 的限制较多,不建议使用,建议通过修改`MARSHOA
| MARSHOAI_SINGLE_LATEX_PARSE | `bool` | `false` | 单行公式是否渲染(当消息富文本解析启用时可用)(如果单行也渲……只能说不好看) |
| MARSHOAI_FIX_TOOLCALLS | `bool` | `true` | 是否修复工具调用(部分模型须关闭,使用 vLLM 部署的模型时须关闭) |
| MARSHOAI_SEND_THINKING | `bool` | `true` | 是否发送思维链(部分模型不支持) |
| MARSHOAI_STREAM | `bool` | `false`| 是否通过流式方式请求 API **开启此项后暂无法使用函数调用,无法在 Bot 用户侧聊天界面呈现出流式效果** |
#### 开发及调试选项

View File

@ -26,17 +26,19 @@ from nonebot.plugin import require
require("nonebot_plugin_alconna")
require("nonebot_plugin_localstore")
require("nonebot_plugin_argot")
import nonebot_plugin_localstore as store # type: ignore
from nonebot import get_driver, logger # type: ignore
from .config import config
# from .hunyuan import *
from .dev import *
from .marsho import *
from .metadata import metadata
# from .hunyuan import *
__author__ = "Asankilp"
__plugin_meta__ = metadata

View File

@ -32,6 +32,7 @@ class ConfigModel(BaseModel):
marshoai_enable_sysasuser_prompt: bool = False
marshoai_additional_prompt: str = ""
marshoai_poke_suffix: str = "揉了揉你的猫耳"
marshoai_stream: bool = False
marshoai_enable_richtext_parse: bool = True
"""
是否启用自动消息富文本解析 即若包含图片链接则发送图片若包含LaTeX公式则发送公式图

View File

@ -37,7 +37,7 @@ OPENAI_NEW_MODELS: list = [
INTRODUCTION: str = f"""MarshoAI-NoneBot by LiteyukiStudio
你好喵~我是一只可爱的猫娘AI名叫小棉~🐾
我的主页在这里哦~
https://marsho.liteyuki.icu
https://marshoai-docs.pages.liteyuki.icu
使用 {config.marshoai_default_name}.status命令获取状态信息
使用{config.marshoai_default_name}.help命令获取使用说明"""

View File

@ -1,15 +1,16 @@
import os
from pathlib import Path
from nonebot import get_driver, logger, require
from nonebot import get_driver, logger, on_command, require
from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher
from nonebot.typing import T_State
from nonebot_plugin_argot import add_argot, get_message_id
from nonebot_plugin_marshoai.plugin.load import reload_plugin
from .config import config
from .marsho import context
from .instances import context
from .plugin.func_call.models import SessionContext
require("nonebot_plugin_alconna")
@ -48,6 +49,21 @@ function_call = on_alconna(
permission=SUPERUSER,
)
argot_test = on_command("argot", permission=SUPERUSER)
@argot_test.handle()
async def _():
await argot_test.send(
"aa",
argot={
"name": "test",
"command": "test",
"segment": f"{os.getcwd()}",
"expired_at": 1000,
},
)
@function_call.assign("list")
async def list_functions():

View File

@ -1,4 +1,5 @@
import json
from datetime import timedelta
from typing import Optional, Tuple, Union
from azure.ai.inference.models import (
@ -17,9 +18,15 @@ from nonebot.matcher import (
current_event,
current_matcher,
)
from nonebot_plugin_alconna.uniseg import UniMessage, UniMsg
from openai import AsyncOpenAI
from openai.types.chat import ChatCompletion, ChatCompletionMessage
from nonebot_plugin_alconna.uniseg import (
Text,
UniMessage,
UniMsg,
get_target,
)
from nonebot_plugin_argot import Argot # type: ignore
from openai import AsyncOpenAI, AsyncStream
from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessage
from .config import config
from .constants import SUPPORT_IMAGE_MODELS
@ -35,6 +42,7 @@ from .util import (
make_chat_openai,
parse_richtext,
)
from .utils.processor import process_chat_stream, process_completion_to_details
class MarshoHandler:
@ -50,7 +58,7 @@ class MarshoHandler:
# self.state: T_State = current_handler.get().state
self.matcher: Matcher = current_matcher.get()
self.message_id: str = UniMessage.get_message_id(self.event)
self.target = UniMessage.get_target(self.event)
self.target = get_target(self.event)
async def process_user_input(
self, user_input: UniMsg, model_name: str
@ -94,14 +102,15 @@ class MarshoHandler:
self,
user_message: Union[str, list],
model_name: str,
tools_list: list,
tools_list: list | None,
tool_message: Optional[list] = None,
) -> ChatCompletion:
stream: bool = False,
) -> Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
"""
处理单条聊天
"""
context_msg = get_prompt(model_name) + (
context_msg = await get_prompt(model_name) + (
self.context.build(self.target.id, self.target.private)
)
response = await make_chat_openai(
@ -109,20 +118,24 @@ class MarshoHandler:
msg=context_msg + [UserMessage(content=user_message).as_dict()] + (tool_message if tool_message else []), # type: ignore
model_name=model_name,
tools=tools_list if tools_list else None,
stream=stream,
)
return response
async def handle_function_call(
self,
completion: ChatCompletion,
completion: Union[ChatCompletion],
user_message: Union[str, list],
model_name: str,
tools_list: list,
tools_list: list | None = None,
):
# function call
# 需要获取额外信息,调用函数工具
tool_msg = []
choice = completion.choices[0]
if isinstance(completion, ChatCompletion):
choice = completion.choices[0]
else:
raise ValueError("Unexpected completion type")
# await UniMessage(str(response)).send()
tool_calls = choice.message.tool_calls
# try:
@ -182,7 +195,7 @@ class MarshoHandler:
self,
user_message: Union[str, list],
model_name: str,
tools_list: list,
tools_list: list | None = None,
stream: bool = False,
tool_message: Optional[list] = None,
) -> Optional[Tuple[UserMessage, ChatCompletionMessage]]:
@ -191,14 +204,20 @@ class MarshoHandler:
"""
global target_list
if stream:
raise NotImplementedError
response = await self.handle_single_chat(
user_message=user_message,
model_name=model_name,
tools_list=tools_list,
tool_message=tool_message,
)
choice = response.choices[0]
response = await self.handle_stream_request(
user_message=user_message,
model_name=model_name,
tools_list=tools_list,
tools_message=tool_message,
)
else:
response = await self.handle_single_chat( # type: ignore
user_message=user_message,
model_name=model_name,
tools_list=tools_list,
tool_message=tool_message,
)
choice = response.choices[0] # type: ignore
# Sprint(choice)
# 当tool_calls非空时将finish_reason设置为TOOL_CALLS
if choice.message.tool_calls is not None and config.marshoai_fix_toolcalls:
@ -218,12 +237,28 @@ class MarshoHandler:
target_list.append([self.target.id, self.target.private])
# 对话成功发送消息
send_message = UniMessage()
if config.marshoai_enable_richtext_parse:
await (await parse_richtext(str(choice_msg_content))).send(
reply_to=True
)
send_message = await parse_richtext(str(choice_msg_content))
else:
await UniMessage(str(choice_msg_content)).send(reply_to=True)
send_message = UniMessage(str(choice_msg_content))
send_message.append(
Argot(
"detail",
Text(await process_completion_to_details(response)),
command="detail",
expired_at=timedelta(minutes=5),
)
)
# send_message.append(
# Argot(
# "debug",
# Text(str(response)),
# command=f"debug",
# expired_at=timedelta(minutes=5),
# )
# )
await send_message.send(reply_to=True)
return UserMessage(content=user_message), choice_msg_after
elif choice.finish_reason == CompletionsFinishReason.CONTENT_FILTERED:
@ -240,3 +275,26 @@ class MarshoHandler:
else:
await UniMessage(f"意外的完成原因:{choice.finish_reason}").send()
return None
async def handle_stream_request(
self,
user_message: Union[str, list],
model_name: str,
tools_list: list | None = None,
tools_message: Optional[list] = None,
) -> ChatCompletion:
"""
处理流式请求
"""
response = await self.handle_single_chat(
user_message=user_message,
model_name=model_name,
tools_list=None, # TODO:让流式调用支持工具调用
tool_message=tools_message,
stream=True,
)
if isinstance(response, AsyncStream):
return await process_chat_stream(response)
else:
raise TypeError("Unexpected response type for stream request")

View File

@ -15,7 +15,15 @@ from nonebot.params import CommandArg
from nonebot.permission import SUPERUSER
from nonebot.rule import to_me
from nonebot.typing import T_State
from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg, on_alconna
from nonebot_plugin_alconna import (
Emoji,
MsgTarget,
UniMessage,
UniMsg,
message_reaction,
on_alconna,
)
from nonebot_plugin_argot.extension import ArgotExtension # type: ignore
from .config import config
from .constants import INTRODUCTION, SUPPORT_IMAGE_MODELS
@ -25,6 +33,7 @@ from .instances import client, context, model_name, target_list, tools
from .metadata import metadata
from .plugin.func_call.caller import get_function_calls
from .util import *
from .utils.processor import process_chat_stream
async def at_enable():
@ -55,6 +64,7 @@ marsho_cmd = on_alconna(
aliases=tuple(config.marshoai_aliases),
priority=96,
block=True,
extensions=[ArgotExtension()],
)
resetmem_cmd = on_alconna(
Alconna(
@ -226,6 +236,7 @@ async def marsho(
if not text:
# 发送说明
# await UniMessage(metadata.usage + "\n当前使用的模型" + model_name).send()
await message_reaction(Emoji("38"))
await marsho_cmd.finish(INTRODUCTION)
backup_context = await get_backup_context(target.id, target.private)
if backup_context:
@ -256,8 +267,11 @@ async def marsho(
map(lambda v: v.data(), get_function_calls().values())
)
logger.info(f"正在获取回答,模型:{model_name}")
await message_reaction(Emoji("66"))
# logger.info(f"上下文:{context_msg}")
response = await handler.handle_common_chat(usermsg, model_name, tools_lists)
response = await handler.handle_common_chat(
usermsg, model_name, tools_lists, config.marshoai_stream
)
# await UniMessage(str(response)).send()
if response is not None:
context_user, context_assistant = response
@ -280,20 +294,24 @@ with contextlib.suppress(ImportError): # 优化先不做()
async def poke(event: Event):
user_nickname = await get_nickname_by_user_id(event.get_user_id())
usermsg = await get_prompt(model_name) + [
UserMessage(content=f"*{user_nickname}{config.marshoai_poke_suffix}"),
]
try:
if config.marshoai_poke_suffix != "":
logger.info(f"收到戳一戳,用户昵称:{user_nickname}")
response = await make_chat_openai(
pre_response = await make_chat_openai(
client=client,
model_name=model_name,
msg=get_prompt(model_name)
+ [
UserMessage(
content=f"*{user_nickname}{config.marshoai_poke_suffix}"
),
],
msg=usermsg,
stream=config.marshoai_stream,
)
choice = response.choices[0]
if isinstance(pre_response, AsyncStream):
response = await process_chat_stream(pre_response)
else:
response = pre_response
choice = response.choices[0] # type: ignore
if choice.finish_reason == CompletionsFinishReason.STOPPED:
content = extract_content_and_think(choice.message)[0]
await UniMessage(" " + str(content)).send(at_sender=True)

View File

@ -5,7 +5,7 @@ from .constants import USAGE
metadata = PluginMetadata(
name="Marsho AI 插件",
description="接入 Azure API 或其他 API 的 AI 聊天插件,支持图片处理,外部函数调用,兼容包括 DeepSeek-R1 在内的多个模型",
description="接入 Azure API 或其他 API 的 AI 聊天插件,支持图片处理,外部函数调用,兼容包括 DeepSeek-R1 QwQ-32B 在内的多个模型",
usage=USAGE,
type="application",
config=ConfigModel,

View File

@ -7,6 +7,7 @@ import sys
import traceback
from nonebot import logger
from typing_extensions import deprecated
from .config import config
@ -73,6 +74,7 @@ class MarshoContext:
return self._get_target_dict(is_private).setdefault(target_id, [])
@deprecated("小棉工具已弃用,无法正常调用")
class MarshoTools:
"""
Marsho 的工具类

View File

@ -2,8 +2,9 @@ import base64
import json
import mimetypes
import re
import ssl
import uuid
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union
import aiofiles # type: ignore
import httpx
@ -15,9 +16,9 @@ from nonebot.log import logger
from nonebot_plugin_alconna import Image as ImageMsg
from nonebot_plugin_alconna import Text as TextMsg
from nonebot_plugin_alconna import UniMessage
from openai import AsyncOpenAI, NotGiven
from openai.types.chat import ChatCompletion, ChatCompletionMessage
from zhDateTime import DateTime
from openai import AsyncOpenAI, AsyncStream, NotGiven
from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessage
from zhDateTime import DateTime # type: ignore
from ._types import DeveloperMessage
from .cache.decos import *
@ -58,6 +59,8 @@ _praises_init_data = {
"""
初始夸赞名单之数据
"""
_ssl_context = ssl.create_default_context()
_ssl_context.set_ciphers("DEFAULT")
async def get_image_raw_and_type(
@ -74,7 +77,7 @@ async def get_image_raw_and_type(
tuple[bytes, str]: 图片二进制数据, 图片MIME格式
"""
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(verify=_ssl_context) as client:
response = await client.get(url, headers=_browser_headers, timeout=timeout)
if response.status_code == 200:
# 获取图片数据
@ -98,9 +101,7 @@ async def get_image_b64(url: str, timeout: int = 10) -> Optional[str]:
return: 图片base64编码
"""
if data_type := await get_image_raw_and_type(
url.replace("https://", "http://"), timeout
):
if data_type := await get_image_raw_and_type(url, timeout):
# image_format = content_type.split("/")[1] if content_type else "jpeg"
base64_image = base64.b64encode(data_type[0]).decode("utf-8")
data_url = "data:{};base64,{}".format(data_type[1], base64_image)
@ -109,35 +110,13 @@ async def get_image_b64(url: str, timeout: int = 10) -> Optional[str]:
return None
async def make_chat(
client: ChatCompletionsClient,
msg: list,
model_name: str,
tools: Optional[list] = None,
):
"""
调用ai获取回复
参数:
client: 用于与AI模型进行通信
msg: 消息内容
model_name: 指定AI模型名
tools: 工具列表
"""
return await client.complete(
messages=msg,
model=model_name,
tools=tools,
**config.marshoai_model_args,
)
async def make_chat_openai(
client: AsyncOpenAI,
msg: list,
model_name: str,
tools: Optional[list] = None,
) -> ChatCompletion:
stream: bool = False,
) -> Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
"""
使用 Openai SDK 调用ai获取回复
@ -152,20 +131,21 @@ async def make_chat_openai(
model=model_name,
tools=tools or NOT_GIVEN,
timeout=config.marshoai_timeout,
stream=stream,
**config.marshoai_model_args,
)
@from_cache("praises")
def get_praises():
async def get_praises():
praises_file = store.get_plugin_data_file(
"praises.json"
) # 夸赞名单文件使用localstore存储
if not praises_file.exists():
with open(praises_file, "w", encoding="utf-8") as f:
json.dump(_praises_init_data, f, ensure_ascii=False, indent=4)
with open(praises_file, "r", encoding="utf-8") as f:
data = json.load(f)
async with aiofiles.open(praises_file, "w", encoding="utf-8") as f:
await f.write(json.dumps(_praises_init_data, ensure_ascii=False, indent=4))
async with aiofiles.open(praises_file, "r", encoding="utf-8") as f:
data = json.loads(await f.read())
praises_json = data
return praises_json
@ -181,8 +161,8 @@ async def refresh_praises_json():
return data
def build_praises() -> str:
praises = get_praises()
async def build_praises() -> str:
praises = await get_praises()
result = ["你喜欢以下几个人物,他们有各自的优点:"]
for item in praises["like"]:
result.append(f"名字:{item['name']},优点:{item['advantages']}")
@ -234,8 +214,8 @@ async def set_nickname(user_id: str, name: str):
data[user_id] = name
if name == "" and user_id in data:
del data[user_id]
with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
async with aiofiles.open(filename, "w", encoding="utf-8") as f:
await f.write(json.dumps(data, ensure_ascii=False, indent=4))
return data
@ -258,11 +238,11 @@ async def refresh_nickname_json():
logger.error("刷新 nickname_json 表错误:无法载入 nickname.json 文件")
def get_prompt(model: str) -> List[Dict[str, Any]]:
async def get_prompt(model: str) -> List[Dict[str, Any]]:
"""获取系统提示词"""
prompts = config.marshoai_additional_prompt
if config.marshoai_enable_praises:
praises_prompt = build_praises()
praises_prompt = await build_praises()
prompts += praises_prompt
if config.marshoai_enable_time_prompt:

View File

@ -0,0 +1,87 @@
from nonebot.log import logger
from openai import AsyncStream
from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessage
from openai.types.chat.chat_completion import Choice
async def process_chat_stream(
stream: AsyncStream[ChatCompletionChunk],
) -> ChatCompletion:
reasoning_contents = ""
answer_contents = ""
last_chunk = None
is_first_token_appeared = False
is_answering = False
async for chunk in stream:
last_chunk = chunk
# print(chunk)
if not is_first_token_appeared:
logger.info(f"{chunk.id}: 第一个 token 已出现")
is_first_token_appeared = True
if not chunk.choices:
logger.info("Usage:", chunk.usage)
else:
delta = chunk.choices[0].delta
if (
hasattr(delta, "reasoning_content")
and delta.reasoning_content is not None
):
reasoning_contents += delta.reasoning_content
else:
if not is_answering:
logger.info(
f"{chunk.id}: 思维链已输出完毕或无 reasoning_content 字段输出"
)
is_answering = True
if delta.content is not None:
answer_contents += delta.content
# print(last_chunk)
# 创建新的 ChatCompletion 对象
if last_chunk and last_chunk.choices:
message = ChatCompletionMessage(
content=answer_contents,
role="assistant",
tool_calls=last_chunk.choices[0].delta.tool_calls, # type: ignore
)
if reasoning_contents != "":
setattr(message, "reasoning_content", reasoning_contents)
choice = Choice(
finish_reason=last_chunk.choices[0].finish_reason, # type: ignore
index=last_chunk.choices[0].index,
message=message,
)
return ChatCompletion(
id=last_chunk.id,
choices=[choice],
created=last_chunk.created,
model=last_chunk.model,
system_fingerprint=last_chunk.system_fingerprint,
object="chat.completion",
usage=last_chunk.usage,
)
else:
return ChatCompletion(
id="",
choices=[],
created=0,
model="",
system_fingerprint="",
object="chat.completion",
usage=None,
)
async def process_completion_to_details(completion: ChatCompletion) -> str:
usage_text = ""
usage = completion.usage
if usage is None:
usage_text = ""
else:
usage_text = str(usage)
details_text = f"""=========消息详情=========
模型: {completion.model}
消息 ID: {completion.id}
用量信息: {usage_text}"""
# print(details_text)
return details_text

32
pdm.lock generated
View File

@ -5,7 +5,7 @@
groups = ["default", "dev", "test"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:d7ab3d9ca825de512d4f87ec846f7fddcf3d5796a7c9562e60c8c7d39c058817"
content_hash = "sha256:6aa043fb1d2d4d384e0d0c698c02a27f22e099828d2973a4baef05c5316f4ee0"
[[metadata.targets]]
requires_python = "~=3.10"
@ -1485,7 +1485,7 @@ files = [
[[package]]
name = "nonebot-plugin-alconna"
version = "0.54.1"
version = "0.57.0"
requires_python = ">=3.9"
summary = "Alconna Adapter for Nonebot"
groups = ["default"]
@ -1499,8 +1499,8 @@ dependencies = [
"tarina<0.7,>=0.6.8",
]
files = [
{file = "nonebot_plugin_alconna-0.54.1-py3-none-any.whl", hash = "sha256:4edb4b081cd64ce37717c7a92d31aadd2cf287a5a0adc2ac86ed82d9bcad5048"},
{file = "nonebot_plugin_alconna-0.54.1.tar.gz", hash = "sha256:66fae03120b8eff25bb0027d65f149e399aa6f73c7585ebdd388d1904cecdeee"},
{file = "nonebot_plugin_alconna-0.57.0-py3-none-any.whl", hash = "sha256:6c4bcce1a9aa176244b4c011b19b1cea00269c4c6794cd4e90d8dd7990ec3ec9"},
{file = "nonebot_plugin_alconna-0.57.0.tar.gz", hash = "sha256:7a9a4bf373f3f6836611dbde1a0917b84441a534dd6f2b20dae3ba6fff142858"},
]
[[package]]
@ -1519,9 +1519,27 @@ files = [
{file = "nonebot_plugin_apscheduler-0.5.0.tar.gz", hash = "sha256:6c0230e99765f275dc83d6639ff33bd6f71203fa10cd1b8a204b0f95530cda86"},
]
[[package]]
name = "nonebot-plugin-argot"
version = "0.1.7"
requires_python = ">=3.10"
summary = "NoneBot 暗语"
groups = ["default"]
dependencies = [
"aiofiles>=24.1.0",
"nonebot-plugin-alconna>=0.51.1",
"nonebot-plugin-apscheduler>=0.5.0",
"nonebot-plugin-localstore>=0.7.4",
"nonebot2>=2.3.2",
]
files = [
{file = "nonebot_plugin_argot-0.1.7-py3-none-any.whl", hash = "sha256:1af939a60967e27aff6f7ce97150d26cba8f1ef0cf216b44372cc0d8e5937204"},
{file = "nonebot_plugin_argot-0.1.7.tar.gz", hash = "sha256:f76c2139c9af1e2de6efdc487b728fbad84737d272bf1f600d085bbe6ed79094"},
]
[[package]]
name = "nonebot-plugin-localstore"
version = "0.7.3"
version = "0.7.4"
requires_python = "<4.0,>=3.9"
summary = "Local Storage Support for NoneBot2"
groups = ["default"]
@ -1532,8 +1550,8 @@ dependencies = [
"typing-extensions<5.0.0,>=4.0.0",
]
files = [
{file = "nonebot_plugin_localstore-0.7.3-py3-none-any.whl", hash = "sha256:1bc239b4b5320df0dc08eada7c4f8ba4cb92d4dc3134bf4646ab5e297bd7e575"},
{file = "nonebot_plugin_localstore-0.7.3.tar.gz", hash = "sha256:1aff10e2dacfc5bc9ce239fd34849f8d7172a118135dbc5aeba1c97605d9959d"},
{file = "nonebot_plugin_localstore-0.7.4-py3-none-any.whl", hash = "sha256:3b08030878eadcdd8b9ce3d079da0dc2d0e41dc91f0b2d8cf7fa862a27de9090"},
{file = "nonebot_plugin_localstore-0.7.4.tar.gz", hash = "sha256:85ddc13814bfcd484ab311306823651390020bf44f4fb4733b343a58e72723ce"},
]
[[package]]

View File

@ -10,7 +10,7 @@ authors = [
]
dependencies = [
"nonebot2>=2.4.0",
"nonebot-plugin-alconna>=0.48.0",
"nonebot-plugin-alconna>=0.57.1",
"nonebot-plugin-localstore>=0.7.1",
"zhDatetime>=2.0.0",
"aiohttp>=3.9",
@ -28,13 +28,14 @@ dependencies = [
"azure-ai-inference>=1.0.0b6",
"watchdog>=6.0.0",
"nonebot-plugin-apscheduler>=0.5.0",
"openai>=1.58.1"
"openai>=1.58.1",
"nonebot-plugin-argot>=0.1.7"
]
license = { text = "MIT, Mulan PSL v2" }
[project.urls]
Homepage = "https://marsho.liteyuki.icu/"
Homepage = "https://marshoai-docs.pages.liteyuki.icu/"
[tool.nonebot]