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.

Files changed (56) hide show
  1. {entari_plugin_hyw-3.2.113/src/entari_plugin_hyw.egg-info → entari_plugin_hyw-3.3.1}/PKG-INFO +25 -17
  2. {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1}/README.md +24 -11
  3. {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1}/pyproject.toml +5 -11
  4. entari_plugin_hyw-3.3.1/src/entari_plugin_hyw/__init__.py +364 -0
  5. entari_plugin_hyw-3.3.1/src/entari_plugin_hyw/hyw_core.py +700 -0
  6. {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1/src/entari_plugin_hyw.egg-info}/PKG-INFO +25 -17
  7. entari_plugin_hyw-3.3.1/src/entari_plugin_hyw.egg-info/SOURCES.txt +9 -0
  8. {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1}/src/entari_plugin_hyw.egg-info/requires.txt +0 -5
  9. entari_plugin_hyw-3.2.113/MANIFEST.in +0 -3
  10. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/__init__.py +0 -813
  11. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/anthropic.svg +0 -1
  12. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/deepseek.png +0 -0
  13. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/gemini.svg +0 -1
  14. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/google.svg +0 -1
  15. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/grok.png +0 -0
  16. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/microsoft.svg +0 -15
  17. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/minimax.png +0 -0
  18. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/mistral.png +0 -0
  19. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/nvida.png +0 -0
  20. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/openai.svg +0 -1
  21. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/openrouter.png +0 -0
  22. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/perplexity.svg +0 -24
  23. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/qwen.png +0 -0
  24. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/xai.png +0 -0
  25. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/icon/zai.png +0 -0
  26. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/libs/highlight.css +0 -10
  27. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/libs/highlight.js +0 -1213
  28. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/libs/katex-auto-render.js +0 -1
  29. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/libs/katex.css +0 -1
  30. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/libs/katex.js +0 -1
  31. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/libs/tailwind.css +0 -1
  32. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/package-lock.json +0 -953
  33. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/package.json +0 -16
  34. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/tailwind.config.js +0 -12
  35. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/tailwind.input.css +0 -235
  36. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/template.html +0 -157
  37. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/template.html.bak +0 -157
  38. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/assets/template.j2 +0 -259
  39. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/__init__.py +0 -0
  40. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/config.py +0 -36
  41. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/history.py +0 -146
  42. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/hyw.py +0 -41
  43. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/pipeline.py +0 -840
  44. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/render.py +0 -531
  45. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/core/render.py.bak +0 -926
  46. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/__init__.py +0 -3
  47. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/browser.py +0 -61
  48. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/mcp_playwright.py +0 -128
  49. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/misc.py +0 -93
  50. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/playwright_tool.py +0 -46
  51. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/prompts.py +0 -94
  52. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw/utils/search.py +0 -193
  53. entari_plugin_hyw-3.2.113/src/entari_plugin_hyw.egg-info/SOURCES.txt +0 -51
  54. {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1}/setup.cfg +0 -0
  55. {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1}/src/entari_plugin_hyw.egg-info/dependency_links.txt +0 -0
  56. {entari_plugin_hyw-3.2.113 → entari_plugin_hyw-3.3.1}/src/entari_plugin_hyw.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entari_plugin_hyw
3
- Version: 3.2.113
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
- - **关于搜索**:一次性触发 Bing 网页与图片搜索,组合结果后再回应。
58
- - 给予 `Alconna` `MessageChain` 混合处理, 深度优化触发体验。
59
- - **网页获取**:使用 Playwright 进行实时页面获取。
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
- 默认通过 HTTP 请求搜索引擎(DuckDuckGo,可在配置中自定义完整搜索链接,如 `https://duckduckgo.com/?q={query}`)。
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
- - **关于搜索**:一次性触发 Bing 网页与图片搜索,组合结果后再回应。
25
- - 给予 `Alconna` `MessageChain` 混合处理, 深度优化触发体验。
26
- - **网页获取**:使用 Playwright 进行实时页面获取。
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
- 默认通过 HTTP 请求搜索引擎(DuckDuckGo,可在配置中自定义完整搜索链接,如 `https://duckduckgo.com/?q={query}`)。
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.2.113"
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
- include-package-data = true
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
+