Compare commits

...

20 Commits

Author SHA1 Message Date
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
00605ad401 修改优先级 2025-02-27 23:24:25 +08:00
Akarin~
1cd60252b5 修复依赖注入问题? (#17)
* 实现缓存装饰器,优化数据获取和存储逻辑

* 重构代码,准备将聊天请求逻辑移入MarshoHandler

* 记录点(

* unfinished

* 🎨 重写基本完毕

* 移除未使用import,添加漏掉的换行

* 修复依赖注入问题?
2025-02-26 00:47:57 +08:00
17 changed files with 249 additions and 175 deletions

View File

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

View File

@@ -1,81 +1,87 @@
import { VitePressSidebarOptions } from "vitepress-sidebar/types" import { VitePressSidebarOptions } from "vitepress-sidebar/types";
export const gitea = { 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 = { const commonSidebarOptions: VitePressSidebarOptions = {
collapsed: true, collapsed: true,
convertSameNameSubFileToGroupIndexPage: true, convertSameNameSubFileToGroupIndexPage: true,
useTitleFromFrontmatter: true, useTitleFromFrontmatter: true,
useFolderTitleFromIndexFile: false, useFolderTitleFromIndexFile: false,
useFolderLinkFromIndexFile: true, useFolderLinkFromIndexFile: true,
useTitleFromFileHeading: true, useTitleFromFileHeading: true,
rootGroupText: 'MARSHOAI', rootGroupText: "MARSHOAI",
includeFolderIndexFile: true, includeFolderIndexFile: true,
sortMenusByFrontmatterOrder: true, sortMenusByFrontmatterOrder: true,
} };
export function generateSidebarConfig(): VitePressSidebarOptions[] { export function generateSidebarConfig(): VitePressSidebarOptions[] {
let sections = ["dev", "start"] let sections = ["dev", "start"];
let languages = ['zh', 'en'] let languages = ["zh", "en"];
let ret: VitePressSidebarOptions[] = [] let ret: VitePressSidebarOptions[] = [];
for (let language of languages) { for (let language of languages) {
for (let section of sections) { for (let section of sections) {
if (language === defaultLang) { if (language === defaultLang) {
ret.push({ ret.push({
basePath: `/${section}/`, basePath: `/${section}/`,
scanStartPath: `docs/${language}/${section}`, scanStartPath: `docs/${language}/${section}`,
resolvePath: `/${section}/`, resolvePath: `/${section}/`,
...commonSidebarOptions ...commonSidebarOptions,
}) });
} else { } else {
ret.push({ ret.push({
basePath: `/${language}/${section}/`, basePath: `/${language}/${section}/`,
scanStartPath: `docs/${language}/${section}`, scanStartPath: `docs/${language}/${section}`,
resolvePath: `/${language}/${section}/`, resolvePath: `/${language}/${section}/`,
...commonSidebarOptions ...commonSidebarOptions,
}) });
} }
}
} }
return ret }
return ret;
} }
export const ThemeConfig = { export const ThemeConfig = {
getEditLink: (editPageText: string): { pattern: (params: { filePath: string; }) => string; text: string; } => { getEditLink: (
return { editPageText: string
pattern: ({filePath}: { filePath: string; }): string => { ): { pattern: (params: { filePath: string }) => string; text: string } => {
if (!filePath) { return {
throw new Error("filePath is undefined"); pattern: ({ filePath }: { filePath: string }): string => {
} if (!filePath) {
const regex = /^(dev\/api|[^\/]+\/dev\/api)/; throw new Error("filePath is undefined");
if (regex.test(filePath)) { }
filePath = filePath.replace(regex, '') const regex = /^(dev\/api|[^\/]+\/dev\/api)/;
.replace('index.md', '__init__.py') if (regex.test(filePath)) {
.replace('.md', '.py'); filePath = filePath
const fileName = filePath.split('/').pop(); .replace(regex, "")
const parentFolder = filePath.split('/').slice(-2, -1)[0]; .replace("index.md", "__init__.py")
if (fileName && parentFolder && fileName.split('.')[0] === parentFolder) { .replace(".md", ".py");
filePath = filePath.split('/').slice(0, -1).join('/') + '/__init__.py'; const fileName = filePath.split("/").pop();
} const parentFolder = filePath.split("/").slice(-2, -1)[0];
return `https://github.com/LiteyukiStudio/nonebot-plugin-marshoai/tree/main/nonebot_plugin_marshoai/${filePath}`; if (
} else { fileName &&
return `https://github.com/LiteyukiStudio/nonebot-plugin-marshoai/tree/main/docs/${filePath}`; parentFolder &&
} fileName.split(".")[0] === parentFolder
}, ) {
text: editPageText 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]; } => { getOutLine: (label: string): { label: string; level: [number, number] } => {
return { return {
label: label, label: label,
level: [2, 6] level: [2, 6],
}; };
}, },
};
copyright: 'Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved'
}

View File

@@ -23,7 +23,7 @@ export const en = defineConfig({
lightModeSwitchTitle: 'Light', lightModeSwitchTitle: 'Light',
darkModeSwitchTitle: 'Dark', darkModeSwitchTitle: 'Dark',
footer: { 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>', 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 // https://vitepress.dev/reference/site-config
export default defineConfig({ export default defineConfig({
head: [ head: [
["script", { src: "https://cdn.liteyuki.icu/js/liteyuki_footer.js" }],
['link', { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], ['link', { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
], ],
rewrites: { rewrites: {
[`${defaultLang}/:rest*`]: ":rest*", [`${defaultLang}/:rest*`]: ":rest*",
}, },
cleanUrls: true, cleanUrls: false,
themeConfig: { themeConfig: {
// https://vitepress.dev/reference/default-theme-config // https://vitepress.dev/reference/default-theme-config
logo: { logo: {

View File

@@ -23,7 +23,7 @@ export const ja = defineConfig({
lightModeSwitchTitle: 'ライト', lightModeSwitchTitle: 'ライト',
darkModeSwitchTitle: 'ダーク', darkModeSwitchTitle: 'ダーク',
footer: { 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>', copyright: '© 2024 <a href="https://liteyuki.icu" target="_blank">Liteyuki Studio</a>',
} }
}, },

View File

@@ -23,7 +23,7 @@ export const zh = defineConfig({
lightModeSwitchTitle: '轻色模式', lightModeSwitchTitle: '轻色模式',
darkModeSwitchTitle: '深色模式', darkModeSwitchTitle: '深色模式',
footer: { 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>', copyright: '© 2024 <a href="https://liteyuki.icu" target="_blank">Liteyuki Studio</a>',
} }
}, },

View File

@@ -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_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_SINGLE_LATEX_PARSE | `bool` | `false`| Render single-line equation or not |
| MARSHOAI_FIX_TOOLCALLS | `bool` | `true` | Fix tool calls 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

@@ -149,6 +149,8 @@ GitHub Models API 的限制较多,不建议使用,建议通过修改`MARSHOA
| MARSHOAI_SINGLE_LATEX_PARSE | `bool` | `false` | 单行公式是否渲染(当消息富文本解析启用时可用)(如果单行也渲……只能说不好看) | | MARSHOAI_SINGLE_LATEX_PARSE | `bool` | `false` | 单行公式是否渲染(当消息富文本解析启用时可用)(如果单行也渲……只能说不好看) |
| MARSHOAI_FIX_TOOLCALLS | `bool` | `true` | 是否修复工具调用(部分模型须关闭,使用 vLLM 部署的模型时须关闭) | | MARSHOAI_FIX_TOOLCALLS | `bool` | `true` | 是否修复工具调用(部分模型须关闭,使用 vLLM 部署的模型时须关闭) |
| MARSHOAI_SEND_THINKING | `bool` | `true` | 是否发送思维链(部分模型不支持) | | MARSHOAI_SEND_THINKING | `bool` | `true` | 是否发送思维链(部分模型不支持) |
| MARSHOAI_STREAM | `bool` | `false`| 是否通过流式方式请求 API **开启此项后暂无法使用函数调用,无法在 Bot 用户侧聊天界面呈现出流式效果** |
#### 开发及调试选项 #### 开发及调试选项

View File

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

View File

@@ -18,8 +18,9 @@ from nonebot.matcher import (
current_matcher, current_matcher,
) )
from nonebot_plugin_alconna.uniseg import UniMessage, UniMsg from nonebot_plugin_alconna.uniseg import UniMessage, UniMsg
from openai import AsyncOpenAI from openai import AsyncOpenAI, AsyncStream
from openai.types.chat import ChatCompletion, ChatCompletionMessage from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessage
from openai.types.chat.chat_completion import Choice
from .config import config from .config import config
from .constants import SUPPORT_IMAGE_MODELS from .constants import SUPPORT_IMAGE_MODELS
@@ -94,14 +95,15 @@ class MarshoHandler:
self, self,
user_message: Union[str, list], user_message: Union[str, list],
model_name: str, model_name: str,
tools_list: list, tools_list: list | None,
tool_message: Optional[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) self.context.build(self.target.id, self.target.private)
) )
response = await make_chat_openai( response = await make_chat_openai(
@@ -109,12 +111,13 @@ class MarshoHandler:
msg=context_msg + [UserMessage(content=user_message).as_dict()] + (tool_message if tool_message else []), # type: ignore msg=context_msg + [UserMessage(content=user_message).as_dict()] + (tool_message if tool_message else []), # type: ignore
model_name=model_name, model_name=model_name,
tools=tools_list if tools_list else None, tools=tools_list if tools_list else None,
stream=stream,
) )
return response return response
async def handle_function_call( async def handle_function_call(
self, self,
completion: ChatCompletion, completion: Union[ChatCompletion, AsyncStream[ChatCompletionChunk]],
user_message: Union[str, list], user_message: Union[str, list],
model_name: str, model_name: str,
tools_list: list, tools_list: list,
@@ -122,7 +125,10 @@ class MarshoHandler:
# function call # function call
# 需要获取额外信息,调用函数工具 # 需要获取额外信息,调用函数工具
tool_msg = [] 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() # await UniMessage(str(response)).send()
tool_calls = choice.message.tool_calls tool_calls = choice.message.tool_calls
# try: # try:
@@ -133,7 +139,7 @@ class MarshoHandler:
# except: # except:
# pass # pass
tool_msg.append(choice.message) tool_msg.append(choice.message)
for tool_call in tool_calls: for tool_call in tool_calls: # type: ignore
try: try:
function_args = json.loads(tool_call.function.arguments) function_args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError: except json.JSONDecodeError:
@@ -160,6 +166,7 @@ class MarshoHandler:
bot=self.bot, bot=self.bot,
event=self.event, event=self.event,
matcher=self.matcher, matcher=self.matcher,
state=None,
) )
).call(**function_args) ).call(**function_args)
else: else:
@@ -184,20 +191,26 @@ class MarshoHandler:
tools_list: list, tools_list: list,
stream: bool = False, stream: bool = False,
tool_message: Optional[list] = None, tool_message: Optional[list] = None,
) -> Union[Tuple[UserMessage, ChatCompletionMessage], None]: ) -> Optional[Tuple[UserMessage, ChatCompletionMessage]]:
""" """
处理一般聊天 处理一般聊天
""" """
global target_list global target_list
if stream: if stream:
raise NotImplementedError response = await self.handle_stream_request(
response = await self.handle_single_chat( user_message=user_message,
user_message=user_message, model_name=model_name,
model_name=model_name, tools_list=tools_list,
tools_list=tools_list, tools_message=tool_message,
tool_message=tool_message, )
) else:
choice = response.choices[0] 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) # Sprint(choice)
# 当tool_calls非空时将finish_reason设置为TOOL_CALLS # 当tool_calls非空时将finish_reason设置为TOOL_CALLS
if choice.message.tool_calls is not None and config.marshoai_fix_toolcalls: if choice.message.tool_calls is not None and config.marshoai_fix_toolcalls:
@@ -239,3 +252,76 @@ class MarshoHandler:
else: else:
await UniMessage(f"意外的完成原因:{choice.finish_reason}").send() await UniMessage(f"意外的完成原因:{choice.finish_reason}").send()
return None return None
async def handle_stream_request(
self,
user_message: Union[str, list],
model_name: str,
tools_list: list,
tools_message: Optional[list] = None,
) -> Union[ChatCompletion, None]:
"""
处理流式请求
"""
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):
reasoning_contents = ""
answer_contents = ""
last_chunk = None
is_first_token_appeared = False
is_answering = False
async for chunk in response:
last_chunk = chunk
# print(chunk)
if not is_first_token_appeared:
logger.debug(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.debug(
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,
)
return None

View File

@@ -32,20 +32,20 @@ async def at_enable():
changemodel_cmd = on_command( changemodel_cmd = on_command(
"changemodel", permission=SUPERUSER, priority=10, block=True "changemodel", permission=SUPERUSER, priority=96, block=True
) )
# setprompt_cmd = on_command("prompt",permission=SUPERUSER) # setprompt_cmd = on_command("prompt",permission=SUPERUSER)
praises_cmd = on_command("praises", permission=SUPERUSER, priority=10, block=True) praises_cmd = on_command("praises", permission=SUPERUSER, priority=96, block=True)
add_usermsg_cmd = on_command("usermsg", permission=SUPERUSER, priority=10, block=True) add_usermsg_cmd = on_command("usermsg", permission=SUPERUSER, priority=96, block=True)
add_assistantmsg_cmd = on_command( add_assistantmsg_cmd = on_command(
"assistantmsg", permission=SUPERUSER, priority=10, block=True "assistantmsg", permission=SUPERUSER, priority=96, block=True
) )
contexts_cmd = on_command("contexts", permission=SUPERUSER, priority=10, block=True) contexts_cmd = on_command("contexts", permission=SUPERUSER, priority=96, block=True)
save_context_cmd = on_command( save_context_cmd = on_command(
"savecontext", permission=SUPERUSER, priority=10, block=True "savecontext", permission=SUPERUSER, priority=96, block=True
) )
load_context_cmd = on_command( load_context_cmd = on_command(
"loadcontext", permission=SUPERUSER, priority=10, block=True "loadcontext", permission=SUPERUSER, priority=96, block=True
) )
marsho_cmd = on_alconna( marsho_cmd = on_alconna(
Alconna( Alconna(
@@ -53,42 +53,42 @@ marsho_cmd = on_alconna(
Args["text?", AllParam], Args["text?", AllParam],
), ),
aliases=tuple(config.marshoai_aliases), aliases=tuple(config.marshoai_aliases),
priority=10, priority=96,
block=True, block=True,
) )
resetmem_cmd = on_alconna( resetmem_cmd = on_alconna(
Alconna( Alconna(
config.marshoai_default_name + ".reset", config.marshoai_default_name + ".reset",
), ),
priority=10, priority=96,
block=True, block=True,
) )
marsho_help_cmd = on_alconna( marsho_help_cmd = on_alconna(
Alconna( Alconna(
config.marshoai_default_name + ".help", config.marshoai_default_name + ".help",
), ),
priority=10, priority=96,
block=True, block=True,
) )
marsho_status_cmd = on_alconna( marsho_status_cmd = on_alconna(
Alconna( Alconna(
config.marshoai_default_name + ".status", config.marshoai_default_name + ".status",
), ),
priority=10, priority=96,
block=True, block=True,
) )
marsho_at = on_message(rule=to_me() & at_enable, priority=11) marsho_at = on_message(rule=to_me() & at_enable, priority=97)
nickname_cmd = on_alconna( nickname_cmd = on_alconna(
Alconna( Alconna(
"nickname", "nickname",
Args["name?", str], Args["name?", str],
), ),
priority=10, priority=96,
block=True, block=True,
) )
refresh_data_cmd = on_command( refresh_data_cmd = on_command(
"refresh_data", permission=SUPERUSER, priority=10, block=True "refresh_data", permission=SUPERUSER, priority=96, block=True
) )
@@ -257,14 +257,16 @@ async def marsho(
) )
logger.info(f"正在获取回答,模型:{model_name}") logger.info(f"正在获取回答,模型:{model_name}")
# logger.info(f"上下文:{context_msg}") # 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() # await UniMessage(str(response)).send()
if response is not None: if response is not None:
context_user, context_assistant = response context_user, context_assistant = response
context.append(context_user.as_dict(), target.id, target.private) context.append(context_user.as_dict(), target.id, target.private)
context.append(context_assistant.to_dict(), target.id, target.private) context.append(context_assistant.to_dict(), target.id, target.private)
else: else:
await UniMessage("没有回答").send() return
except Exception as e: except Exception as e:
await UniMessage(str(e) + suggest_solution(str(e))).send() await UniMessage(str(e) + suggest_solution(str(e))).send()
traceback.print_exc() traceback.print_exc()
@@ -286,14 +288,14 @@ with contextlib.suppress(ImportError): # 优化先不做()
response = await make_chat_openai( response = await make_chat_openai(
client=client, client=client,
model_name=model_name, model_name=model_name,
msg=get_prompt(model_name) msg=await get_prompt(model_name)
+ [ + [
UserMessage( UserMessage(
content=f"*{user_nickname}{config.marshoai_poke_suffix}" content=f"*{user_nickname}{config.marshoai_poke_suffix}"
), ),
], ],
) )
choice = response.choices[0] choice = response.choices[0] # type: ignore
if choice.finish_reason == CompletionsFinishReason.STOPPED: if choice.finish_reason == CompletionsFinishReason.STOPPED:
content = extract_content_and_think(choice.message)[0] content = extract_content_and_think(choice.message)[0]
await UniMessage(" " + str(content)).send(at_sender=True) await UniMessage(" " + str(content)).send(at_sender=True)

View File

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

View File

@@ -72,7 +72,9 @@ class Caller:
# if self.ctx.state is None: # if self.ctx.state is None:
# return False, "State is None" # return False, "State is None"
if self._rule and not await self._rule(self.ctx.bot, self.ctx.event): if self._rule and not await self._rule(
self.ctx.bot, self.ctx.event, self.ctx.state
):
return False, "告诉用户 Rule Denied 规则不匹配" return False, "告诉用户 Rule Denied 规则不匹配"
return True, "" return True, ""
@@ -113,9 +115,9 @@ class Caller:
# 检查函数签名,确定依赖注入参数 # 检查函数签名,确定依赖注入参数
sig = inspect.signature(func) sig = inspect.signature(func)
for name, param in sig.parameters.items(): for name, param in sig.parameters.items():
if param.annotation == T_State: # if param.annotation == T_State:
self.di.state = name # self.di.state = name
continue # 防止后续判断T_State子类时报错 # continue # 防止后续判断T_State子类时报错
if issubclass(param.annotation, Event) or isinstance( if issubclass(param.annotation, Event) or isinstance(
param.annotation, Event param.annotation, Event

View File

@@ -19,7 +19,7 @@ class SessionContext(BaseModel):
bot: Bot bot: Bot
event: Event event: Event
matcher: Matcher matcher: Matcher
# state: T_State state: T_State | None
caller: Any = None caller: Any = None
class Config: class Config:

View File

@@ -16,7 +16,7 @@ marsho_memory_cmd = on_alconna(
Subcommand("view", alias={"v"}), Subcommand("view", alias={"v"}),
Subcommand("reset", alias={"r"}), Subcommand("reset", alias={"r"}),
), ),
priority=10, priority=96,
block=True, block=True,
) )

View File

@@ -3,7 +3,7 @@ import json
import mimetypes import mimetypes
import re import re
import uuid import uuid
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Union
import aiofiles # type: ignore import aiofiles # type: ignore
import httpx import httpx
@@ -15,8 +15,8 @@ from nonebot.log import logger
from nonebot_plugin_alconna import Image as ImageMsg from nonebot_plugin_alconna import Image as ImageMsg
from nonebot_plugin_alconna import Text as TextMsg from nonebot_plugin_alconna import Text as TextMsg
from nonebot_plugin_alconna import UniMessage from nonebot_plugin_alconna import UniMessage
from openai import AsyncOpenAI, NotGiven from openai import AsyncOpenAI, AsyncStream, NotGiven
from openai.types.chat import ChatCompletion, ChatCompletionMessage from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessage
from zhDateTime import DateTime from zhDateTime import DateTime
from ._types import DeveloperMessage from ._types import DeveloperMessage
@@ -109,35 +109,13 @@ async def get_image_b64(url: str, timeout: int = 10) -> Optional[str]:
return None 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( async def make_chat_openai(
client: AsyncOpenAI, client: AsyncOpenAI,
msg: list, msg: list,
model_name: str, model_name: str,
tools: Optional[list] = None, tools: Optional[list] = None,
) -> ChatCompletion: stream: bool = False,
) -> Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
""" """
使用 Openai SDK 调用ai获取回复 使用 Openai SDK 调用ai获取回复
@@ -152,20 +130,21 @@ async def make_chat_openai(
model=model_name, model=model_name,
tools=tools or NOT_GIVEN, tools=tools or NOT_GIVEN,
timeout=config.marshoai_timeout, timeout=config.marshoai_timeout,
stream=stream,
**config.marshoai_model_args, **config.marshoai_model_args,
) )
@from_cache("praises") @from_cache("praises")
def get_praises(): async def get_praises():
praises_file = store.get_plugin_data_file( praises_file = store.get_plugin_data_file(
"praises.json" "praises.json"
) # 夸赞名单文件使用localstore存储 ) # 夸赞名单文件使用localstore存储
if not praises_file.exists(): if not praises_file.exists():
with open(praises_file, "w", encoding="utf-8") as f: async with aiofiles.open(praises_file, "w", encoding="utf-8") as f:
json.dump(_praises_init_data, f, ensure_ascii=False, indent=4) json.dump(_praises_init_data, f, ensure_ascii=False, indent=4)
with open(praises_file, "r", encoding="utf-8") as f: async with aiofiles.open(praises_file, "r", encoding="utf-8") as f:
data = json.load(f) data = json.loads(await f.read())
praises_json = data praises_json = data
return praises_json return praises_json
@@ -181,8 +160,8 @@ async def refresh_praises_json():
return data return data
def build_praises() -> str: async def build_praises() -> str:
praises = get_praises() praises = await get_praises()
result = ["你喜欢以下几个人物,他们有各自的优点:"] result = ["你喜欢以下几个人物,他们有各自的优点:"]
for item in praises["like"]: for item in praises["like"]:
result.append(f"名字:{item['name']},优点:{item['advantages']}") result.append(f"名字:{item['name']},优点:{item['advantages']}")
@@ -258,11 +237,11 @@ async def refresh_nickname_json():
logger.error("刷新 nickname_json 表错误:无法载入 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 prompts = config.marshoai_additional_prompt
if config.marshoai_enable_praises: if config.marshoai_enable_praises:
praises_prompt = build_praises() praises_prompt = await build_praises()
prompts += praises_prompt prompts += praises_prompt
if config.marshoai_enable_time_prompt: if config.marshoai_enable_time_prompt: