hermes-baiduapp 0.1.0__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.
@@ -0,0 +1,121 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ lerna-debug.log*
8
+
9
+ # Diagnostic reports (https://nodejs.org/api/report.html)
10
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11
+
12
+ # Runtime data
13
+ pids
14
+ *.pid
15
+ *.seed
16
+ *.pid.lock
17
+
18
+ # Directory for instrumented libs generated by jscoverage/JSCover
19
+ lib-cov
20
+
21
+ # Coverage directory used by tools like istanbul
22
+ coverage
23
+ *.lcov
24
+
25
+ # nyc test coverage
26
+ .nyc_output
27
+
28
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29
+ .grunt
30
+
31
+ # Bower dependency directory (https://bower.io/)
32
+ bower_components
33
+
34
+ # node-waf configuration
35
+ .lock-wscript
36
+
37
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
38
+ build/Release
39
+
40
+ # Dependency directories
41
+ node_modules/
42
+ jspm_packages/
43
+
44
+ # TypeScript v1 declaration files
45
+ typings/
46
+
47
+ # TypeScript cache
48
+ *.tsbuildinfo
49
+
50
+ # Optional npm cache directory
51
+ .npm
52
+
53
+ # Optional eslint cache
54
+ .eslintcache
55
+
56
+ # Microbundle cache
57
+ .rpt2_cache/
58
+ .rts2_cache_cjs/
59
+ .rts2_cache_es/
60
+ .rts2_cache_umd/
61
+
62
+ # Optional REPL history
63
+ .node_repl_history
64
+
65
+ # Output of 'npm pack'
66
+ *.tgz
67
+
68
+ # Yarn Integrity file
69
+ .yarn-integrity
70
+
71
+ # dotenv environment variables file
72
+ .env
73
+ .env.test
74
+
75
+ # parcel-bundler cache (https://parceljs.org/)
76
+ .cache
77
+
78
+ # Next.js build output
79
+ .next
80
+
81
+ # Nuxt.js build / generate output
82
+ .nuxt
83
+ dist
84
+
85
+ # Gatsby files
86
+ .cache/
87
+ # Comment in the public line in if your project uses Gatsby and *not* Next.js
88
+ # https://nextjs.org/blog/next-9-1#public-directory-support
89
+ # public
90
+
91
+ # vuepress build output
92
+ .vuepress/dist
93
+
94
+ # Serverless directories
95
+ .serverless/
96
+
97
+ # FuseBox cache
98
+ .fusebox/
99
+
100
+ # DynamoDB Local files
101
+ .dynamodb/
102
+
103
+ # TernJS port file
104
+ .tern-port
105
+
106
+ # mac
107
+ .DS_Store
108
+
109
+ log/
110
+
111
+ .env
112
+
113
+ node_modules.zip
114
+
115
+ output
116
+
117
+ # ReCode local database
118
+ .recode
119
+
120
+ CLAUDE.md
121
+ .sisyphus
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: hermes-baiduapp
3
+ Version: 0.1.0
4
+ Summary: Baidu App platform plugin for Hermes Agent (polling mode, no webhook)
5
+ License: MIT
6
+ Keywords: baidu,chatbot,gateway,hermes,hermes-agent
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
10
+ # hermes-baiduapp
11
+
12
+ Baidu App platform plugin for [Hermes Agent](https://github.com/NousResearch/hermes-agent).
13
+
14
+ Uses polling mode (`/channel/msg/poll`) to receive messages. No webhook required.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install hermes-baiduapp
20
+ ```
21
+
22
+ ## Configure
23
+
24
+ Add to `~/.hermes/.env`:
25
+
26
+ ```
27
+ BAIDU_APP_KEY=your-app-key
28
+ BAIDU_APP_SECRET=your-app-secret
29
+ # Optional:
30
+ BAIDU_API_BASE=https://claw.baidu.com
31
+ GATEWAY_ALLOW_ALL_USERS=true
32
+ ```
33
+
34
+ Then restart the gateway:
35
+
36
+ ```bash
37
+ hermes gateway run
38
+ ```
39
+
40
+ No changes to `config.yaml` needed — the plugin is auto-discovered via entry_points.
@@ -0,0 +1,31 @@
1
+ # hermes-baiduapp
2
+
3
+ Baidu App platform plugin for [Hermes Agent](https://github.com/NousResearch/hermes-agent).
4
+
5
+ Uses polling mode (`/channel/msg/poll`) to receive messages. No webhook required.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install hermes-baiduapp
11
+ ```
12
+
13
+ ## Configure
14
+
15
+ Add to `~/.hermes/.env`:
16
+
17
+ ```
18
+ BAIDU_APP_KEY=your-app-key
19
+ BAIDU_APP_SECRET=your-app-secret
20
+ # Optional:
21
+ BAIDU_API_BASE=https://claw.baidu.com
22
+ GATEWAY_ALLOW_ALL_USERS=true
23
+ ```
24
+
25
+ Then restart the gateway:
26
+
27
+ ```bash
28
+ hermes gateway run
29
+ ```
30
+
31
+ No changes to `config.yaml` needed — the plugin is auto-discovered via entry_points.
@@ -0,0 +1,3 @@
1
+ from .adapter import register
2
+
3
+ __all__ = ["register"]
@@ -0,0 +1,297 @@
1
+ import asyncio
2
+ import binascii
3
+ import hashlib
4
+ import json
5
+ import logging
6
+ import os
7
+ import time
8
+ from typing import Any, Dict, Optional
9
+ from urllib.parse import urlencode
10
+ from urllib.request import Request, urlopen
11
+
12
+ try:
13
+ import secrets
14
+ except ImportError:
15
+ secrets = None
16
+
17
+ from gateway.config import Platform
18
+ from gateway.platforms.base import BasePlatformAdapter, MessageEvent, MessageType, SendResult
19
+
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ DEFAULT_API_BASE = "https://claw.baidu.com"
24
+ DEFAULT_SESSION_ID = "agent:main:main"
25
+ DEFAULT_POLL_INTERVAL = 3.0 # seconds
26
+ DEFAULT_POLL_TIMEOUT = 10.0 # seconds
27
+ PLUGIN_VERSION = "hermes-baiduapp/1.0.0"
28
+
29
+
30
+ def _generate_nonce():
31
+ if secrets is not None:
32
+ return secrets.token_hex(8)
33
+ return binascii.hexlify(os.urandom(8)).decode("ascii")
34
+
35
+
36
+ def _compute_aksk_token(ak, sk, timestamp, nonce):
37
+ return hashlib.sha1((ak + sk + timestamp + nonce).encode()).hexdigest()
38
+
39
+
40
+ def build_authorization_header(ak, sk, timestamp, nonce):
41
+ token = _compute_aksk_token(ak, sk, timestamp, nonce)
42
+ return "Bearer {token}".format(token=token)
43
+
44
+
45
+ def _coerce_float(value, default):
46
+ try:
47
+ return float(value)
48
+ except (TypeError, ValueError):
49
+ return default
50
+
51
+
52
+ def _coerce_poll_interval(extra):
53
+ env_ms = os.getenv("BAIDU_POLL_INTERVAL_MS")
54
+ if env_ms:
55
+ interval_ms = _coerce_float(env_ms, DEFAULT_POLL_INTERVAL * 1000)
56
+ return max(interval_ms / 1000.0, 0.1)
57
+
58
+ if "poll_interval" in extra:
59
+ return max(_coerce_float(extra.get("poll_interval"), DEFAULT_POLL_INTERVAL), 0.1)
60
+
61
+ if "poll_interval_ms" in extra:
62
+ interval_ms = _coerce_float(extra.get("poll_interval_ms"), DEFAULT_POLL_INTERVAL * 1000)
63
+ return max(interval_ms / 1000.0, 0.1)
64
+
65
+ return DEFAULT_POLL_INTERVAL
66
+
67
+
68
+ def _extract_text_from_list(items):
69
+ texts = []
70
+ if not isinstance(items, list):
71
+ return ""
72
+
73
+ for item in items:
74
+ if not isinstance(item, dict) or item.get("type") != "text":
75
+ continue
76
+ raw_data = item.get("data")
77
+ data = raw_data if isinstance(raw_data, dict) else {}
78
+ raw_text = data.get("text")
79
+ text_data = raw_text if isinstance(raw_text, dict) else {}
80
+ content = text_data.get("content")
81
+ if isinstance(content, str) and content:
82
+ texts.append(content)
83
+
84
+ return "\n".join(texts).strip()
85
+
86
+
87
+ class BaiduAppAdapter(BasePlatformAdapter):
88
+ """Polling adapter for the Baidu App V2 channel API."""
89
+
90
+ def __init__(self, config, **kwargs):
91
+ super().__init__(config=config, platform=Platform("baiduapp"))
92
+
93
+ extra = getattr(config, "extra", {}) or {}
94
+ self.app_key = os.getenv("BAIDU_APP_KEY") or extra.get("app_key") or extra.get("appKey") or ""
95
+ self.app_secret = os.getenv("BAIDU_APP_SECRET") or extra.get("app_secret") or extra.get("appSecret") or ""
96
+ self.api_base = (os.getenv("BAIDU_API_BASE") or extra.get("api_base") or extra.get("apiBase") or DEFAULT_API_BASE).rstrip("/")
97
+ self.poll_interval = _coerce_poll_interval(extra)
98
+ self.request_timeout = _coerce_float(extra.get("request_timeout"), DEFAULT_POLL_TIMEOUT)
99
+ self.session_id = extra.get("session_id") or extra.get("sessionId") or DEFAULT_SESSION_ID
100
+ self._poll_task: Optional[asyncio.Task] = None
101
+
102
+ @property
103
+ def name(self) -> str:
104
+ return "Baidu App"
105
+
106
+ async def connect(self) -> bool:
107
+ if not self.app_key or not self.app_secret:
108
+ logger.error("Baidu App: BAIDU_APP_KEY and BAIDU_APP_SECRET must be configured")
109
+ self._set_fatal_error(
110
+ "config_missing",
111
+ "BAIDU_APP_KEY and BAIDU_APP_SECRET must be configured",
112
+ retryable=False,
113
+ )
114
+ return False
115
+
116
+ if self._poll_task is None or self._poll_task.done():
117
+ self._poll_task = asyncio.create_task(self._poll_loop())
118
+ self._mark_connected()
119
+ logger.info("Baidu App: connected using polling interval %.1fs", self.poll_interval)
120
+ return True
121
+
122
+ async def disconnect(self) -> None:
123
+ self._mark_disconnected()
124
+ if self._poll_task and not self._poll_task.done():
125
+ self._poll_task.cancel()
126
+ try:
127
+ await self._poll_task
128
+ except asyncio.CancelledError:
129
+ pass
130
+ self._poll_task = None
131
+
132
+ async def send(
133
+ self,
134
+ chat_id: str,
135
+ content: str,
136
+ reply_to: Optional[str] = None,
137
+ metadata: Optional[Dict[str, Any]] = None,
138
+ ):
139
+ body = {
140
+ "sessionId": self.session_id,
141
+ "list": [{"type": "text", "data": {"text": {"content": content}}}],
142
+ "version": PLUGIN_VERSION,
143
+ "isActive": True,
144
+ }
145
+ try:
146
+ response = await self._post_json("/channel/msg/callback", body)
147
+ except asyncio.CancelledError:
148
+ raise
149
+ except Exception as exc:
150
+ logger.warning("Baidu App: send failed: %s", exc)
151
+ return SendResult(success=False, error=str(exc), retryable=True)
152
+
153
+ if response.get("code") != 0:
154
+ error = response.get("msg") or response.get("message") or "channel callback failed"
155
+ return SendResult(success=False, error=str(error), raw_response=response)
156
+
157
+ raw_data = response.get("data")
158
+ data = raw_data if isinstance(raw_data, dict) else {}
159
+ message_id = data.get("msgId") or data.get("messageId") or str(int(time.time() * 1000))
160
+ return SendResult(success=True, message_id=str(message_id), raw_response=response)
161
+
162
+ async def send_typing(self, chat_id: str, metadata=None) -> None:
163
+ pass
164
+
165
+ async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
166
+ return {"name": chat_id, "type": "dm", "chat_id": chat_id}
167
+
168
+ async def _poll_loop(self):
169
+ try:
170
+ while True:
171
+ try:
172
+ await self._poll_once()
173
+ except asyncio.CancelledError:
174
+ raise
175
+ except Exception as exc:
176
+ logger.warning("Baidu App: poll cycle failed: %s", exc)
177
+ await asyncio.sleep(self.poll_interval)
178
+ except asyncio.CancelledError:
179
+ raise
180
+
181
+ async def _poll_once(self):
182
+ body = {"sessionId": self.session_id, "num": 5, "version": PLUGIN_VERSION}
183
+ response = await self._post_json("/channel/msg/poll", body)
184
+ if response.get("code") != 0:
185
+ logger.debug(
186
+ "Baidu App: poll returned non-zero code=%s message=%s",
187
+ response.get("code"),
188
+ response.get("message") or response.get("msg"),
189
+ )
190
+
191
+ raw_data = response.get("data")
192
+ data = raw_data if isinstance(raw_data, dict) else {}
193
+ raw_msg_list = data.get("msgList")
194
+ msg_list = raw_msg_list if isinstance(raw_msg_list, list) else []
195
+ for msg in msg_list:
196
+ if isinstance(msg, dict):
197
+ await self._dispatch_poll_message(msg)
198
+
199
+ async def _dispatch_poll_message(self, msg):
200
+ text = _extract_text_from_list(msg.get("list"))
201
+ if not text:
202
+ return
203
+
204
+ message_id = msg.get("msgId") or str(int(time.time() * 1000))
205
+ raw_from = msg.get("from")
206
+ from_info = raw_from if isinstance(raw_from, dict) else {}
207
+ user_id = from_info.get("uid") or "baiduapp-user"
208
+ user_name = from_info.get("name") or str(user_id)
209
+ chat_id = msg.get("sessionId") or self.session_id
210
+
211
+ source = self.build_source(
212
+ chat_id=chat_id,
213
+ chat_name=chat_id,
214
+ chat_type="dm",
215
+ user_id=str(user_id),
216
+ user_name=user_name,
217
+ message_id=str(message_id),
218
+ )
219
+ event = MessageEvent(
220
+ text=text,
221
+ message_type=MessageType.TEXT,
222
+ source=source,
223
+ raw_message=msg,
224
+ message_id=str(message_id),
225
+ )
226
+ await self.handle_message(event)
227
+
228
+ async def _post_json(self, path, body):
229
+ timestamp = str(int(time.time()))
230
+ nonce = _generate_nonce()
231
+ query = urlencode({"ak": self.app_key, "timestamp": timestamp, "nonce": nonce})
232
+ url = "%s%s?%s" % (self.api_base, path, query)
233
+ headers = {
234
+ "Authorization": build_authorization_header(self.app_key, self.app_secret, timestamp, nonce),
235
+ "Content-Type": "application/json",
236
+ }
237
+ payload = json.dumps(body).encode("utf-8")
238
+ loop = asyncio.get_running_loop()
239
+ return await loop.run_in_executor(None, self._post_json_sync, url, headers, payload)
240
+
241
+ def _post_json_sync(self, url, headers, payload):
242
+ request = Request(url, data=payload, headers=headers, method="POST")
243
+ with urlopen(request, timeout=self.request_timeout) as response:
244
+ response_text = response.read().decode("utf-8")
245
+ if not response_text:
246
+ raise ValueError("empty response from Baidu App API")
247
+ try:
248
+ return json.loads(response_text)
249
+ except ValueError as exc:
250
+ raise ValueError("invalid JSON response from Baidu App API") from exc
251
+
252
+
253
+ def check_requirements():
254
+ return bool(os.getenv("BAIDU_APP_KEY") and os.getenv("BAIDU_APP_SECRET"))
255
+
256
+
257
+ def validate_config(config) -> bool:
258
+ extra = getattr(config, "extra", {}) or {}
259
+ app_key = os.getenv("BAIDU_APP_KEY") or extra.get("app_key") or extra.get("appKey")
260
+ app_secret = os.getenv("BAIDU_APP_SECRET") or extra.get("app_secret") or extra.get("appSecret")
261
+ return bool(app_key and app_secret)
262
+
263
+
264
+ def is_connected(config) -> bool:
265
+ return validate_config(config)
266
+
267
+
268
+ def _env_enablement():
269
+ app_key = os.getenv("BAIDU_APP_KEY", "").strip()
270
+ app_secret = os.getenv("BAIDU_APP_SECRET", "").strip()
271
+ if not (app_key and app_secret):
272
+ return None
273
+ return {
274
+ "app_key": app_key,
275
+ "api_base": os.getenv("BAIDU_API_BASE", DEFAULT_API_BASE).strip(),
276
+ }
277
+
278
+
279
+ def register(ctx):
280
+ ctx.register_platform(
281
+ name="baiduapp",
282
+ label="Baidu App",
283
+ adapter_factory=lambda cfg: BaiduAppAdapter(cfg),
284
+ check_fn=check_requirements,
285
+ validate_config=validate_config,
286
+ is_connected=is_connected,
287
+ required_env=["BAIDU_APP_KEY", "BAIDU_APP_SECRET"],
288
+ install_hint="Configure BAIDU_APP_KEY and BAIDU_APP_SECRET in ~/.hermes/.env",
289
+ env_enablement_fn=_env_enablement,
290
+ allowed_users_env="BAIDU_ALLOWED_USERS",
291
+ allow_all_env="BAIDU_ALLOW_ALL_USERS",
292
+ platform_hint=(
293
+ "You are chatting via Baidu App. "
294
+ "Use plain text or markdown. "
295
+ "Keep responses concise."
296
+ ),
297
+ )
@@ -0,0 +1,15 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hermes-baiduapp"
7
+ version = "0.1.0"
8
+ description = "Baidu App platform plugin for Hermes Agent (polling mode, no webhook)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ keywords = ["hermes", "hermes-agent", "baidu", "chatbot", "gateway"]
13
+
14
+ [project.entry-points."hermes.plugins"]
15
+ baiduapp = "hermes_baiduapp"