entari-plugin-hyw 3.2.113__tar.gz → 3.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of entari-plugin-hyw might be problematic. Click here for more details.
- {entari_plugin_hyw-3.2.113/src/entari_plugin_hyw.egg-info → entari_plugin_hyw-3.3.1}/PKG-INFO +25 -17
- {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1}/README.md +24 -11
- {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1}/pyproject.toml +5 -11
- entari_plugin_hyw-3.3.1/src/entari_plugin_hyw/__init__.py +364 -0
- entari_plugin_hyw-3.3.1/src/entari_plugin_hyw/hyw_core.py +700 -0
- {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1/src/entari_plugin_hyw.egg-info}/PKG-INFO +25 -17
- entari_plugin_hyw-3.3.1/src/entari_plugin_hyw.egg-info/SOURCES.txt +9 -0
- {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1}/src/entari_plugin_hyw.egg-info/requires.txt +0 -5
- entari_plugin_hyw-3.2.113/MANIFEST.in +0 -3
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/__init__.py +0 -813
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/anthropic.svg +0 -1
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/deepseek.png +0 -0
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/gemini.svg +0 -1
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/google.svg +0 -1
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/grok.png +0 -0
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/microsoft.svg +0 -15
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/minimax.png +0 -0
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/mistral.png +0 -0
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/nvida.png +0 -0
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/openai.svg +0 -1
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/openrouter.png +0 -0
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/perplexity.svg +0 -24
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/qwen.png +0 -0
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/xai.png +0 -0
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/zai.png +0 -0
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/libs/highlight.css +0 -10
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/libs/highlight.js +0 -1213
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/libs/katex-auto-render.js +0 -1
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/libs/katex.css +0 -1
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/libs/katex.js +0 -1
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/libs/tailwind.css +0 -1
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/package-lock.json +0 -953
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/package.json +0 -16
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/tailwind.config.js +0 -12
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/tailwind.input.css +0 -235
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/template.html +0 -157
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/template.html.bak +0 -157
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/template.j2 +0 -259
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/__init__.py +0 -0
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/config.py +0 -36
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/history.py +0 -146
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/hyw.py +0 -41
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/pipeline.py +0 -840
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/render.py +0 -531
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/render.py.bak +0 -926
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/__init__.py +0 -3
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/browser.py +0 -61
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/mcp_playwright.py +0 -128
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/misc.py +0 -93
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/playwright_tool.py +0 -46
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/prompts.py +0 -94
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/search.py +0 -193
- entari_plugin_hyw-3.2.113/src/entari_plugin_hyw.egg-info/SOURCES.txt +0 -51
- {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1}/setup.cfg +0 -0
- {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1}/src/entari_plugin_hyw.egg-info/dependency_links.txt +0 -0
- {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1}/src/entari_plugin_hyw.egg-info/top_level.txt +0 -0
{entari_plugin_hyw-3.2.113/src/entari_plugin_hyw.egg-info → entari_plugin_hyw-3.3.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: entari_plugin_hyw
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: Use large language models to interpret chat messages
|
|
5
5
|
Author-email: kumoSleeping <zjr2992@outlook.com>
|
|
6
6
|
License: MIT
|
|
@@ -18,12 +18,7 @@ Requires-Python: >=3.10
|
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
Requires-Dist: arclet-entari[full]>=0.16.5
|
|
20
20
|
Requires-Dist: openai
|
|
21
|
-
Requires-Dist: mcp
|
|
22
21
|
Requires-Dist: httpx
|
|
23
|
-
Requires-Dist: markdown>=3.10
|
|
24
|
-
Requires-Dist: trafilatura>=2.0.0
|
|
25
|
-
Requires-Dist: playwright>=1.56.0
|
|
26
|
-
Requires-Dist: jinja2>=3.0
|
|
27
22
|
Provides-Extra: playwright
|
|
28
23
|
Requires-Dist: playwright>=1.56.0; extra == "playwright"
|
|
29
24
|
Requires-Dist: trafilatura>=2.0.0; extra == "playwright"
|
|
@@ -43,8 +38,6 @@ Requires-Dist: satori-python-adapter-onebot11>=0.2.5; extra == "dev"
|
|
|
43
38
|
|
|
44
39
|
</div>
|
|
45
40
|
|
|
46
|
-
# v3.2迎来大幅度改动、现在图文不符
|
|
47
|
-
|
|
48
41
|
## 🎑 效果展示
|
|
49
42
|
|
|
50
43
|
|
|
@@ -54,9 +47,12 @@ Requires-Dist: satori-python-adapter-onebot11>=0.2.5; extra == "dev"
|
|
|
54
47
|
</div>
|
|
55
48
|
|
|
56
49
|
## ✨ 功能特性
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
50
|
+
- **关于搜索**:
|
|
51
|
+
- 如果不设置 jina token, 模型会根据提示词优先使用 jina / playwright(成功率较低) 获取渲染 bing / google 混合搜索结果。
|
|
52
|
+
- 存在 jina token 时,模型会获得一个 web search 工具,~~但我没试过我喜欢白嫖~~。
|
|
53
|
+
- 也可以 OpenRouter 的 `:online` 参数,该参数会优先使用模型提供商的搜索、其次 `exa`(较贵) 进行网页搜索。
|
|
54
|
+
- 给予 `Alconna` 与 `MessageChain` 混合处理, 深度优化触发体验`。
|
|
55
|
+
- **网页获取**:支持通过 **Jina AI** 或 **Playwright** 进行实时页面获取。
|
|
60
56
|
- **多模态理解**:支持图片视觉分析。
|
|
61
57
|
- **上下文感知**:维护对话历史记录,支持连续的多轮对话。
|
|
62
58
|
- `reaction` 表情, 表示任务开始。
|
|
@@ -71,8 +67,12 @@ Requires-Dist: satori-python-adapter-onebot11>=0.2.5; extra == "dev"
|
|
|
71
67
|
pip install entari-plugin-hyw
|
|
72
68
|
```
|
|
73
69
|
|
|
74
|
-
###
|
|
75
|
-
|
|
70
|
+
### 启用 Playwright 支持
|
|
71
|
+
如果你希望使用 Playwright 进行本地网页渲染(而非仅使用 Jina AI):
|
|
72
|
+
```bash
|
|
73
|
+
pip install entari-plugin-hyw[playwright]
|
|
74
|
+
playwright install chromium
|
|
75
|
+
```
|
|
76
76
|
|
|
77
77
|
## ⚙️ 配置
|
|
78
78
|
|
|
@@ -86,6 +86,7 @@ plugins:
|
|
|
86
86
|
command_name_list: ["zssm", "hyw"]
|
|
87
87
|
|
|
88
88
|
# 主 LLM 模型配置(必需), 如 x-ai/grok-4.1-fast:online、perplexity/sonar
|
|
89
|
+
# 如果模型不自带搜索 模型会根据提示词优先使用 jina / playwright(成功率较低) 获取渲染 bing / google 混合搜索结果
|
|
89
90
|
model_name: "gx-ai/grok-4.1-fast:free"
|
|
90
91
|
api_key: "your-api-key"
|
|
91
92
|
|
|
@@ -93,8 +94,19 @@ plugins:
|
|
|
93
94
|
base_url: "openai-compatible-url"
|
|
94
95
|
|
|
95
96
|
# --- 浏览器与搜索 ---
|
|
97
|
+
# 网页浏览工具: "jina" (默认) 或 "playwright"
|
|
98
|
+
browser_tool: "jina"
|
|
99
|
+
|
|
100
|
+
# 可选: Jina AI API Key (配置以获得更高限额)(免费方案20/min)
|
|
101
|
+
# 配置此项同时会启用 web search 工具
|
|
102
|
+
jina_api_key: "jina_..."
|
|
103
|
+
|
|
104
|
+
# Playwright 设置
|
|
96
105
|
headless: true
|
|
97
106
|
|
|
107
|
+
# 浏览器回退: 当首选 browser_tool 失败时,尝试使用备用 browser_tool (默认: false)
|
|
108
|
+
enable_browser_fallback: false
|
|
109
|
+
|
|
98
110
|
# --- 视觉配置 (可选) ---
|
|
99
111
|
# 如果未设置,将回退使用主模型
|
|
100
112
|
vision_model_name: "qwen-vl-plus"
|
|
@@ -106,10 +118,6 @@ plugins:
|
|
|
106
118
|
reasoning:
|
|
107
119
|
effort: low
|
|
108
120
|
|
|
109
|
-
# --- 交互体验 ---
|
|
110
|
-
# 是否开启表情反应 (默认: true)
|
|
111
|
-
reaction: true
|
|
112
|
-
|
|
113
121
|
# --- 调试 ---
|
|
114
122
|
save_conversation: false
|
|
115
123
|
```
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
# v3.2迎来大幅度改动、现在图文不符
|
|
14
|
-
|
|
15
13
|
## 🎑 效果展示
|
|
16
14
|
|
|
17
15
|
|
|
@@ -21,9 +19,12 @@
|
|
|
21
19
|
</div>
|
|
22
20
|
|
|
23
21
|
## ✨ 功能特性
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
22
|
+
- **关于搜索**:
|
|
23
|
+
- 如果不设置 jina token, 模型会根据提示词优先使用 jina / playwright(成功率较低) 获取渲染 bing / google 混合搜索结果。
|
|
24
|
+
- 存在 jina token 时,模型会获得一个 web search 工具,~~但我没试过我喜欢白嫖~~。
|
|
25
|
+
- 也可以 OpenRouter 的 `:online` 参数,该参数会优先使用模型提供商的搜索、其次 `exa`(较贵) 进行网页搜索。
|
|
26
|
+
- 给予 `Alconna` 与 `MessageChain` 混合处理, 深度优化触发体验`。
|
|
27
|
+
- **网页获取**:支持通过 **Jina AI** 或 **Playwright** 进行实时页面获取。
|
|
27
28
|
- **多模态理解**:支持图片视觉分析。
|
|
28
29
|
- **上下文感知**:维护对话历史记录,支持连续的多轮对话。
|
|
29
30
|
- `reaction` 表情, 表示任务开始。
|
|
@@ -38,8 +39,12 @@
|
|
|
38
39
|
pip install entari-plugin-hyw
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
###
|
|
42
|
-
|
|
42
|
+
### 启用 Playwright 支持
|
|
43
|
+
如果你希望使用 Playwright 进行本地网页渲染(而非仅使用 Jina AI):
|
|
44
|
+
```bash
|
|
45
|
+
pip install entari-plugin-hyw[playwright]
|
|
46
|
+
playwright install chromium
|
|
47
|
+
```
|
|
43
48
|
|
|
44
49
|
## ⚙️ 配置
|
|
45
50
|
|
|
@@ -53,6 +58,7 @@ plugins:
|
|
|
53
58
|
command_name_list: ["zssm", "hyw"]
|
|
54
59
|
|
|
55
60
|
# 主 LLM 模型配置(必需), 如 x-ai/grok-4.1-fast:online、perplexity/sonar
|
|
61
|
+
# 如果模型不自带搜索 模型会根据提示词优先使用 jina / playwright(成功率较低) 获取渲染 bing / google 混合搜索结果
|
|
56
62
|
model_name: "gx-ai/grok-4.1-fast:free"
|
|
57
63
|
api_key: "your-api-key"
|
|
58
64
|
|
|
@@ -60,8 +66,19 @@ plugins:
|
|
|
60
66
|
base_url: "openai-compatible-url"
|
|
61
67
|
|
|
62
68
|
# --- 浏览器与搜索 ---
|
|
69
|
+
# 网页浏览工具: "jina" (默认) 或 "playwright"
|
|
70
|
+
browser_tool: "jina"
|
|
71
|
+
|
|
72
|
+
# 可选: Jina AI API Key (配置以获得更高限额)(免费方案20/min)
|
|
73
|
+
# 配置此项同时会启用 web search 工具
|
|
74
|
+
jina_api_key: "jina_..."
|
|
75
|
+
|
|
76
|
+
# Playwright 设置
|
|
63
77
|
headless: true
|
|
64
78
|
|
|
79
|
+
# 浏览器回退: 当首选 browser_tool 失败时,尝试使用备用 browser_tool (默认: false)
|
|
80
|
+
enable_browser_fallback: false
|
|
81
|
+
|
|
65
82
|
# --- 视觉配置 (可选) ---
|
|
66
83
|
# 如果未设置,将回退使用主模型
|
|
67
84
|
vision_model_name: "qwen-vl-plus"
|
|
@@ -73,10 +90,6 @@ plugins:
|
|
|
73
90
|
reasoning:
|
|
74
91
|
effort: low
|
|
75
92
|
|
|
76
|
-
# --- 交互体验 ---
|
|
77
|
-
# 是否开启表情反应 (默认: true)
|
|
78
|
-
reaction: true
|
|
79
|
-
|
|
80
93
|
# --- 调试 ---
|
|
81
94
|
save_conversation: false
|
|
82
95
|
```
|
|
@@ -4,18 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "entari_plugin_hyw"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.3.1"
|
|
8
8
|
description = "Use large language models to interpret chat messages"
|
|
9
9
|
authors = [{name = "kumoSleeping", email = "zjr2992@outlook.com"}]
|
|
10
10
|
dependencies = [
|
|
11
11
|
"arclet-entari[full]>=0.16.5",
|
|
12
12
|
"openai",
|
|
13
|
-
"mcp",
|
|
14
13
|
"httpx",
|
|
15
|
-
"markdown>=3.10",
|
|
16
|
-
"trafilatura>=2.0.0",
|
|
17
|
-
"playwright>=1.56.0",
|
|
18
|
-
"jinja2>=3.0",
|
|
19
14
|
]
|
|
20
15
|
requires-python = ">=3.10"
|
|
21
16
|
readme = "README.md"
|
|
@@ -43,9 +38,8 @@ Repository = "https://github.com/kumoSleeping/entari-plugin-hyw"
|
|
|
43
38
|
"Issue Tracker" = "https://github.com/kumoSleeping/entari-plugin-hyw/issues"
|
|
44
39
|
|
|
45
40
|
[tool.setuptools]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
[tool.setuptools.packages.find]
|
|
49
|
-
where = ["src"]
|
|
50
|
-
include = ["entari_plugin_hyw*"]
|
|
41
|
+
packages = ["entari_plugin_hyw"]
|
|
42
|
+
package-dir = {"" = "src"}
|
|
51
43
|
|
|
44
|
+
[tool.setuptools.package-data]
|
|
45
|
+
entari_plugin_hyw = ["*.txt", "*.md"]
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import html
|
|
3
|
+
import time
|
|
4
|
+
from collections import deque
|
|
5
|
+
from typing import Any, Deque, Dict, List, Optional, Set, Text, Tuple, Union, TYPE_CHECKING, cast
|
|
6
|
+
from typing_extensions import override
|
|
7
|
+
from arclet.entari import metadata
|
|
8
|
+
from arclet.entari import MessageChain, Session
|
|
9
|
+
from arclet.entari.event.base import MessageEvent
|
|
10
|
+
from loguru import logger
|
|
11
|
+
from satori.exception import ActionFailed
|
|
12
|
+
from arclet.entari import MessageChain, Image, Quote, Text
|
|
13
|
+
import arclet.letoderea as leto
|
|
14
|
+
from arclet.entari import MessageCreatedEvent, Session
|
|
15
|
+
from arclet.entari import BasicConfModel, metadata, plugin_config
|
|
16
|
+
import httpx
|
|
17
|
+
import asyncio
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
from arclet.alconna import (
|
|
21
|
+
Args,
|
|
22
|
+
Alconna,
|
|
23
|
+
AllParam,
|
|
24
|
+
MultiVar,
|
|
25
|
+
CommandMeta,
|
|
26
|
+
Option,
|
|
27
|
+
)
|
|
28
|
+
from arclet.entari import MessageChain, Session, command
|
|
29
|
+
from arclet.entari import plugin, Ready, Cleanup, Startup
|
|
30
|
+
from satori.element import Custom, E
|
|
31
|
+
from .hyw_core import HYW, HYWConfig
|
|
32
|
+
|
|
33
|
+
# 全局变量
|
|
34
|
+
hyw_core = None
|
|
35
|
+
|
|
36
|
+
class HistoryManager:
|
|
37
|
+
def __init__(self, max_records: int = 20):
|
|
38
|
+
self.max_records = max_records
|
|
39
|
+
self._order: Deque[str] = deque()
|
|
40
|
+
self._store: Dict[str, List[dict]] = {}
|
|
41
|
+
self._bindings: Dict[str, Set[str]] = {}
|
|
42
|
+
self._msg_map: Dict[str, str] = {}
|
|
43
|
+
|
|
44
|
+
def extract_message_id(self, message_like: Any) -> Optional[str]:
|
|
45
|
+
if message_like is None:
|
|
46
|
+
return None
|
|
47
|
+
if isinstance(message_like, (list, tuple)):
|
|
48
|
+
for item in message_like:
|
|
49
|
+
mid = self.extract_message_id(item)
|
|
50
|
+
if mid:
|
|
51
|
+
return mid
|
|
52
|
+
return None
|
|
53
|
+
if isinstance(message_like, dict):
|
|
54
|
+
for key in ("message_id", "id"):
|
|
55
|
+
value = message_like.get(key)
|
|
56
|
+
if value:
|
|
57
|
+
return str(value)
|
|
58
|
+
for attr in ("message_id", "id"):
|
|
59
|
+
value = getattr(message_like, attr, None)
|
|
60
|
+
if value:
|
|
61
|
+
return str(value)
|
|
62
|
+
nested = getattr(message_like, "message", None)
|
|
63
|
+
if nested is not None and nested is not message_like:
|
|
64
|
+
return self.extract_message_id(nested)
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def remove(self, conversation_id: Optional[str], *, remove_from_order: bool = True) -> None:
|
|
68
|
+
if not conversation_id:
|
|
69
|
+
return
|
|
70
|
+
cid = str(conversation_id)
|
|
71
|
+
if remove_from_order:
|
|
72
|
+
try:
|
|
73
|
+
self._order.remove(cid)
|
|
74
|
+
except ValueError:
|
|
75
|
+
pass
|
|
76
|
+
bindings = self._bindings.pop(cid, set())
|
|
77
|
+
for msg_id in bindings:
|
|
78
|
+
self._msg_map.pop(msg_id, None)
|
|
79
|
+
self._store.pop(cid, None)
|
|
80
|
+
|
|
81
|
+
def _enforce_limit(self) -> None:
|
|
82
|
+
while len(self._order) > self.max_records:
|
|
83
|
+
obsolete = self._order.popleft()
|
|
84
|
+
self.remove(obsolete, remove_from_order=False)
|
|
85
|
+
|
|
86
|
+
def remember(self, conversation_id: Optional[str], history: Optional[List[dict]], related_ids: List[Optional[str]]) -> None:
|
|
87
|
+
if not conversation_id or not history:
|
|
88
|
+
return
|
|
89
|
+
cid = str(conversation_id)
|
|
90
|
+
self._store[cid] = list(history)
|
|
91
|
+
binding_ids = {str(mid) for mid in related_ids if mid}
|
|
92
|
+
self._bindings[cid] = binding_ids
|
|
93
|
+
for mid in binding_ids:
|
|
94
|
+
self._msg_map[mid] = cid
|
|
95
|
+
self._order.append(cid)
|
|
96
|
+
self._enforce_limit()
|
|
97
|
+
|
|
98
|
+
def get_history(self, msg_id: str) -> Optional[List[dict]]:
|
|
99
|
+
cid = self._msg_map.get(msg_id)
|
|
100
|
+
if cid:
|
|
101
|
+
return list(self._store.get(cid, []))
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def get_conversation_id(self, msg_id: str) -> Optional[str]:
|
|
105
|
+
return self._msg_map.get(msg_id)
|
|
106
|
+
|
|
107
|
+
history_manager = HistoryManager()
|
|
108
|
+
|
|
109
|
+
# Request lock for HYW agent
|
|
110
|
+
_hyw_request_lock: Optional[asyncio.Lock] = None
|
|
111
|
+
|
|
112
|
+
def _get_hyw_request_lock() -> asyncio.Lock:
|
|
113
|
+
global _hyw_request_lock
|
|
114
|
+
if _hyw_request_lock is None:
|
|
115
|
+
_hyw_request_lock = asyncio.Lock()
|
|
116
|
+
return _hyw_request_lock
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class HywConfig(BasicConfModel):
|
|
120
|
+
command_name_list: Union[str, List[str]] = "hyw"
|
|
121
|
+
model_name: str
|
|
122
|
+
api_key: str
|
|
123
|
+
base_url: str = "https://openrouter.ai/api/v1"
|
|
124
|
+
headless: bool = False
|
|
125
|
+
save_conversation: bool = False
|
|
126
|
+
|
|
127
|
+
browser_tool: str = "jina"
|
|
128
|
+
jina_api_key: Optional[str] = None
|
|
129
|
+
|
|
130
|
+
vision_model_name: Optional[str] = None
|
|
131
|
+
vision_base_url: Optional[str] = None
|
|
132
|
+
vision_api_key: Optional[str] = None
|
|
133
|
+
|
|
134
|
+
extra_body: Optional[Dict[str, Any]] = None
|
|
135
|
+
|
|
136
|
+
enable_browser_fallback: bool = False
|
|
137
|
+
# verbose: bool = False
|
|
138
|
+
|
|
139
|
+
metadata(
|
|
140
|
+
"hyw",
|
|
141
|
+
author=[{"name": "kumoSleeping", "email": "zjr2992@outlook.com"}],
|
|
142
|
+
version="3.3.1",
|
|
143
|
+
description="",
|
|
144
|
+
config=HywConfig,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
conf = plugin_config(HywConfig)
|
|
148
|
+
alc = Alconna(
|
|
149
|
+
conf.command_name_list,
|
|
150
|
+
Option("-t|--text", dest="text_only", default=False, help_text="仅文本模式(禁用图片识别)"),
|
|
151
|
+
Args["all_param", AllParam],
|
|
152
|
+
# Option("-v|--verbose", dest="verbose", default=False, help_text="启用详细日志输出"),
|
|
153
|
+
meta=CommandMeta(compact=False)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Create HYW configuration
|
|
157
|
+
hyw_config = HYWConfig(
|
|
158
|
+
api_key=conf.api_key,
|
|
159
|
+
model_name=conf.model_name,
|
|
160
|
+
base_url=conf.base_url,
|
|
161
|
+
save_conversation=conf.save_conversation,
|
|
162
|
+
headless=conf.headless,
|
|
163
|
+
browser_tool=conf.browser_tool,
|
|
164
|
+
jina_api_key=conf.jina_api_key,
|
|
165
|
+
vision_model_name=conf.vision_model_name,
|
|
166
|
+
vision_base_url=conf.vision_base_url,
|
|
167
|
+
vision_api_key=conf.vision_api_key,
|
|
168
|
+
extra_body=conf.extra_body,
|
|
169
|
+
enable_browser_fallback=conf.enable_browser_fallback
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
hyw = HYW(config=hyw_config)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# Emoji到代码的映射字典
|
|
177
|
+
EMOJI_TO_CODE = {
|
|
178
|
+
"🐳": "128051",
|
|
179
|
+
"❌": "10060",
|
|
180
|
+
"🍧": "127847",
|
|
181
|
+
"✨": "10024",
|
|
182
|
+
"📫": "128235"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async def download_image(url: str) -> bytes:
|
|
186
|
+
"""下载图片"""
|
|
187
|
+
try:
|
|
188
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
189
|
+
resp = await client.get(url)
|
|
190
|
+
if resp.status_code == 200:
|
|
191
|
+
return resp.content
|
|
192
|
+
else:
|
|
193
|
+
raise ActionFailed(f"下载图片失败,状态码: {resp.status_code}")
|
|
194
|
+
except Exception as e:
|
|
195
|
+
raise ActionFailed(f"下载图片失败: {url}, 错误: {str(e)}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def process_onebot_json(json_data_str: str) -> str:
|
|
199
|
+
try:
|
|
200
|
+
# 解码HTML实体
|
|
201
|
+
json_str = html.unescape(json_data_str)
|
|
202
|
+
return json_str
|
|
203
|
+
except Exception as e:
|
|
204
|
+
return json_data_str
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def react(session: Session, emoji: str):
|
|
208
|
+
try:
|
|
209
|
+
if session.event.login.platform == "onebot":
|
|
210
|
+
code = EMOJI_TO_CODE.get(emoji, "10024")
|
|
211
|
+
await session.account.protocol.call_api("internal/set_group_reaction", {"group_id": int(session.guild.id), "message_id": int(session.event.message.id), "code": code, "is_add": True})
|
|
212
|
+
else:
|
|
213
|
+
await session.reaction_create(emoji=emoji)
|
|
214
|
+
except ActionFailed:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
def handle_shortcut(message_chain: MessageChain) -> Tuple[bool, str]:
|
|
218
|
+
current_msg_text = str(message_chain.get(Text)) if message_chain.get(Text) else ""
|
|
219
|
+
is_shortcut = False
|
|
220
|
+
shortcut_replacement = ""
|
|
221
|
+
if current_msg_text.strip().startswith("/"):
|
|
222
|
+
is_shortcut = True
|
|
223
|
+
shortcut_replacement = current_msg_text.strip()[1:]
|
|
224
|
+
return is_shortcut, shortcut_replacement
|
|
225
|
+
|
|
226
|
+
async def process_images(mc: MessageChain, parse_result: Any) -> Tuple[List[str], Optional[str]]:
|
|
227
|
+
is_text_only = False
|
|
228
|
+
if parse_result.matched:
|
|
229
|
+
def get_bool_value(val):
|
|
230
|
+
if hasattr(val, 'value'):
|
|
231
|
+
return bool(val.value)
|
|
232
|
+
return bool(val)
|
|
233
|
+
is_text_only = get_bool_value(getattr(parse_result, 'text_only', False))
|
|
234
|
+
|
|
235
|
+
text_str = str(mc.get(Text) or "")
|
|
236
|
+
if not is_text_only and re.search(r'(?:^|\s)(-t|--text)(?:$|\s)', text_str):
|
|
237
|
+
is_text_only = True
|
|
238
|
+
|
|
239
|
+
if is_text_only:
|
|
240
|
+
logger.info("检测到仅文本模式参数,跳过图片分析")
|
|
241
|
+
return [], None
|
|
242
|
+
|
|
243
|
+
has_images = bool(mc.get(Image))
|
|
244
|
+
images = []
|
|
245
|
+
if has_images:
|
|
246
|
+
urls = mc[Image].map(lambda x: x.src)
|
|
247
|
+
tasks = [download_image(url) for url in urls]
|
|
248
|
+
raw_images = await asyncio.gather(*tasks)
|
|
249
|
+
import base64
|
|
250
|
+
images = [base64.b64encode(img).decode('utf-8') for img in raw_images]
|
|
251
|
+
|
|
252
|
+
return images, None
|
|
253
|
+
|
|
254
|
+
@leto.on(MessageCreatedEvent)
|
|
255
|
+
async def on_message_created(message_chain: MessageChain, session: Session[MessageEvent]):
|
|
256
|
+
# Skip if no substantial content in original message
|
|
257
|
+
original_text = str(message_chain.get(Text)).strip()
|
|
258
|
+
has_images = bool(message_chain.get(Image))
|
|
259
|
+
has_custom = bool(message_chain.get(Custom))
|
|
260
|
+
if not original_text and not has_images and not has_custom:
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
if session.reply:
|
|
264
|
+
try:
|
|
265
|
+
message_chain.extend(MessageChain(" ") + session.reply.origin.message)
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
message_chain = message_chain.get(Text) + message_chain.get(Image) + message_chain.get(Custom)
|
|
270
|
+
|
|
271
|
+
quoted_message_id: Optional[str] = None
|
|
272
|
+
conversation_history_key: Optional[str] = None
|
|
273
|
+
conversation_history_payload: List[dict] = []
|
|
274
|
+
|
|
275
|
+
if session.reply:
|
|
276
|
+
try:
|
|
277
|
+
quoted_message_id = str(session.reply.origin.id) if hasattr(session.reply.origin, 'id') else None
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.warning(f"提取引用消息ID失败: {e}")
|
|
280
|
+
quoted_message_id = None
|
|
281
|
+
|
|
282
|
+
if quoted_message_id:
|
|
283
|
+
conversation_history_key = history_manager.get_conversation_id(quoted_message_id)
|
|
284
|
+
if conversation_history_key:
|
|
285
|
+
conversation_history_payload = history_manager.get_history(quoted_message_id) or []
|
|
286
|
+
logger.info(f"继续对话模式触发, 引用消息ID: {quoted_message_id}, 历史长度: {len(conversation_history_payload)}")
|
|
287
|
+
|
|
288
|
+
parse_result = alc.parse(message_chain)
|
|
289
|
+
is_shortcut, shortcut_replacement = handle_shortcut(message_chain)
|
|
290
|
+
|
|
291
|
+
should_process = parse_result.matched or (bool(conversation_history_key) and is_shortcut)
|
|
292
|
+
|
|
293
|
+
if not should_process:
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
raw_param_chain: MessageChain = parse_result.all_param if parse_result.matched else message_chain # type: ignore
|
|
297
|
+
if not parse_result.matched and is_shortcut:
|
|
298
|
+
logger.debug(f"触发快捷指令,替换内容: {shortcut_replacement}")
|
|
299
|
+
|
|
300
|
+
mc = MessageChain(raw_param_chain)
|
|
301
|
+
|
|
302
|
+
async def process_request() -> None:
|
|
303
|
+
await react(session, "✨")
|
|
304
|
+
try:
|
|
305
|
+
if is_shortcut and not parse_result.matched:
|
|
306
|
+
msg = shortcut_replacement
|
|
307
|
+
else:
|
|
308
|
+
msg = mc.get(Text).strip() if mc.get(Text) else ""
|
|
309
|
+
|
|
310
|
+
if mc.get(Custom): # type: ignore
|
|
311
|
+
custom_elements = [e for e in mc if isinstance(e, Custom)]
|
|
312
|
+
for custom in custom_elements:
|
|
313
|
+
if custom.tag == 'onebot:json':
|
|
314
|
+
decoded_json = process_onebot_json(custom.attributes())
|
|
315
|
+
msg += decoded_json
|
|
316
|
+
break
|
|
317
|
+
|
|
318
|
+
time_start = time.perf_counter()
|
|
319
|
+
images, error_msg = await process_images(mc, parse_result)
|
|
320
|
+
|
|
321
|
+
if error_msg:
|
|
322
|
+
await session.send(error_msg)
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
lock = _get_hyw_request_lock()
|
|
326
|
+
async with lock:
|
|
327
|
+
response = await hyw.agent(str(msg), conversation_history=conversation_history_payload, images=images)
|
|
328
|
+
|
|
329
|
+
response_content = response.get("llm_response", "") if isinstance(response, dict) else ""
|
|
330
|
+
new_history = response.get("conversation_history", []) if isinstance(response, dict) else []
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
send_result = await session.send([Quote(session.event.message.id), response_content])
|
|
334
|
+
except ActionFailed as e:
|
|
335
|
+
if "9057" in str(e):
|
|
336
|
+
logger.warning(f"发送消息失败(9057),尝试截断发送: {e}")
|
|
337
|
+
truncated_content = response_content[:1000] + "\n\n[...内容过长,已大幅截断...]"
|
|
338
|
+
send_result = await session.send([Quote(session.event.message.id), truncated_content])
|
|
339
|
+
else:
|
|
340
|
+
raise e
|
|
341
|
+
|
|
342
|
+
sent_message_id = history_manager.extract_message_id(send_result)
|
|
343
|
+
current_user_message_id = str(session.event.message.id)
|
|
344
|
+
related_ids: List[Optional[str]] = [current_user_message_id, sent_message_id]
|
|
345
|
+
|
|
346
|
+
if conversation_history_key:
|
|
347
|
+
history_manager.remove(conversation_history_key)
|
|
348
|
+
related_ids.append(quoted_message_id)
|
|
349
|
+
|
|
350
|
+
# Check turn limit
|
|
351
|
+
user_turns = len([m for m in new_history if m.get("role") == "user"])
|
|
352
|
+
if user_turns < 5:
|
|
353
|
+
history_manager.remember(sent_message_id, new_history, related_ids)
|
|
354
|
+
else:
|
|
355
|
+
logger.info(f"对话轮数达到上限 ({user_turns}),停止记录历史")
|
|
356
|
+
|
|
357
|
+
except Exception as exc:
|
|
358
|
+
await react(session, "❌")
|
|
359
|
+
logger.exception("处理HYW消息失败: {}", exc)
|
|
360
|
+
|
|
361
|
+
asyncio.create_task(process_request())
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
|