wechatbridge-cli 1.3.3__tar.gz → 1.3.4__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.
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/PKG-INFO +3 -3
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/README.md +2 -2
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/tests/test_hardening.py +2 -2
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge/__init__.py +1 -1
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge/agy.py +34 -28
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge/config.py +2 -1
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge/grok.py +26 -21
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge/main.py +94 -49
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge/runner_common.py +39 -38
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge_cli.egg-info/PKG-INFO +3 -3
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/LICENSE +0 -0
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/pyproject.toml +0 -0
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/setup.cfg +0 -0
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge/__main__.py +0 -0
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge/ilink.py +0 -0
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge/update_check.py +0 -0
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge_cli.egg-info/SOURCES.txt +0 -0
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge_cli.egg-info/dependency_links.txt +0 -0
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge_cli.egg-info/entry_points.txt +0 -0
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge_cli.egg-info/requires.txt +0 -0
- {wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wechatbridge-cli
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.4
|
|
4
4
|
Summary: Bridge WeChat messages to agy or Grok Build CLIs — text/image/file/voice in, CLI replies and generated files back.
|
|
5
5
|
Author: WeChatBridge contributors
|
|
6
6
|
License: MIT
|
|
@@ -155,7 +155,7 @@ Key variables (all have defaults):
|
|
|
155
155
|
| `AGY_TIMEOUT` | `600` | CLI run timeout in seconds (both backends) |
|
|
156
156
|
| `WECHATBRIDGE_MAX_OUTBOUND_BYTES` | `104857600` | max file size sent back to WeChat (100 MB) |
|
|
157
157
|
| `WECHATBRIDGE_MAX_INBOUND_BYTES` | `20971520` | max inbound image/file after download (20 MB) |
|
|
158
|
-
| `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent
|
|
158
|
+
| `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent process slots; same user serial (queue does not hold a slot); extras get a busy reply |
|
|
159
159
|
| `WECHATBRIDGE_CONFIRM_TOKEN` | `y` | reply this token to approve a gated dangerous prompt |
|
|
160
160
|
| `WECHATBRIDGE_ENABLE_MCP` | `true` | enable the `/mcp` help text command |
|
|
161
161
|
| `WECHATBRIDGE_ENABLE_SUBAGENT` | `true` | enable the `/agent` prompt-rewrite command |
|
|
@@ -262,7 +262,7 @@ Other `/…` commands are either rejected (e.g. `/exit`), reported as unsupporte
|
|
|
262
262
|
- **Danger gate is keyword-based**, not full intent understanding. Defaults target concrete patterns (`rm -rf /`, pipe-to-shell, `mkfs`, `format c:`, a few heavy Chinese phrases, …). Everyday wording like bare “delete” is **not** gated. Override list via `WECHATBRIDGE_CONFIRM_KEYWORDS`; approve with `WECHATBRIDGE_CONFIRM_TOKEN` (default `y`), TTL `WECHATBRIDGE_PENDING_TTL`.
|
|
263
263
|
- **Inbound media** is size-capped (default 20 MB), streamed, and CDN hosts are allowlisted. Missing `aes_key` returns a clear error.
|
|
264
264
|
- **Outbound artifacts** only leave the allowed per-user tree (agy: session scratch; grok: under session dir), after `realpath` checks, and only if under `WECHATBRIDGE_MAX_OUTBOUND_BYTES`.
|
|
265
|
-
- **Concurrency:** global cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4)
|
|
265
|
+
- **Concurrency:** global process-slot cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4). Same user is serialized and does **not** hold a global slot while waiting on their previous message; different users can run in parallel up to the cap.
|
|
266
266
|
- **Long replies** are split into chunks (`WECHATBRIDGE_MESSAGE_CHUNK`, default 2000 characters).
|
|
267
267
|
- **Data layout:** instance data under `~/.local/share/wechatbridge/<instance>/` (override with env). Runtime dirs prefer `0700`; token/QR files prefer `0600` (Unix; Windows relies on NTFS ACLs).
|
|
268
268
|
- **Retention:** session temps vs dialogue history use separate TTLs (`WECHATBRIDGE_SESSION_RETENTION_DAYS`, `WECHATBRIDGE_HISTORY_RETENTION_DAYS`). Prefs/auth are kept.
|
|
@@ -124,7 +124,7 @@ Key variables (all have defaults):
|
|
|
124
124
|
| `AGY_TIMEOUT` | `600` | CLI run timeout in seconds (both backends) |
|
|
125
125
|
| `WECHATBRIDGE_MAX_OUTBOUND_BYTES` | `104857600` | max file size sent back to WeChat (100 MB) |
|
|
126
126
|
| `WECHATBRIDGE_MAX_INBOUND_BYTES` | `20971520` | max inbound image/file after download (20 MB) |
|
|
127
|
-
| `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent
|
|
127
|
+
| `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent process slots; same user serial (queue does not hold a slot); extras get a busy reply |
|
|
128
128
|
| `WECHATBRIDGE_CONFIRM_TOKEN` | `y` | reply this token to approve a gated dangerous prompt |
|
|
129
129
|
| `WECHATBRIDGE_ENABLE_MCP` | `true` | enable the `/mcp` help text command |
|
|
130
130
|
| `WECHATBRIDGE_ENABLE_SUBAGENT` | `true` | enable the `/agent` prompt-rewrite command |
|
|
@@ -231,7 +231,7 @@ Other `/…` commands are either rejected (e.g. `/exit`), reported as unsupporte
|
|
|
231
231
|
- **Danger gate is keyword-based**, not full intent understanding. Defaults target concrete patterns (`rm -rf /`, pipe-to-shell, `mkfs`, `format c:`, a few heavy Chinese phrases, …). Everyday wording like bare “delete” is **not** gated. Override list via `WECHATBRIDGE_CONFIRM_KEYWORDS`; approve with `WECHATBRIDGE_CONFIRM_TOKEN` (default `y`), TTL `WECHATBRIDGE_PENDING_TTL`.
|
|
232
232
|
- **Inbound media** is size-capped (default 20 MB), streamed, and CDN hosts are allowlisted. Missing `aes_key` returns a clear error.
|
|
233
233
|
- **Outbound artifacts** only leave the allowed per-user tree (agy: session scratch; grok: under session dir), after `realpath` checks, and only if under `WECHATBRIDGE_MAX_OUTBOUND_BYTES`.
|
|
234
|
-
- **Concurrency:** global cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4)
|
|
234
|
+
- **Concurrency:** global process-slot cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4). Same user is serialized and does **not** hold a global slot while waiting on their previous message; different users can run in parallel up to the cap.
|
|
235
235
|
- **Long replies** are split into chunks (`WECHATBRIDGE_MESSAGE_CHUNK`, default 2000 characters).
|
|
236
236
|
- **Data layout:** instance data under `~/.local/share/wechatbridge/<instance>/` (override with env). Runtime dirs prefer `0700`; token/QR files prefer `0600` (Unix; Windows relies on NTFS ACLs).
|
|
237
237
|
- **Retention:** session temps vs dialogue history use separate TTLs (`WECHATBRIDGE_SESSION_RETENTION_DAYS`, `WECHATBRIDGE_HISTORY_RETENTION_DAYS`). Prefs/auth are kept.
|
|
@@ -145,7 +145,7 @@ class TestOversizedArtifactNotice(unittest.TestCase):
|
|
|
145
145
|
self.assertNotIn("/root/", text)
|
|
146
146
|
self.assertNotIn("sessions/", text)
|
|
147
147
|
self.assertIn("report.pdf", text)
|
|
148
|
-
self.assertIn("
|
|
148
|
+
self.assertIn("无法发到微信", text)
|
|
149
149
|
|
|
150
150
|
def test_send_artifacts_back_uses_safe_notice(self):
|
|
151
151
|
"""Call the real async helper; assert user text has no server path."""
|
|
@@ -187,7 +187,7 @@ class TestOversizedArtifactNotice(unittest.TestCase):
|
|
|
187
187
|
self.assertNotIn(big_path, text)
|
|
188
188
|
self.assertNotIn(td, text)
|
|
189
189
|
self.assertIn("huge.bin", text)
|
|
190
|
-
self.assertIn("
|
|
190
|
+
self.assertIn("无法发到微信", text)
|
|
191
191
|
|
|
192
192
|
asyncio.run(_run())
|
|
193
193
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""WeChatBridge — bridge WeChat messages to agy or Grok Build CLIs."""
|
|
2
|
-
__version__ = "1.3.
|
|
2
|
+
__version__ = "1.3.4"
|
|
@@ -117,7 +117,7 @@ async def run_agy(prompt: str, user_id: str, timeout: int = None) -> tuple[str,
|
|
|
117
117
|
logger.warning("Prompt too large for argv from user %s", user_id)
|
|
118
118
|
return format_error(
|
|
119
119
|
"消息过长",
|
|
120
|
-
f"
|
|
120
|
+
f"这条消息太长了(超过 {_MAX_ARG_BYTES // 1024}KB),请精简或分段发送。",
|
|
121
121
|
), []
|
|
122
122
|
|
|
123
123
|
t0 = time.time()
|
|
@@ -252,8 +252,8 @@ async def run_agy(prompt: str, user_id: str, timeout: int = None) -> tuple[str,
|
|
|
252
252
|
)
|
|
253
253
|
await terminate_process(retry_process, graceful=True)
|
|
254
254
|
return format_error(
|
|
255
|
-
"
|
|
256
|
-
"
|
|
255
|
+
"模型响应超时",
|
|
256
|
+
"模型响应超时,自动重试仍超时。请稍后重试或简化指令。",
|
|
257
257
|
), []
|
|
258
258
|
except (asyncio.CancelledError, Exception):
|
|
259
259
|
await terminate_process(retry_process, graceful=False)
|
|
@@ -269,12 +269,12 @@ async def run_agy(prompt: str, user_id: str, timeout: int = None) -> tuple[str,
|
|
|
269
269
|
mark_initialized(session_dir)
|
|
270
270
|
return r_display, r_artifacts
|
|
271
271
|
return format_error(
|
|
272
|
-
"
|
|
273
|
-
"
|
|
272
|
+
"模型响应超时",
|
|
273
|
+
"模型响应超时,自动重试仍失败。请稍后重试或简化指令。",
|
|
274
274
|
), []
|
|
275
275
|
return format_cli_error(stderr_text, backend="agy"), []
|
|
276
276
|
# Non-zero exit: never treat raw stdout as a normal success reply
|
|
277
|
-
raw = stderr_text or stdout_text or display or "
|
|
277
|
+
raw = stderr_text or stdout_text or display or "process exited abnormally"
|
|
278
278
|
return format_cli_error(raw, backend="agy"), []
|
|
279
279
|
|
|
280
280
|
# Success path only
|
|
@@ -305,7 +305,10 @@ async def run_agy(prompt: str, user_id: str, timeout: int = None) -> tuple[str,
|
|
|
305
305
|
except Exception as e:
|
|
306
306
|
logger.exception("Unexpected error running agy: %s", e)
|
|
307
307
|
await terminate_process(process, graceful=False)
|
|
308
|
-
return format_error(
|
|
308
|
+
return format_error(
|
|
309
|
+
"执行出错",
|
|
310
|
+
"这次没处理好,请稍后再试。若一直失败,请联系管理员。",
|
|
311
|
+
), []
|
|
309
312
|
|
|
310
313
|
|
|
311
314
|
# ---------------------------------------------------------------------------
|
|
@@ -352,16 +355,19 @@ async def _run_agy_subcommand(subcmd_args: list, user_id: str) -> str:
|
|
|
352
355
|
return clean_output(stdout_text) or EMPTY_REPLY
|
|
353
356
|
|
|
354
357
|
except asyncio.TimeoutError:
|
|
355
|
-
#
|
|
358
|
+
# 超时必须回收子进程,否则挂死的查询进程成为孤儿
|
|
356
359
|
await terminate_process(process, graceful=True)
|
|
357
|
-
return format_error("
|
|
360
|
+
return format_error("查询超时", "查询超时,请稍后再试。")
|
|
358
361
|
except asyncio.CancelledError:
|
|
359
362
|
await terminate_process(process, graceful=False)
|
|
360
363
|
raise
|
|
361
364
|
except Exception as e:
|
|
362
365
|
logger.exception("Subcommand error: %s", e)
|
|
363
366
|
await terminate_process(process, graceful=False)
|
|
364
|
-
return format_error(
|
|
367
|
+
return format_error(
|
|
368
|
+
"执行出错",
|
|
369
|
+
"这次没处理好,请稍后再试。若一直失败,请联系管理员。",
|
|
370
|
+
)
|
|
365
371
|
|
|
366
372
|
|
|
367
373
|
def _cmd_help() -> str:
|
|
@@ -372,28 +378,28 @@ def _cmd_help() -> str:
|
|
|
372
378
|
"**模型控制**",
|
|
373
379
|
"- `/model <名称>` — 切换模型(用 `/models` 查看可用列表)",
|
|
374
380
|
"- `/models` — 查看可用模型列表",
|
|
375
|
-
"- `/backend <agy|grok>` —
|
|
381
|
+
"- `/backend <agy|grok>` — 切换助手引擎",
|
|
376
382
|
"",
|
|
377
383
|
"**对话控制**",
|
|
378
384
|
"- `/clear` 或 `/new` — 重置对话(开始新会话)",
|
|
379
|
-
"- `/fast` —
|
|
380
|
-
"- `/planning` —
|
|
385
|
+
"- `/fast` — 开启**快速模式**(回答更快,思考更少)",
|
|
386
|
+
"- `/planning` — 开启**规划模式**(先想清楚再动手)",
|
|
381
387
|
"",
|
|
382
388
|
"**工具**",
|
|
383
389
|
"- `/add-dir <路径>` — 添加工作目录",
|
|
384
|
-
"- `/agents` —
|
|
390
|
+
"- `/agents` — 查看可用助手",
|
|
385
391
|
"",
|
|
386
|
-
"
|
|
387
|
-
"- `/mcp` —
|
|
388
|
-
"- `/agent <名称> <任务>` —
|
|
392
|
+
"**扩展工具 & 子助手**",
|
|
393
|
+
"- `/mcp` — 扩展工具使用说明",
|
|
394
|
+
"- `/agent <名称> <任务>` — 调用子助手执行任务",
|
|
389
395
|
"",
|
|
390
396
|
"**人格**",
|
|
391
|
-
"- `/persona <内容>` —
|
|
397
|
+
"- `/persona <内容>` — 设置你专属的人格文档(另有 show / clear / reset)",
|
|
392
398
|
"",
|
|
393
399
|
"**其他**",
|
|
394
400
|
"- `/help` — 显示本帮助",
|
|
395
401
|
"",
|
|
396
|
-
"提示:其他 `/` 指令(如 `/goal`、`/grill-me`、`/schedule`
|
|
402
|
+
"提示:其他 `/` 指令(如 `/goal`、`/grill-me`、`/schedule` 等)会直接交给助手处理。",
|
|
397
403
|
]
|
|
398
404
|
return "\n".join(lines)
|
|
399
405
|
|
|
@@ -496,13 +502,13 @@ def _cmd_clear(user_id: str) -> str:
|
|
|
496
502
|
def _cmd_fast(user_id: str) -> str:
|
|
497
503
|
"""Handle /fast: set effort=low (scoped to current backend)."""
|
|
498
504
|
update_active_prefs(user_id, effort="low")
|
|
499
|
-
return "✅
|
|
505
|
+
return "✅ **已开启快速模式** ✅"
|
|
500
506
|
|
|
501
507
|
|
|
502
508
|
def _cmd_planning(user_id: str) -> str:
|
|
503
509
|
"""Handle /planning: set mode=plan (scoped to current backend)."""
|
|
504
510
|
update_active_prefs(user_id, mode="plan")
|
|
505
|
-
return "✅
|
|
511
|
+
return "✅ **已开启规划模式** ✅"
|
|
506
512
|
|
|
507
513
|
|
|
508
514
|
def _cmd_add_dir(args: str, user_id: str) -> str:
|
|
@@ -627,8 +633,9 @@ async def handle_slash_command(text: str, user_id: str) -> str | None:
|
|
|
627
633
|
|
|
628
634
|
if cmd == "/agents":
|
|
629
635
|
output = await _run_agy_subcommand(["agents"], user_id)
|
|
630
|
-
|
|
631
|
-
|
|
636
|
+
# Match CLI empty output; rewrite user-facing wording only
|
|
637
|
+
if not output or output.strip() in ("Available agents:", "Available agents"):
|
|
638
|
+
output = "**可用助手**\n\n(当前没有自定义助手。)"
|
|
632
639
|
return output
|
|
633
640
|
|
|
634
641
|
if cmd == "/models":
|
|
@@ -642,12 +649,11 @@ async def handle_slash_command(text: str, user_id: str) -> str | None:
|
|
|
642
649
|
if not config.enable_mcp:
|
|
643
650
|
return "ℹ️ **该功能已禁用** ℹ️"
|
|
644
651
|
return (
|
|
645
|
-
"ℹ️
|
|
646
|
-
"
|
|
647
|
-
"使用方法:用自然语言描述调用,格式为:\n"
|
|
648
|
-
"> 用 call_mcp_tool 调用 `<工具名>`,参数 `<json>`\n\n"
|
|
652
|
+
"ℹ️ **扩展工具说明** ℹ️\n\n"
|
|
653
|
+
"可以直接用自然语言让助手调用已配置的扩展工具(如代码检索等)。\n\n"
|
|
649
654
|
"示例:\n"
|
|
650
|
-
"> 用 codegraph 的 search
|
|
655
|
+
"> 用 codegraph 的 search 搜一下 ctxmode\n"
|
|
656
|
+
"> 帮我查一下这个项目里 xxx 怎么实现的"
|
|
651
657
|
)
|
|
652
658
|
|
|
653
659
|
# /agent 已上移到 main.py 统一处理(必须经过危险确认门,不能再绕过)
|
|
@@ -191,7 +191,8 @@ class AppConfig:
|
|
|
191
191
|
# Maximum inbound image/file size after download (bytes) — default 20 MB
|
|
192
192
|
max_inbound_file_bytes: int = _env_int("WECHATBRIDGE_MAX_INBOUND_BYTES", 20 * 1024 * 1024)
|
|
193
193
|
|
|
194
|
-
#
|
|
194
|
+
# Global concurrent process_message slots. Same user serializes first and
|
|
195
|
+
# does not hold a slot while waiting on their previous message; extras get a busy reply.
|
|
195
196
|
max_concurrent_tasks: int = _env_int("WECHATBRIDGE_MAX_CONCURRENT", 4)
|
|
196
197
|
|
|
197
198
|
# WeChat text chunk size (characters) when splitting long replies
|
|
@@ -372,7 +372,7 @@ async def run_grok(prompt: str, user_id: str, timeout: int = None) -> tuple:
|
|
|
372
372
|
logger.warning("Prompt too large for argv from user %s", user_id)
|
|
373
373
|
return format_error(
|
|
374
374
|
"消息过长",
|
|
375
|
-
f"
|
|
375
|
+
f"这条消息太长了(超过 {_MAX_ARG_BYTES // 1024}KB),请精简或分段发送。",
|
|
376
376
|
), []
|
|
377
377
|
|
|
378
378
|
t0 = time.time()
|
|
@@ -432,7 +432,7 @@ async def run_grok(prompt: str, user_id: str, timeout: int = None) -> tuple:
|
|
|
432
432
|
)
|
|
433
433
|
artifacts = []
|
|
434
434
|
if not (isinstance(display, str) and display.startswith("❌")):
|
|
435
|
-
raw_err = stderr_text or ("" if display == EMPTY_REPLY else display) or "
|
|
435
|
+
raw_err = stderr_text or ("" if display == EMPTY_REPLY else display) or "process exited abnormally"
|
|
436
436
|
display = format_cli_error(raw_err, backend="grok")
|
|
437
437
|
|
|
438
438
|
# Only mark session initialized on a real successful reply
|
|
@@ -459,7 +459,10 @@ async def run_grok(prompt: str, user_id: str, timeout: int = None) -> tuple:
|
|
|
459
459
|
except Exception as e:
|
|
460
460
|
logger.exception("Unexpected error running grok: %s", e)
|
|
461
461
|
await terminate_process(process, graceful=False)
|
|
462
|
-
return format_error(
|
|
462
|
+
return format_error(
|
|
463
|
+
"执行出错",
|
|
464
|
+
"这次没处理好,请稍后再试。若一直失败,请联系管理员。",
|
|
465
|
+
), []
|
|
463
466
|
|
|
464
467
|
|
|
465
468
|
async def _run_grok_subcommand(subcmd_args: list, user_id: str) -> str:
|
|
@@ -502,16 +505,19 @@ async def _run_grok_subcommand(subcmd_args: list, user_id: str) -> str:
|
|
|
502
505
|
return clean_output(stdout_text) or EMPTY_REPLY
|
|
503
506
|
|
|
504
507
|
except asyncio.TimeoutError:
|
|
505
|
-
#
|
|
508
|
+
# 超时必须回收子进程,否则挂死的查询进程成为孤儿
|
|
506
509
|
await terminate_process(process, graceful=True)
|
|
507
|
-
return format_error("
|
|
510
|
+
return format_error("查询超时", "查询超时,请稍后再试。")
|
|
508
511
|
except asyncio.CancelledError:
|
|
509
512
|
await terminate_process(process, graceful=False)
|
|
510
513
|
raise
|
|
511
514
|
except Exception as e:
|
|
512
515
|
logger.exception("Subcommand error: %s", e)
|
|
513
516
|
await terminate_process(process, graceful=False)
|
|
514
|
-
return format_error(
|
|
517
|
+
return format_error(
|
|
518
|
+
"执行出错",
|
|
519
|
+
"这次没处理好,请稍后再试。若一直失败,请联系管理员。",
|
|
520
|
+
)
|
|
515
521
|
|
|
516
522
|
|
|
517
523
|
# ---------------------------------------------------------------------------
|
|
@@ -582,24 +588,24 @@ def _cmd_help() -> str:
|
|
|
582
588
|
"**模型控制**",
|
|
583
589
|
"- `/model <名称>` — 切换模型(用 `/models` 查看可用列表)",
|
|
584
590
|
"- `/models` — 查看可用模型列表",
|
|
585
|
-
"- `/backend <agy|grok>` —
|
|
591
|
+
"- `/backend <agy|grok>` — 切换助手引擎",
|
|
586
592
|
"",
|
|
587
593
|
"**对话控制**",
|
|
588
594
|
"- `/clear` 或 `/new` — 重置对话(开始新会话)",
|
|
589
|
-
"- `/fast` —
|
|
590
|
-
"- `/planning` —
|
|
595
|
+
"- `/fast` — 开启**快速模式**(回答更快,思考更少)",
|
|
596
|
+
"- `/planning` — 开启**规划模式**(先想清楚再动手)",
|
|
591
597
|
"",
|
|
592
598
|
"**工具**",
|
|
593
|
-
"- `/add-dir <路径>` —
|
|
594
|
-
"- `/agents` —
|
|
599
|
+
"- `/add-dir <路径>` — 添加工作目录(当前 grok 引擎暂不支持,仅记录)",
|
|
600
|
+
"- `/agents` — 查看可用助手",
|
|
595
601
|
"",
|
|
596
602
|
"**人格**",
|
|
597
|
-
"- `/persona <内容>` —
|
|
603
|
+
"- `/persona <内容>` — 设置你专属的人格文档(另有 show / clear / reset)",
|
|
598
604
|
"",
|
|
599
605
|
"**其他**",
|
|
600
606
|
"- `/help` — 显示本帮助",
|
|
601
607
|
"",
|
|
602
|
-
"提示:其他 `/`
|
|
608
|
+
"提示:其他 `/` 指令会直接交给助手处理。",
|
|
603
609
|
]
|
|
604
610
|
return "\n".join(lines)
|
|
605
611
|
|
|
@@ -646,11 +652,11 @@ async def handle_grok_slash_command(text: str, user_id: str) -> str | None:
|
|
|
646
652
|
|
|
647
653
|
if cmd == "/fast":
|
|
648
654
|
update_active_prefs(user_id, effort="low")
|
|
649
|
-
return "✅
|
|
655
|
+
return "✅ **已开启快速模式** ✅"
|
|
650
656
|
|
|
651
657
|
if cmd == "/planning":
|
|
652
658
|
update_active_prefs(user_id, mode="plan")
|
|
653
|
-
return "✅
|
|
659
|
+
return "✅ **已开启规划模式** ✅"
|
|
654
660
|
|
|
655
661
|
if cmd == "/model":
|
|
656
662
|
return await _cmd_model(args, user_id)
|
|
@@ -671,7 +677,7 @@ async def handle_grok_slash_command(text: str, user_id: str) -> str | None:
|
|
|
671
677
|
save_prefs(user_id, prefs)
|
|
672
678
|
return (
|
|
673
679
|
f"✅ **已记录工作目录** ✅\n\n```\n{resolved}\n```\n\n"
|
|
674
|
-
"ℹ️
|
|
680
|
+
"ℹ️ 当前引擎暂不支持额外工作目录,路径已记下备用。"
|
|
675
681
|
)
|
|
676
682
|
|
|
677
683
|
if cmd == "/agents":
|
|
@@ -689,12 +695,11 @@ async def handle_grok_slash_command(text: str, user_id: str) -> str | None:
|
|
|
689
695
|
if not config.enable_mcp:
|
|
690
696
|
return "ℹ️ **该功能已禁用** ℹ️"
|
|
691
697
|
return (
|
|
692
|
-
"ℹ️
|
|
693
|
-
"
|
|
694
|
-
"使用方法:用自然语言描述调用,格式为:\n"
|
|
695
|
-
"> 用 `<工具名>` 调用,参数 `<json>`\n\n"
|
|
698
|
+
"ℹ️ **扩展工具说明** ℹ️\n\n"
|
|
699
|
+
"可以直接用自然语言让助手调用已配置的扩展工具。\n\n"
|
|
696
700
|
"示例:\n"
|
|
697
|
-
"> 用 codegraph 的 search
|
|
701
|
+
"> 用 codegraph 的 search 搜一下 ctxmode\n"
|
|
702
|
+
"> 帮我查一下这个项目里 xxx 怎么实现的"
|
|
698
703
|
)
|
|
699
704
|
|
|
700
705
|
# /agent 已上移到 main.py 统一处理(必须经过危险确认门,不能再绕过)
|
|
@@ -158,7 +158,7 @@ async def _handle_slash(client: ILinkClient, text: str, user_id: str, context_to
|
|
|
158
158
|
if cmd == "/version":
|
|
159
159
|
return (
|
|
160
160
|
f"📦 **版本信息** 📦\n\n当前版本: `{__version__}`\n"
|
|
161
|
-
f"实例: `{config.instance}`
|
|
161
|
+
f"实例: `{config.instance}` 引擎: `{_get_backend(user_id)}`"
|
|
162
162
|
) + format_update_hint()
|
|
163
163
|
|
|
164
164
|
backend = _get_backend(user_id)
|
|
@@ -183,12 +183,12 @@ def _cmd_backend(args: str, user_id: str) -> str:
|
|
|
183
183
|
current = prefs.get("backend", config.backend)
|
|
184
184
|
model_label = format_model_label(prefs.get("model", ""))
|
|
185
185
|
return (
|
|
186
|
-
f"📋
|
|
186
|
+
f"📋 **当前助手引擎** 📋\n\n`{current}`\n"
|
|
187
187
|
f"模型: `{model_label}`\n\n"
|
|
188
188
|
"用法: `/backend agy` 或 `/backend grok`"
|
|
189
189
|
)
|
|
190
190
|
if name not in ("agy", "grok"):
|
|
191
|
-
return "❌
|
|
191
|
+
return "❌ **未知引擎** ❌\n\n支持: `agy` / `grok`\n\n`/backend agy` 或 `/backend grok`"
|
|
192
192
|
prefs = load_prefs(user_id)
|
|
193
193
|
old, new = switch_backend_prefs(prefs, name)
|
|
194
194
|
save_prefs(user_id, prefs)
|
|
@@ -200,19 +200,19 @@ def _cmd_backend(args: str, user_id: str) -> str:
|
|
|
200
200
|
if os.path.exists(flag):
|
|
201
201
|
os.remove(flag)
|
|
202
202
|
return (
|
|
203
|
-
f"✅
|
|
203
|
+
f"✅ **助手引擎已切换** ✅\n\n"
|
|
204
204
|
f"`{old}` → `{new}`\n"
|
|
205
205
|
f"模型: `{model_label}`\n\n"
|
|
206
|
-
"⚠️
|
|
206
|
+
"⚠️ 对话已重置,新引擎将开始新会话。"
|
|
207
207
|
)
|
|
208
208
|
return (
|
|
209
|
-
f"📋
|
|
209
|
+
f"📋 **当前助手引擎** 📋\n\n`{name}`(未变化)\n"
|
|
210
210
|
f"模型: `{model_label}`"
|
|
211
211
|
)
|
|
212
212
|
|
|
213
213
|
|
|
214
214
|
async def _cmd_agent(client: ILinkClient, args: str, user_id: str, context_token: str):
|
|
215
|
-
"""Handle /agent <名称> <任务> —
|
|
215
|
+
"""Handle /agent <名称> <任务> — 调用子助手执行任务。
|
|
216
216
|
|
|
217
217
|
必须经过 gate_and_run 的危险确认门(历史上后端各自实现时绕过了该检查)。
|
|
218
218
|
"""
|
|
@@ -223,9 +223,18 @@ async def _cmd_agent(client: ILinkClient, args: str, user_id: str, context_token
|
|
|
223
223
|
agent_parts = args.split(maxsplit=1)
|
|
224
224
|
agent_name = agent_parts[0]
|
|
225
225
|
agent_task = agent_parts[1] if len(agent_parts) > 1 else ""
|
|
226
|
+
# Execution path still uses the invoke_subagent wording for the backend;
|
|
227
|
+
# confirmation UI shows the user's original agent/task only.
|
|
226
228
|
crafted = f"请用 invoke_subagent 调用 agent {agent_name} 执行任务:{agent_task}"
|
|
229
|
+
display = (
|
|
230
|
+
f"调用助手「{agent_name}」执行:{agent_task}"
|
|
231
|
+
if agent_task
|
|
232
|
+
else f"调用助手「{agent_name}」"
|
|
233
|
+
)
|
|
227
234
|
logger.info("Agent subcmd: user=%s agent=%s task=%.100s", user_id, agent_name, agent_task)
|
|
228
|
-
result = await gate_and_run(
|
|
235
|
+
result = await gate_and_run(
|
|
236
|
+
client, user_id, context_token, crafted, display_prompt=display,
|
|
237
|
+
)
|
|
229
238
|
if result is None:
|
|
230
239
|
return _HANDLED # 已进入危险确认流程
|
|
231
240
|
return result # (reply, artifacts)
|
|
@@ -326,10 +335,20 @@ async def login_flow(client: ILinkClient) -> bool:
|
|
|
326
335
|
return False
|
|
327
336
|
|
|
328
337
|
|
|
329
|
-
async def gate_and_run(
|
|
338
|
+
async def gate_and_run(
|
|
339
|
+
client,
|
|
340
|
+
from_user,
|
|
341
|
+
context_token,
|
|
342
|
+
prompt,
|
|
343
|
+
*,
|
|
344
|
+
display_prompt: str | None = None,
|
|
345
|
+
) -> tuple[str, list] | None:
|
|
330
346
|
"""Check prompt with is_dangerous; if dangerous, ask for confirmation.
|
|
331
347
|
|
|
332
348
|
Returns (reply, artifacts) on safe prompt, None if confirmation asked.
|
|
349
|
+
|
|
350
|
+
``display_prompt`` (optional) is what the user sees in the confirm bubble;
|
|
351
|
+
the stored/executed prompt remains ``prompt`` (execution path unchanged).
|
|
333
352
|
"""
|
|
334
353
|
if is_dangerous(prompt):
|
|
335
354
|
expire_at = time.time() + config.pending_confirm_ttl
|
|
@@ -338,10 +357,11 @@ async def gate_and_run(client, from_user, context_token, prompt) -> tuple[str, l
|
|
|
338
357
|
"expire_at": expire_at,
|
|
339
358
|
"context_token": context_token,
|
|
340
359
|
}
|
|
360
|
+
shown = display_prompt if display_prompt is not None else prompt
|
|
341
361
|
await client.send_message(
|
|
342
362
|
to_user_id=from_user,
|
|
343
363
|
text=(
|
|
344
|
-
f"⚠️ **危险操作确认** ⚠️\n\n```\n{
|
|
364
|
+
f"⚠️ **危险操作确认** ⚠️\n\n```\n{shown}\n```\n\n"
|
|
345
365
|
f"- 回复 **{config.confirm_token}** → 执行\n"
|
|
346
366
|
f"- 回复其他 → 取消"
|
|
347
367
|
),
|
|
@@ -537,7 +557,7 @@ async def process_message(client: ILinkClient, msg: dict) -> None:
|
|
|
537
557
|
reply = ""
|
|
538
558
|
if image_media:
|
|
539
559
|
if not image_media.get("aes_key"):
|
|
540
|
-
reply = format_error("
|
|
560
|
+
reply = format_error("图片处理失败", "图片信息不完整,请重新发。")
|
|
541
561
|
logger.warning("图片缺少 aes_key from=%s", from_user)
|
|
542
562
|
else:
|
|
543
563
|
try:
|
|
@@ -572,15 +592,21 @@ async def process_message(client: ILinkClient, msg: dict) -> None:
|
|
|
572
592
|
reply, artifacts = result
|
|
573
593
|
|
|
574
594
|
except Exception as e:
|
|
595
|
+
# ilink 细节(aes_key / CDN / 字节数等)只写日志,不直出给用户
|
|
575
596
|
logger.exception("图片下载/解密失败: %s", e)
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
597
|
+
err_s = str(e)
|
|
598
|
+
if isinstance(e, ValueError) and "过大" in err_s:
|
|
599
|
+
reply = format_error(
|
|
600
|
+
"文件太大了",
|
|
601
|
+
"图片太大,发不进来。请压缩后再发,或换成更小的图。",
|
|
602
|
+
)
|
|
603
|
+
else:
|
|
604
|
+
reply = format_error("图片处理失败", "图片没处理好,请重新发。")
|
|
579
605
|
|
|
580
606
|
# ---- Case 1.5: Message contains a file (non-image) ----
|
|
581
607
|
elif file_media:
|
|
582
608
|
if not file_media.get("aes_key"):
|
|
583
|
-
reply = format_error("
|
|
609
|
+
reply = format_error("文件处理失败", "文件信息不完整,请重新发。")
|
|
584
610
|
logger.warning("文件缺少 aes_key from=%s", from_user)
|
|
585
611
|
else:
|
|
586
612
|
try:
|
|
@@ -612,10 +638,16 @@ async def process_message(client: ILinkClient, msg: dict) -> None:
|
|
|
612
638
|
reply, artifacts = result
|
|
613
639
|
|
|
614
640
|
except Exception as e:
|
|
641
|
+
# ilink 细节(aes_key / CDN / 字节数等)只写日志,不直出给用户
|
|
615
642
|
logger.exception("文件下载/解密失败: %s", e)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
643
|
+
err_s = str(e)
|
|
644
|
+
if isinstance(e, ValueError) and "过大" in err_s:
|
|
645
|
+
reply = format_error(
|
|
646
|
+
"文件太大了",
|
|
647
|
+
"文件太大,发不进来。请压缩后再发,或换成更小的文件。",
|
|
648
|
+
)
|
|
649
|
+
else:
|
|
650
|
+
reply = format_error("文件处理失败", "文件没处理好,请重新发。")
|
|
619
651
|
|
|
620
652
|
# ---- Case 1.6: Voice message (text transcription passthrough) ----
|
|
621
653
|
elif has_voice:
|
|
@@ -627,7 +659,7 @@ async def process_message(client: ILinkClient, msg: dict) -> None:
|
|
|
627
659
|
reply, artifacts = result
|
|
628
660
|
else:
|
|
629
661
|
# WeChat failed to transcribe the voice → ask user to type.
|
|
630
|
-
reply = "🤔
|
|
662
|
+
reply = "🤔 **没听清语音** 🤔\n\n请改成打字发。"
|
|
631
663
|
logger.info("语音未识别出文字 from=%s", from_user)
|
|
632
664
|
|
|
633
665
|
# ---- Case 2: Text-only message (original logic) ----
|
|
@@ -849,43 +881,56 @@ def _prune_user_locks() -> None:
|
|
|
849
881
|
|
|
850
882
|
|
|
851
883
|
async def _safe_process_message(client: ILinkClient, msg: dict) -> None:
|
|
852
|
-
"""Run process_message
|
|
884
|
+
"""Run process_message with per-user serial queue then global concurrency.
|
|
885
|
+
|
|
886
|
+
Order matters:
|
|
887
|
+
1) Take per-user lock first — same-user messages queue here and do NOT
|
|
888
|
+
hold a global slot while waiting for the previous one.
|
|
889
|
+
2) Acquire global semaphore only immediately before process_message.
|
|
890
|
+
3) Fail-fast (short timeout) if global slots are full; release only if held.
|
|
853
891
|
|
|
854
|
-
|
|
855
|
-
|
|
892
|
+
Thus one user can occupy at most one global slot at a time; cross-user
|
|
893
|
+
parallelism is still capped by WECHATBRIDGE_MAX_CONCURRENT.
|
|
856
894
|
"""
|
|
857
895
|
from_user = msg.get("from_user_id", "")
|
|
858
896
|
context_token = msg.get("context_token", "")
|
|
859
897
|
sem = _get_global_sem()
|
|
898
|
+
sem_held = False
|
|
860
899
|
|
|
861
|
-
#
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
except asyncio.TimeoutError:
|
|
865
|
-
logger.warning("并发已满,拒绝处理 from=%s", from_user)
|
|
866
|
-
if context_token and from_user:
|
|
867
|
-
try:
|
|
868
|
-
await client.send_message(
|
|
869
|
-
to_user_id=from_user,
|
|
870
|
-
text="⏳ **系统繁忙** ⏳\n\n当前处理人数已满,请稍后再试。",
|
|
871
|
-
context_token=context_token,
|
|
872
|
-
baseurl=client.state.baseurl,
|
|
873
|
-
bot_token=client.state.bot_token,
|
|
874
|
-
)
|
|
875
|
-
except Exception as e:
|
|
876
|
-
logger.warning("发送繁忙提示失败: %s", e)
|
|
877
|
-
return
|
|
900
|
+
# Serialize same user first (empty from_user still gets a dedicated lock key)
|
|
901
|
+
if from_user not in user_locks:
|
|
902
|
+
user_locks[from_user] = asyncio.Lock()
|
|
878
903
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
904
|
+
async with user_locks[from_user]:
|
|
905
|
+
# Global slot only after we own the user lock
|
|
906
|
+
try:
|
|
907
|
+
await asyncio.wait_for(sem.acquire(), timeout=0.05)
|
|
908
|
+
sem_held = True
|
|
909
|
+
except asyncio.TimeoutError:
|
|
910
|
+
logger.warning("并发已满,拒绝处理 from=%s", from_user)
|
|
911
|
+
if context_token and from_user:
|
|
912
|
+
try:
|
|
913
|
+
await client.send_message(
|
|
914
|
+
to_user_id=from_user,
|
|
915
|
+
text=(
|
|
916
|
+
"⏳ **现在有点忙** ⏳\n\n"
|
|
917
|
+
"同时处理的消息太多,请过几秒再发。"
|
|
918
|
+
),
|
|
919
|
+
context_token=context_token,
|
|
920
|
+
baseurl=client.state.baseurl,
|
|
921
|
+
bot_token=client.state.bot_token,
|
|
922
|
+
)
|
|
923
|
+
except Exception as e:
|
|
924
|
+
logger.warning("发送繁忙提示失败: %s", e)
|
|
925
|
+
return
|
|
926
|
+
|
|
927
|
+
try:
|
|
928
|
+
await process_message(client, msg)
|
|
929
|
+
except Exception as e:
|
|
930
|
+
logger.exception("处理消息异常 (from=%s): %s", from_user, e)
|
|
931
|
+
finally:
|
|
932
|
+
if sem_held:
|
|
933
|
+
sem.release()
|
|
889
934
|
|
|
890
935
|
|
|
891
936
|
# ---------------------------------------------------------------------------
|
|
@@ -113,8 +113,8 @@ def validate_add_dir(path: str, user_id: str) -> tuple[bool, str]:
|
|
|
113
113
|
if not any(path_is_under(resolved, root) for root in allowed_roots):
|
|
114
114
|
return False, (
|
|
115
115
|
"路径不在允许范围内。"
|
|
116
|
-
"
|
|
117
|
-
+ ("
|
|
116
|
+
"仅允许你的工作区"
|
|
117
|
+
+ (" 或管理员已开放的目录" if config.add_dir_roots else "")
|
|
118
118
|
+ "。"
|
|
119
119
|
)
|
|
120
120
|
return True, resolved
|
|
@@ -151,8 +151,8 @@ def clean_output(text: str) -> str:
|
|
|
151
151
|
# WeChat error reply format (fixed header + body)
|
|
152
152
|
# ---------------------------------------------------------------------------
|
|
153
153
|
|
|
154
|
-
# Shown when
|
|
155
|
-
EMPTY_REPLY = "
|
|
154
|
+
# Shown when backend returns successfully but with no display text
|
|
155
|
+
EMPTY_REPLY = "(这次没有文字回复)"
|
|
156
156
|
|
|
157
157
|
|
|
158
158
|
def format_error(title: str, detail: str = "") -> str:
|
|
@@ -161,7 +161,7 @@ def format_error(title: str, detail: str = "") -> str:
|
|
|
161
161
|
Example:
|
|
162
162
|
❌ **未登录** ❌
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
助手尚未登录,请联系管理员。
|
|
165
165
|
"""
|
|
166
166
|
title = (title or "错误").strip().replace("\n", " ")
|
|
167
167
|
detail = (detail or "").strip()
|
|
@@ -179,29 +179,26 @@ def format_oversized_artifact_notice(art_name: str, size_mb: float) -> str:
|
|
|
179
179
|
return (
|
|
180
180
|
f"⚠️ **文件过大** ⚠️\n\n"
|
|
181
181
|
f"`{name}` {float(size_mb):.1f} MB\n"
|
|
182
|
-
f"
|
|
182
|
+
f"文件已生成,但太大无法发到微信。请联系管理员获取,或让我改小后再试。"
|
|
183
183
|
)
|
|
184
184
|
|
|
185
185
|
|
|
186
186
|
def format_cli_error(raw_message: str, *, backend: str = "") -> str:
|
|
187
|
-
"""Map
|
|
187
|
+
"""Map backend stderr/JSON error text into a short Chinese user reply.
|
|
188
188
|
|
|
189
|
-
Never put
|
|
190
|
-
|
|
191
|
-
is kept under「原始信息」only when it is not already Chinese-heavy.
|
|
189
|
+
Never put English raw blobs or internal path/env names into user text.
|
|
190
|
+
Details stay in the server log only.
|
|
192
191
|
"""
|
|
193
192
|
raw = clean_output(raw_message or "") or "未知错误"
|
|
194
193
|
lower = raw.lower()
|
|
195
194
|
backend = (backend or "").strip().lower()
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return format_error(title, zh)
|
|
202
|
-
return format_error(title, f"{zh}\n\n原始信息:\n{raw}")
|
|
195
|
+
logger.info(
|
|
196
|
+
"format_cli_error backend=%s raw=%.300s",
|
|
197
|
+
backend or "?",
|
|
198
|
+
raw,
|
|
199
|
+
)
|
|
203
200
|
|
|
204
|
-
# Auth / login
|
|
201
|
+
# Auth / login — ops details stay in logs; users contact admin
|
|
205
202
|
if (
|
|
206
203
|
"not signed in" in lower
|
|
207
204
|
or "authenticate" in lower
|
|
@@ -213,11 +210,11 @@ def format_cli_error(raw_message: str, *, backend: str = "") -> str:
|
|
|
213
210
|
or "unauthorized" in lower
|
|
214
211
|
or "401" in lower and ("auth" in lower or "token" in lower or "login" in lower)
|
|
215
212
|
or ("xai_api_key" in lower and ("sign" in lower or "login" in lower or "auth" in lower))
|
|
213
|
+
or "api_key" in lower and ("missing" in lower or "invalid" in lower or "required" in lower)
|
|
216
214
|
):
|
|
217
|
-
return
|
|
215
|
+
return format_error(
|
|
218
216
|
"未登录",
|
|
219
|
-
|
|
220
|
-
+ (" 请在本机执行 `grok login --device-code`,或设置 `XAI_API_KEY`。" if backend == "grok" else " 请检查本机登录/凭证是否有效。"),
|
|
217
|
+
"助手尚未登录或凭证失效,请联系管理员处理。",
|
|
221
218
|
)
|
|
222
219
|
|
|
223
220
|
# Rate limit / quota
|
|
@@ -229,7 +226,10 @@ def format_cli_error(raw_message: str, *, backend: str = "") -> str:
|
|
|
229
226
|
or "resource exhausted" in lower
|
|
230
227
|
or "429" in lower
|
|
231
228
|
):
|
|
232
|
-
return
|
|
229
|
+
return format_error(
|
|
230
|
+
"请求过于频繁",
|
|
231
|
+
"用得有点多,暂时被限制了,请稍后再试。",
|
|
232
|
+
)
|
|
233
233
|
|
|
234
234
|
# Network
|
|
235
235
|
if (
|
|
@@ -244,17 +244,20 @@ def format_cli_error(raw_message: str, *, backend: str = "") -> str:
|
|
|
244
244
|
or "fetch failed" in lower
|
|
245
245
|
or "socket hang up" in lower
|
|
246
246
|
):
|
|
247
|
-
return
|
|
247
|
+
return format_error("网络错误", "连不上服务,请检查网络后重试。")
|
|
248
248
|
|
|
249
|
-
# Cascade / API hang (agy)
|
|
249
|
+
# Cascade / API hang (agy) — plain language for users
|
|
250
250
|
if "timeout waiting for cascade" in lower or "timeout waiting for response" in lower:
|
|
251
|
-
return
|
|
251
|
+
return format_error(
|
|
252
|
+
"模型响应超时",
|
|
253
|
+
"模型响应超时,请稍后重试或简化指令。",
|
|
254
|
+
)
|
|
252
255
|
|
|
253
256
|
if "permission" in lower and ("denied" in lower or "refuse" in lower or "rejected" in lower):
|
|
254
|
-
return
|
|
257
|
+
return format_error("权限不足", "没有执行该操作的权限。")
|
|
255
258
|
|
|
256
259
|
if "timeout" in lower or "timed out" in lower or "deadline exceeded" in lower:
|
|
257
|
-
return
|
|
260
|
+
return format_error("超时", "等待响应超时,请稍后重试。")
|
|
258
261
|
|
|
259
262
|
if "model" in lower and (
|
|
260
263
|
"not found" in lower
|
|
@@ -265,21 +268,19 @@ def format_cli_error(raw_message: str, *, backend: str = "") -> str:
|
|
|
265
268
|
or "not supported" in lower
|
|
266
269
|
or "no such" in lower
|
|
267
270
|
):
|
|
268
|
-
return
|
|
271
|
+
return format_error("模型无效", "指定的模型不可用,请用 `/models` 查看后重选。")
|
|
269
272
|
|
|
270
273
|
if "command not found" in lower or "not a command" in lower:
|
|
271
|
-
return
|
|
274
|
+
return format_error(
|
|
275
|
+
"助手不可用",
|
|
276
|
+
"助手程序未正确安装或配置,请联系管理员。",
|
|
277
|
+
)
|
|
272
278
|
|
|
273
279
|
if "not found" in lower or "no such file" in lower or "enoent" in lower:
|
|
274
|
-
return
|
|
280
|
+
return format_error("未找到", "请求的资源或文件不存在。")
|
|
275
281
|
|
|
276
|
-
#
|
|
277
|
-
|
|
278
|
-
if backend == "grok":
|
|
279
|
-
title = "Grok 执行失败"
|
|
280
|
-
elif backend == "agy":
|
|
281
|
-
title = "agy 执行失败"
|
|
282
|
-
return format_error(title, raw)
|
|
282
|
+
# Unknown: fixed Chinese only — never echo English raw to WeChat users
|
|
283
|
+
return format_error("执行失败", "这次没处理好,请稍后再试。")
|
|
283
284
|
|
|
284
285
|
|
|
285
286
|
# ---------------------------------------------------------------------------
|
|
@@ -454,7 +455,7 @@ def format_model_label(model: str) -> str:
|
|
|
454
455
|
"""Human-readable model for switch replies."""
|
|
455
456
|
model = (model or "").strip()
|
|
456
457
|
if not model:
|
|
457
|
-
return "
|
|
458
|
+
return "默认(未指定)"
|
|
458
459
|
return model
|
|
459
460
|
|
|
460
461
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wechatbridge-cli
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.4
|
|
4
4
|
Summary: Bridge WeChat messages to agy or Grok Build CLIs — text/image/file/voice in, CLI replies and generated files back.
|
|
5
5
|
Author: WeChatBridge contributors
|
|
6
6
|
License: MIT
|
|
@@ -155,7 +155,7 @@ Key variables (all have defaults):
|
|
|
155
155
|
| `AGY_TIMEOUT` | `600` | CLI run timeout in seconds (both backends) |
|
|
156
156
|
| `WECHATBRIDGE_MAX_OUTBOUND_BYTES` | `104857600` | max file size sent back to WeChat (100 MB) |
|
|
157
157
|
| `WECHATBRIDGE_MAX_INBOUND_BYTES` | `20971520` | max inbound image/file after download (20 MB) |
|
|
158
|
-
| `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent
|
|
158
|
+
| `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent process slots; same user serial (queue does not hold a slot); extras get a busy reply |
|
|
159
159
|
| `WECHATBRIDGE_CONFIRM_TOKEN` | `y` | reply this token to approve a gated dangerous prompt |
|
|
160
160
|
| `WECHATBRIDGE_ENABLE_MCP` | `true` | enable the `/mcp` help text command |
|
|
161
161
|
| `WECHATBRIDGE_ENABLE_SUBAGENT` | `true` | enable the `/agent` prompt-rewrite command |
|
|
@@ -262,7 +262,7 @@ Other `/…` commands are either rejected (e.g. `/exit`), reported as unsupporte
|
|
|
262
262
|
- **Danger gate is keyword-based**, not full intent understanding. Defaults target concrete patterns (`rm -rf /`, pipe-to-shell, `mkfs`, `format c:`, a few heavy Chinese phrases, …). Everyday wording like bare “delete” is **not** gated. Override list via `WECHATBRIDGE_CONFIRM_KEYWORDS`; approve with `WECHATBRIDGE_CONFIRM_TOKEN` (default `y`), TTL `WECHATBRIDGE_PENDING_TTL`.
|
|
263
263
|
- **Inbound media** is size-capped (default 20 MB), streamed, and CDN hosts are allowlisted. Missing `aes_key` returns a clear error.
|
|
264
264
|
- **Outbound artifacts** only leave the allowed per-user tree (agy: session scratch; grok: under session dir), after `realpath` checks, and only if under `WECHATBRIDGE_MAX_OUTBOUND_BYTES`.
|
|
265
|
-
- **Concurrency:** global cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4)
|
|
265
|
+
- **Concurrency:** global process-slot cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4). Same user is serialized and does **not** hold a global slot while waiting on their previous message; different users can run in parallel up to the cap.
|
|
266
266
|
- **Long replies** are split into chunks (`WECHATBRIDGE_MESSAGE_CHUNK`, default 2000 characters).
|
|
267
267
|
- **Data layout:** instance data under `~/.local/share/wechatbridge/<instance>/` (override with env). Runtime dirs prefer `0700`; token/QR files prefer `0600` (Unix; Windows relies on NTFS ACLs).
|
|
268
268
|
- **Retention:** session temps vs dialogue history use separate TTLs (`WECHATBRIDGE_SESSION_RETENTION_DAYS`, `WECHATBRIDGE_HISTORY_RETENTION_DAYS`). Prefs/auth are kept.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{wechatbridge_cli-1.3.3 → wechatbridge_cli-1.3.4}/wechatbridge_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|