entari-plugin-hyw 3.3.0__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 (55) hide show
  1. {entari_plugin_hyw-3.3.0/src/entari_plugin_hyw.egg-info → entari_plugin_hyw-3.3.1}/PKG-INFO +28 -20
  2. {entari_plugin_hyw-3.3.0 → entari_plugin_hyw-3.3.1}/README.md +24 -16
  3. {entari_plugin_hyw-3.3.0 → entari_plugin_hyw-3.3.1}/pyproject.toml +6 -8
  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.3.0 → entari_plugin_hyw-3.3.1/src/entari_plugin_hyw.egg-info}/PKG-INFO +28 -20
  7. entari_plugin_hyw-3.3.1/src/entari_plugin_hyw.egg-info/SOURCES.txt +9 -0
  8. {entari_plugin_hyw-3.3.0 → entari_plugin_hyw-3.3.1}/src/entari_plugin_hyw.egg-info/requires.txt +4 -3
  9. entari_plugin_hyw-3.3.0/MANIFEST.in +0 -3
  10. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/__init__.py +0 -818
  11. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/anthropic.svg +0 -1
  12. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/deepseek.png +0 -0
  13. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/gemini.svg +0 -1
  14. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/google.svg +0 -1
  15. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/grok.png +0 -0
  16. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/microsoft.svg +0 -15
  17. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/minimax.png +0 -0
  18. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/mistral.png +0 -0
  19. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/nvida.png +0 -0
  20. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/openai.svg +0 -1
  21. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/openrouter.png +0 -0
  22. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/perplexity.svg +0 -24
  23. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/qwen.png +0 -0
  24. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/xai.png +0 -0
  25. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/icon/zai.png +0 -0
  26. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/libs/highlight.css +0 -10
  27. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/libs/highlight.js +0 -1213
  28. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/libs/katex-auto-render.js +0 -1
  29. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/libs/katex.css +0 -1
  30. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/libs/katex.js +0 -1
  31. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/libs/tailwind.css +0 -1
  32. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/package-lock.json +0 -953
  33. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/package.json +0 -16
  34. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/tailwind.config.js +0 -12
  35. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/tailwind.input.css +0 -235
  36. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/template.html +0 -157
  37. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/template.html.bak +0 -157
  38. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/assets/template.j2 +0 -307
  39. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/core/__init__.py +0 -0
  40. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/core/config.py +0 -35
  41. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/core/history.py +0 -146
  42. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/core/hyw.py +0 -41
  43. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/core/pipeline.py +0 -1065
  44. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/core/render.py +0 -596
  45. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/core/render.py.bak +0 -926
  46. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/utils/__init__.py +0 -2
  47. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/utils/browser.py +0 -40
  48. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/utils/misc.py +0 -93
  49. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/utils/playwright_tool.py +0 -36
  50. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/utils/prompts.py +0 -128
  51. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw/utils/search.py +0 -241
  52. entari_plugin_hyw-3.3.0/src/entari_plugin_hyw.egg-info/SOURCES.txt +0 -50
  53. {entari_plugin_hyw-3.3.0 → entari_plugin_hyw-3.3.1}/setup.cfg +0 -0
  54. {entari_plugin_hyw-3.3.0 → entari_plugin_hyw-3.3.1}/src/entari_plugin_hyw.egg-info/dependency_links.txt +0 -0
  55. {entari_plugin_hyw-3.3.0 → 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.3.0
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
@@ -19,9 +19,9 @@ Description-Content-Type: text/markdown
19
19
  Requires-Dist: arclet-entari[full]>=0.16.5
20
20
  Requires-Dist: openai
21
21
  Requires-Dist: httpx
22
- Requires-Dist: markdown>=3.10
23
- Requires-Dist: crawl4ai>=0.7.8
24
- Requires-Dist: jinja2>=3.0
22
+ Provides-Extra: playwright
23
+ Requires-Dist: playwright>=1.56.0; extra == "playwright"
24
+ Requires-Dist: trafilatura>=2.0.0; extra == "playwright"
25
25
  Provides-Extra: dev
26
26
  Requires-Dist: entari-plugin-server>=0.5.0; extra == "dev"
27
27
  Requires-Dist: satori-python-adapter-onebot11>=0.2.5; extra == "dev"
@@ -38,10 +38,6 @@ Requires-Dist: satori-python-adapter-onebot11>=0.2.5; extra == "dev"
38
38
 
39
39
  </div>
40
40
 
41
- # v3.3 迎来大幅度改动、现在图文不符
42
-
43
-
44
-
45
41
  ## 🎑 效果展示
46
42
 
47
43
 
@@ -51,9 +47,12 @@ Requires-Dist: satori-python-adapter-onebot11>=0.2.5; extra == "dev"
51
47
  </div>
52
48
 
53
49
  ## ✨ 功能特性
54
- - **关于搜索**:一次性触发 Bing 网页与图片搜索,组合结果后再回应。
55
- - 给予 `Alconna` `MessageChain` 混合处理, 深度优化触发体验。
56
- - **网页获取**:使用 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** 进行实时页面获取。
57
56
  - **多模态理解**:支持图片视觉分析。
58
57
  - **上下文感知**:维护对话历史记录,支持连续的多轮对话。
59
58
  - `reaction` 表情, 表示任务开始。
@@ -68,8 +67,12 @@ Requires-Dist: satori-python-adapter-onebot11>=0.2.5; extra == "dev"
68
67
  pip install entari-plugin-hyw
69
68
  ```
70
69
 
71
- ### 搜索
72
- 默认通过 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
+ ```
73
76
 
74
77
  ## ⚙️ 配置
75
78
 
@@ -83,6 +86,7 @@ plugins:
83
86
  command_name_list: ["zssm", "hyw"]
84
87
 
85
88
  # 主 LLM 模型配置(必需), 如 x-ai/grok-4.1-fast:online、perplexity/sonar
89
+ # 如果模型不自带搜索 模型会根据提示词优先使用 jina / playwright(成功率较低) 获取渲染 bing / google 混合搜索结果
86
90
  model_name: "gx-ai/grok-4.1-fast:free"
87
91
  api_key: "your-api-key"
88
92
 
@@ -90,8 +94,19 @@ plugins:
90
94
  base_url: "openai-compatible-url"
91
95
 
92
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 设置
93
105
  headless: true
94
106
 
107
+ # 浏览器回退: 当首选 browser_tool 失败时,尝试使用备用 browser_tool (默认: false)
108
+ enable_browser_fallback: false
109
+
95
110
  # --- 视觉配置 (可选) ---
96
111
  # 如果未设置,将回退使用主模型
97
112
  vision_model_name: "qwen-vl-plus"
@@ -103,10 +118,6 @@ plugins:
103
118
  reasoning:
104
119
  effort: low
105
120
 
106
- # --- 交互体验 ---
107
- # 是否开启表情反应 (默认: true)
108
- reaction: true
109
-
110
121
  # --- 调试 ---
111
122
  save_conversation: false
112
123
  ```
@@ -137,6 +148,3 @@ hyw -t 一大段话。
137
148
  ### 引用回复
138
149
  支持引用消息进行追问,机器人会自动读取被引用的消息作为上下文:
139
150
  - **引用 + 命令**:机器人将理解被引用消息的内容(包括图片)通过 `MessageChain` 操作拼接 `Text`、`Image` 与部分 `Custom`。
140
-
141
- UncleCode. (2024). Crawl4AI: Open-source LLM Friendly Web Crawler & Scraper [Computer software].
142
- GitHub. https://github.com/unclecode/crawl4ai
@@ -10,10 +10,6 @@
10
10
 
11
11
  </div>
12
12
 
13
- # v3.3 迎来大幅度改动、现在图文不符
14
-
15
-
16
-
17
13
  ## 🎑 效果展示
18
14
 
19
15
 
@@ -23,9 +19,12 @@
23
19
  </div>
24
20
 
25
21
  ## ✨ 功能特性
26
- - **关于搜索**:一次性触发 Bing 网页与图片搜索,组合结果后再回应。
27
- - 给予 `Alconna` `MessageChain` 混合处理, 深度优化触发体验。
28
- - **网页获取**:使用 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** 进行实时页面获取。
29
28
  - **多模态理解**:支持图片视觉分析。
30
29
  - **上下文感知**:维护对话历史记录,支持连续的多轮对话。
31
30
  - `reaction` 表情, 表示任务开始。
@@ -40,8 +39,12 @@
40
39
  pip install entari-plugin-hyw
41
40
  ```
42
41
 
43
- ### 搜索
44
- 默认通过 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
+ ```
45
48
 
46
49
  ## ⚙️ 配置
47
50
 
@@ -55,6 +58,7 @@ plugins:
55
58
  command_name_list: ["zssm", "hyw"]
56
59
 
57
60
  # 主 LLM 模型配置(必需), 如 x-ai/grok-4.1-fast:online、perplexity/sonar
61
+ # 如果模型不自带搜索 模型会根据提示词优先使用 jina / playwright(成功率较低) 获取渲染 bing / google 混合搜索结果
58
62
  model_name: "gx-ai/grok-4.1-fast:free"
59
63
  api_key: "your-api-key"
60
64
 
@@ -62,8 +66,19 @@ plugins:
62
66
  base_url: "openai-compatible-url"
63
67
 
64
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 设置
65
77
  headless: true
66
78
 
79
+ # 浏览器回退: 当首选 browser_tool 失败时,尝试使用备用 browser_tool (默认: false)
80
+ enable_browser_fallback: false
81
+
67
82
  # --- 视觉配置 (可选) ---
68
83
  # 如果未设置,将回退使用主模型
69
84
  vision_model_name: "qwen-vl-plus"
@@ -75,10 +90,6 @@ plugins:
75
90
  reasoning:
76
91
  effort: low
77
92
 
78
- # --- 交互体验 ---
79
- # 是否开启表情反应 (默认: true)
80
- reaction: true
81
-
82
93
  # --- 调试 ---
83
94
  save_conversation: false
84
95
  ```
@@ -109,6 +120,3 @@ hyw -t 一大段话。
109
120
  ### 引用回复
110
121
  支持引用消息进行追问,机器人会自动读取被引用的消息作为上下文:
111
122
  - **引用 + 命令**:机器人将理解被引用消息的内容(包括图片)通过 `MessageChain` 操作拼接 `Text`、`Image` 与部分 `Custom`。
112
-
113
- UncleCode. (2024). Crawl4AI: Open-source LLM Friendly Web Crawler & Scraper [Computer software].
114
- GitHub. https://github.com/unclecode/crawl4ai
@@ -4,16 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "entari_plugin_hyw"
7
- version = "3.3.0"
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
13
  "httpx",
14
- "markdown>=3.10",
15
- "crawl4ai>=0.7.8",
16
- "jinja2>=3.0",
17
14
  ]
18
15
  requires-python = ">=3.10"
19
16
  readme = "README.md"
@@ -29,6 +26,7 @@ classifiers = [
29
26
  ]
30
27
 
31
28
  [project.optional-dependencies]
29
+ playwright = ["playwright>=1.56.0", "trafilatura>=2.0.0"]
32
30
  dev = [
33
31
  "entari-plugin-server>=0.5.0",
34
32
  "satori-python-adapter-onebot11>=0.2.5",
@@ -40,8 +38,8 @@ Repository = "https://github.com/kumoSleeping/entari-plugin-hyw"
40
38
  "Issue Tracker" = "https://github.com/kumoSleeping/entari-plugin-hyw/issues"
41
39
 
42
40
  [tool.setuptools]
43
- include-package-data = true
41
+ packages = ["entari_plugin_hyw"]
42
+ package-dir = {"" = "src"}
44
43
 
45
- [tool.setuptools.packages.find]
46
- where = ["src"]
47
- include = ["entari_plugin_hyw*"]
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
+