myagent-ai 1.22.2 → 1.23.0

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.
@@ -63,37 +63,37 @@ class MainAgent(BaseAgent):
63
63
  </output>
64
64
 
65
65
  注意事项:
66
- 1. toolstocal标签: 尽量一次性列出所有需执行工具调用的,多个"tool""工具调用只要按顺序重复堆叠tool标签即可,解析器会按顺序执行工具调用,最终全部执行完后,会连同所有结果,回调大语言模型。如果某个工具执行超时了,也会回调回调大模型,让大模型分析为什么超时,改用其他工具。如非必要,不要一次仅调用一个工具。
66
+ 1. toolstocal标签: 尽量一次性列出所有需执行工具调用的,多个"tool"工具调用只要按顺序重复堆叠tool标签即可,解析器会按顺序执行工具调用,最终全部执行完后,会连同所有结果,回调大语言模型。如果某个工具执行超时了,也会回调大模型,让大模型分析为什么超时,改用其他工具。如非必要,不要一次仅调用一个工具。
67
67
  2. 上下文中的记忆系统说明
68
68
  - <automemory>: 系统自动根据你通过 <remember> 保存的记忆和当前用户输入,搜索出的 top10 相关记忆。这些是你过去主动记住的内容(包含时间信息),可供参考。
69
69
  - <recall_memory>: 你在上一轮通过 <recall> 指定的记忆搜索结果。系统根据你提供的关键字和时间点搜索了 top5 相关记忆。
70
70
  - 两种记忆互补:automemory 是自动匹配的,recall_memory 是你主动指定搜索的。如果 automemory 不足,使用 <recall> 请求更多。
71
- 3. 工具选择指南
72
- - **搜索信息**: 用 `web_search`(返回标题+URL+摘要)
73
- - **读取网页内容**: 用 `web_read`(传入URL,提取正文)
74
- - **浏览器交互**(填表、点击、截图等): 才使用 browser_open / browser_click 等
75
- - **执行命令/代码**: `command` 工具执行 shell 命令(python/node/bash 等代码也通过 command 执行,如 `python script.py`、`node app.js`)
76
- - **文件操作**: `file_read` / `file_write` / `file_list` 等文件工具
77
- - **发送文件给用户**: `file_send` 工具(参数: file_path=文件路径, description=说明),当你生成或处理了文件需要返回给用户时使用
78
- - **播放音频**: `playaudio` 工具(参数: url=音乐链接或file_path=本地文件路径),在聊天中内嵌播放音频(支持QQ音乐、YouTube音乐、本地MP3/WAV等),播放时自动关闭语音合成
79
- - **播放视频**: `playvideo` 工具(参数: url=视频链接或file_path=本地文件路径),在聊天中内嵌播放视频(支持抖音、YouTube、B站、本地MP4等),播放时自动关闭语音合成
80
- - **网页控制**: `web_control` 工具在聊天中打开一个可控制的浏览器面板,可浏览网页、点击元素、填写表单、滚动页面、执行JS、管理Cookie等。
81
- - 打开: `{"action": "open", "url": "https://example.com"}` — 首次使用会自动创建会话,后续可传 session_id 复用
82
- - 导航: `{"action": "navigate", "url": "https://...", "session_id": "xxx"}`
83
- - 获取内容: `{"action": "get_content", "what": "text|html|url|title|links|images|forms|inputs", "session_id": "xxx"}`
84
- - 点击元素: `{"action": "click", "selector": "CSS选择器", "session_id": "xxx"}`
85
- - 填写输入: `{"action": "fill", "selector": "CSS选择器", "value": "内容", "session_id": "xxx"}`
86
- - 滚动页面: `{"action": "scroll", "direction": "up|down|top|bottom", "distance": 300, "session_id": "xxx"}`
87
- - 执行JS: `{"action": "evaluate", "script": "JavaScript代码", "session_id": "xxx"}`
88
- - 管理Cookie: `{"action": "set_cookies", "cookies": [{"name":"k","value":"v","domain":"example.com"}], "session_id": "xxx"}` 或 `{"action": "get_cookies", "session_id": "xxx"}`
89
- - 等待: `{"action": "wait", "time": 1000}` 毫秒 或 `{"action": "wait", "selector": ".result", "timeout": 10}` 秒
90
- - 关闭: `{"action": "close", "session_id": "xxx"}`
91
- - 注意: 网页通过服务端代理加载,用户可在面板中手动操作。复杂交互建议先用 get_content 查看页面结构再用 click/fill。
92
- - **主动召回记忆**: 用 `recall_memory` 工具(参数: keyword=关键字, time_point=可选时间点如"2025-01", limit=数量默认5),根据关键字和时间搜索历史记忆
93
- - **OCR 文字识别**: 用 `image_ocr` 工具(参数: image_path=图片路径, lang=ch/en),从截图、扫描件、照片中提取文字
94
- - **图片内容分析**: `image_analyze` 工具(参数: image_path=图片路径, prompt=分析提示词),识别图片中的物体、文字、图表数据等(需模型支持视觉)
95
- - **语音转文字**: 用 `audio_transcribe` 工具(参数: audio_path=音频路径, language=zh/en),将音频文件转录为文本
96
- - **专业技能指令**: 系统内置了丰富的专业技能指南(如 PDF/DOCX/XLSX/PPT 生成、图表绘制、前端开发、全栈开发、图像生成等),当你需要执行特定领域的复杂任务时,通过 `<get_knowledge>` 标签请求相关技能指令(如填写 "PDF文档生成指南"、"PPT制作规范" 等),系统将在下一轮通过 `<knowledge>` 注入完整指令。
71
+ 3. 工具使用指南 — 只有一个工具: command
72
+ 你只有一个工具就是 command(执行命令行),所有操作都通过它完成。格式:
73
+ <tool><toolname>command</toolname><parms>{"command": "要执行的命令"}</parms><timeout>超时秒数</timeout></tool>
74
+
75
+ 常用 CLI 命令 (通过 command 工具调用):
76
+ - OCR 文字识别: myagent-ai ocr 图片路径 [ch|en]
77
+ - 图片分析(VLM): myagent-ai analyze-image 图片路径 [分析提示词]
78
+ - 语音转文字: myagent-ai transcribe 音频路径 [zh|en|ja]
79
+ - 网络搜索: myagent-ai search 关键词
80
+ - 读取网页: myagent-ai read-url URL
81
+ - 发送文件给用户: myagent-ai send-file 文件路径 描述
82
+ - 读文件: cat 文件路径
83
+ - 写文件: echo "内容" > 文件路径 或 python3 -c "open('f','w').write('内容')"
84
+ - 执行代码: python3 script.py 或 python3 -c "代码"
85
+ - 文件列表: ls -la 目录
86
+ - 系统信息: uname -a / df -h / free -h
87
+
88
+ 调用示例:
89
+ <tool><toolname>command</toolname><parms>{"command": "myagent-ai ocr /tmp/screenshot.png"}</parms><timeout>30</timeout></tool>
90
+ <tool><toolname>command</toolname><parms>{"command": "myagent-ai search 人工智能最新进展"}</parms><timeout>15</timeout></tool>
91
+ <tool><toolname>command</toolname><parms>{"command": "myagent-ai send-file /tmp/report.pdf 季度报告"}</parms><timeout>10</timeout></tool>
92
+
93
+ 多个命令可用 && 连接一次执行:
94
+ <tool><toolname>command</toolname><parms>{"command": "myagent-ai search xxx && myagent-ai read-url https://..."}</parms><timeout>30</timeout></tool>
95
+
96
+ 专业技能指令: 系统内置了丰富的专业技能指南(PDF/DOCX/XLSX/PPT 生成、图表绘制、前端开发等),通过 <get_knowledge> 请求相关技能指令。
97
97
  4. 准备好内容后,最后,再检查输出格式,确保满足以下要求:
98
98
  <output>
99
99
  <mainsubject>当前对话的6字以内标题(每轮都需输出,系统会每3轮自动更新会话名称)</mainsubject>
@@ -169,7 +169,38 @@ class ToolDispatcher:
169
169
  exec_result = await self.executor.execute(
170
170
  language="shell", code=code_text, timeout=timeout,
171
171
  )
172
- return exec_result.to_dict()
172
+ result = exec_result.to_dict()
173
+
174
+ # [v1.22.0] 检测 __SEND_FILE__ 标记 — CLI send-file 命令输出此标记
175
+ # 格式: __SEND_FILE__绝对路径|描述__END__
176
+ output = result.get("output", "")
177
+ import re as _re
178
+ send_markers = _re.findall(r'__SEND_FILE__(.+?)\|(.+?)__END__', output)
179
+ if send_markers:
180
+ # 从输出中移除标记行
181
+ clean_output = _re.sub(r'__SEND_FILE__.+?__END__\n?', '', output).strip()
182
+ result["output"] = clean_output
183
+ # 执行 file_send
184
+ for send_path, send_desc in send_markers:
185
+ send_path = send_path.strip()
186
+ send_desc = send_desc.strip()
187
+ try:
188
+ p = _P(send_path)
189
+ if p.exists():
190
+ from skills.file_send import FileSendSkill
191
+ fskill = FileSendSkill()
192
+ fresult = await fskill.execute(
193
+ str(p), send_desc or f"文件: {p.name}",
194
+ )
195
+ if fresult.get("success"):
196
+ logger.info(f"[{task_id}] CLI 自动发送文件: {p.name}")
197
+ else:
198
+ result["output"] += f"\n[文件发送失败: {fresult.get('error', '')}]"
199
+ except Exception as e:
200
+ logger.warning(f"[{task_id}] CLI 文件发送异常: {e}")
201
+ result["output"] += f"\n[文件发送异常: {e}]"
202
+
203
+ return result
173
204
 
174
205
  async def _exec_recall_memory(self, params: Dict, task_id: str) -> Dict:
175
206
  """主动召回记忆"""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.22.2",
3
+ "version": "1.23.0",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/scripts/cli.py ADDED
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ scripts/cli.py - MyAgent CLI 工具集
4
+ ====================================
5
+ 轻量级命令行工具,LLM 通过 command 工具调用这些命令完成所有操作。
6
+
7
+ 用法: myagent-ai <command> [args...]
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import base64
13
+ import json
14
+ import mimetypes
15
+ import os
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ # 确保可以导入 myagent 模块
20
+ _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
21
+ _PROJECT_ROOT = os.path.dirname(_SCRIPT_DIR)
22
+ if _PROJECT_ROOT not in sys.path:
23
+ sys.path.insert(0, _PROJECT_ROOT)
24
+
25
+ _DATA_DIR = os.path.expanduser("~/.myagent/data")
26
+
27
+
28
+ def _load_config() -> dict:
29
+ """加载用户配置"""
30
+ config_path = os.path.join(_DATA_DIR, "config.json")
31
+ if os.path.exists(config_path):
32
+ with open(config_path, encoding="utf-8") as f:
33
+ return json.load(f)
34
+ return {}
35
+
36
+
37
+ def _err(msg: str) -> None:
38
+ print(f"错误: {msg}", file=sys.stderr)
39
+
40
+
41
+ # =============================================================================
42
+ # OCR 光学字符识别
43
+ # =============================================================================
44
+ def cmd_ocr(args: list[str]) -> int:
45
+ """从图片中提取文字"""
46
+ image = args[0] if args else ""
47
+ lang = "ch"
48
+ for a in args[1:]:
49
+ if a in ("ch", "en", "japan", "korean", "ta", "te", "kn", "hi", "ar", "ur"):
50
+ lang = a
51
+ break
52
+
53
+ if not image:
54
+ _err("用法: myagent-ai ocr <图片路径> [ch|en|japan|korean|...]")
55
+ return 1
56
+ image = os.path.expanduser(image)
57
+ if not os.path.isfile(image):
58
+ _err(f"图片不存在: {image}")
59
+ return 1
60
+
61
+ ext = Path(image).suffix.lower().lstrip(".")
62
+ supported = ("png", "jpg", "jpeg", "bmp", "tiff", "tif", "webp", "gif")
63
+ if ext not in supported:
64
+ _err(f"不支持的图片格式: .{ext}")
65
+ return 1
66
+
67
+ try:
68
+ from paddleocr import PaddleOCR
69
+ except ImportError:
70
+ _err("paddleocr 未安装。安装: pip install paddleocr paddlepaddle")
71
+ return 1
72
+
73
+ try:
74
+ ocr = PaddleOCR(use_angle_cls=True, lang=lang, show_log=False)
75
+ result = ocr.ocr(image, cls=True)
76
+ if result and result[0]:
77
+ for line in result[0]:
78
+ text, (conf,) = line[1]
79
+ print(text)
80
+ else:
81
+ print("(未检测到文字)")
82
+ except Exception as e:
83
+ _err(f"OCR 失败: {e}")
84
+ return 1
85
+ return 0
86
+
87
+
88
+ # =============================================================================
89
+ # 图片内容分析 (VLM)
90
+ # =============================================================================
91
+ def cmd_analyze_image(args: list[str]) -> int:
92
+ """使用视觉语言模型分析图片"""
93
+ image = args[0] if args else ""
94
+ # prompt 从 --prompt 后面取,或剩余参数拼接
95
+ prompt = ""
96
+ rest = args[1:]
97
+ if "--prompt" in rest:
98
+ idx = rest.index("--prompt")
99
+ prompt = " ".join(rest[idx + 1:]) if idx + 1 < len(rest) else ""
100
+ elif rest:
101
+ prompt = " ".join(rest)
102
+ if not prompt:
103
+ prompt = "请详细描述这张图片的内容,包括文字、布局、物体等信息。"
104
+
105
+ if not image:
106
+ _err("用法: myagent-ai analyze-image <图片路径> [--prompt 分析提示词]")
107
+ return 1
108
+ image = os.path.expanduser(image)
109
+ if not os.path.isfile(image):
110
+ _err(f"图片不存在: {image}")
111
+ return 1
112
+
113
+ # 检查图片大小
114
+ fsize = os.path.getsize(image)
115
+ if fsize > 20 * 1024 * 1024:
116
+ _err(f"图片过大 ({fsize / 1024 / 1024:.1f}MB),限制 20MB")
117
+ return 1
118
+
119
+ # 读取 LLM 配置
120
+ cfg = _load_config().get("llm", {})
121
+ api_key = cfg.get("api_key", "")
122
+ base_url = cfg.get("base_url", "")
123
+ model = cfg.get("model", "")
124
+
125
+ # 模型链支持: 检查是否有视觉模型
126
+ model_chain = cfg.get("model_chain", [])
127
+ vision_model = None
128
+ vision_base_url = base_url
129
+ for m in model_chain:
130
+ m_name = m.get("model", "")
131
+ m_modes = m.get("modes", [])
132
+ if "image" in m_modes:
133
+ vision_model = m_name
134
+ provider_cfg = m.get("provider", "")
135
+ if provider_cfg:
136
+ # 从 provider 配置中获取 base_url
137
+ provider_settings = cfg.get("providers", {}).get(provider_cfg, {})
138
+ vision_base_url = provider_settings.get("base_url", base_url)
139
+ break
140
+
141
+ if not vision_model and not api_key:
142
+ _err("未配置支持视觉的模型。请在配置中添加 image 模式的模型。")
143
+ return 1
144
+
145
+ use_model = vision_model or model
146
+ use_url = vision_base_url or base_url
147
+
148
+ if not use_url:
149
+ _err("未配置 LLM API 地址")
150
+ return 1
151
+
152
+ # 编码图片
153
+ with open(image, "rb") as f:
154
+ b64 = base64.b64encode(f.read()).decode()
155
+ mime = mimetypes.guess_type(image)[0] or "image/png"
156
+
157
+ try:
158
+ from openai import OpenAI
159
+ client = OpenAI(api_key=api_key, base_url=use_url)
160
+ resp = client.chat.completions.create(
161
+ model=use_model,
162
+ messages=[{
163
+ "role": "user",
164
+ "content": [
165
+ {"type": "text", "text": prompt},
166
+ {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}},
167
+ ],
168
+ }],
169
+ max_tokens=4096,
170
+ temperature=0.1,
171
+ )
172
+ content = resp.choices[0].message.content
173
+ if content:
174
+ print(content)
175
+ else:
176
+ print("(模型未返回分析结果)")
177
+ except Exception as e:
178
+ _err(f"图片分析失败: {e}")
179
+ return 1
180
+ return 0
181
+
182
+
183
+ # =============================================================================
184
+ # 语音转文字 (STT)
185
+ # =============================================================================
186
+ async def _do_transcribe(audio_path: str, language: str) -> dict:
187
+ from core.stt import transcribe
188
+ return await transcribe(audio_path=audio_path, language=language)
189
+
190
+
191
+ def cmd_transcribe(args: list[str]) -> int:
192
+ """将音频文件转录为文本"""
193
+ audio = args[0] if args else ""
194
+ lang = "zh"
195
+ for a in args[1:]:
196
+ if a in ("zh", "en", "ja", "ko", "yue", "auto"):
197
+ lang = a
198
+ break
199
+
200
+ if not audio:
201
+ _err("用法: myagent-ai transcribe <音频路径> [zh|en|ja|ko|yue]")
202
+ return 1
203
+ audio = os.path.expanduser(audio)
204
+ if not os.path.isfile(audio):
205
+ _err(f"音频文件不存在: {audio}")
206
+ return 1
207
+
208
+ try:
209
+ result = asyncio.run(_do_transcribe(audio, lang))
210
+ if result.get("success"):
211
+ print(result["output"])
212
+ return 0
213
+ else:
214
+ _err(result.get("error", "转录失败"))
215
+ return 1
216
+ except RuntimeError:
217
+ # 已有事件循环
218
+ loop = asyncio.new_event_loop()
219
+ asyncio.set_event_loop(loop)
220
+ try:
221
+ result = loop.run_until_complete(_do_transcribe(audio, lang))
222
+ if result.get("success"):
223
+ print(result["output"])
224
+ return 0
225
+ else:
226
+ _err(result.get("error", "转录失败"))
227
+ return 1
228
+ finally:
229
+ loop.close()
230
+ except Exception as e:
231
+ _err(f"语音转文字失败: {e}")
232
+ return 1
233
+
234
+
235
+ # =============================================================================
236
+ # 网络搜索
237
+ # =============================================================================
238
+ def cmd_search(args: list[str]) -> int:
239
+ """DuckDuckGo 网络搜索"""
240
+ query = " ".join(args)
241
+ if not query:
242
+ _err("用法: myagent-ai search <关键词>")
243
+ return 1
244
+
245
+ try:
246
+ from duckduckgo_search import DDGS
247
+ with DDGS() as ddgs:
248
+ results = list(ddgs.text(query, max_results=5))
249
+ if results:
250
+ for i, r in enumerate(results, 1):
251
+ print(f"{i}. {r['title']}")
252
+ print(f" URL: {r['href']}")
253
+ print(f" {r['body']}")
254
+ print()
255
+ else:
256
+ print("(未找到搜索结果)")
257
+ except Exception as e:
258
+ _err(f"搜索失败: {e}")
259
+ return 1
260
+ return 0
261
+
262
+
263
+ # =============================================================================
264
+ # 读取网页内容
265
+ # =============================================================================
266
+ def cmd_read_url(args: list[str]) -> int:
267
+ """提取网页正文内容"""
268
+ url = args[0] if args else ""
269
+ if not url:
270
+ _err("用法: myagent-ai read-url <URL>")
271
+ return 1
272
+
273
+ try:
274
+ import requests
275
+ from bs4 import BeautifulSoup
276
+
277
+ resp = requests.get(url, timeout=15, headers={
278
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
279
+ })
280
+ resp.raise_for_status()
281
+ soup = BeautifulSoup(resp.text, "html.parser")
282
+ # 移除无关标签
283
+ for tag in soup(["script", "style", "nav", "header", "footer", "aside"]):
284
+ tag.decompose()
285
+ # 提取标题
286
+ title = soup.title.string.strip() if soup.title else ""
287
+ if title:
288
+ print(f"标题: {title}\n")
289
+ text = soup.get_text(separator="\n", strip=True)
290
+ # 截断过长内容
291
+ if len(text) > 8000:
292
+ text = text[:8000] + "\n...(内容过长,已截断)"
293
+ print(text)
294
+ except Exception as e:
295
+ _err(f"读取网页失败: {e}")
296
+ return 1
297
+ return 0
298
+
299
+
300
+ # =============================================================================
301
+ # 发送文件给用户
302
+ # =============================================================================
303
+ def cmd_send_file(args: list[str]) -> int:
304
+ """发送文件给用户 (输出特殊标记,由 ToolDispatcher 解析)"""
305
+ fpath = args[0] if args else ""
306
+ desc = ""
307
+ if len(args) > 1:
308
+ # 支持两种格式:
309
+ # myagent-ai send-file path description
310
+ # myagent-ai send-file path --desc "description"
311
+ rest = args[1:]
312
+ if "--desc" in rest:
313
+ idx = rest.index("--desc")
314
+ desc = " ".join(rest[idx + 1:]) if idx + 1 < len(rest) else ""
315
+ else:
316
+ desc = " ".join(rest)
317
+
318
+ if not fpath:
319
+ _err("用法: myagent-ai send-file <文件路径> [描述]")
320
+ return 1
321
+ fpath = os.path.expanduser(fpath)
322
+ if not os.path.isfile(fpath):
323
+ _err(f"文件不存在: {fpath}")
324
+ return 1
325
+
326
+ # 输出特殊标记 (ToolDispatcher._exec_command 会检测并自动发送)
327
+ abs_path = os.path.abspath(fpath)
328
+ print(f"__SEND_FILE__{abs_path}|{desc}__END__")
329
+ return 0
330
+
331
+
332
+ # =============================================================================
333
+ # 命令注册
334
+ # =============================================================================
335
+ COMMANDS: dict[str, tuple] = {
336
+ "ocr": (cmd_ocr, "OCR 光学字符识别", "<图片路径> [ch|en|japan|korean|...]"),
337
+ "analyze-image": (cmd_analyze_image, "图片内容分析 (VLM)", "<图片路径> [--prompt 分析提示词]"),
338
+ "transcribe": (cmd_transcribe, "语音转文字", "<音频路径> [zh|en|ja|ko|yue]"),
339
+ "search": (cmd_search, "网络搜索 (DuckDuckGo)", "<关键词>"),
340
+ "read-url": (cmd_read_url, "读取网页正文内容", "<URL>"),
341
+ "send-file": (cmd_send_file, "发送文件给用户", "<文件路径> [描述]"),
342
+ }
343
+
344
+
345
+ def main():
346
+ if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help", "help"):
347
+ print("MyAgent CLI - 命令行工具集\n")
348
+ print("用法: myagent-ai <command> [args...]\n")
349
+ print("可用命令:")
350
+ for name, (_, desc, usage) in COMMANDS.items():
351
+ print(f" myagent-ai {name} {usage}")
352
+ print(f" → {desc}")
353
+ print()
354
+ if len(sys.argv) < 2:
355
+ sys.exit(1)
356
+ return
357
+
358
+ cmd = sys.argv[1]
359
+ if cmd not in COMMANDS:
360
+ _err(f"未知命令: {cmd}")
361
+ _err(f"可用命令: {', '.join(COMMANDS.keys())}")
362
+ _err("运行 myagent-ai help 查看帮助")
363
+ sys.exit(1)
364
+
365
+ handler = COMMANDS[cmd][0]
366
+ rc = handler(sys.argv[2:])
367
+ sys.exit(rc or 0)
368
+
369
+
370
+ if __name__ == "__main__":
371
+ main()
package/start.js CHANGED
@@ -539,7 +539,7 @@ function cmdUpdate(pkgDir) {
539
539
  // 1. npm 升级全局包(--force 确保跳过缓存)
540
540
  console.log(" 正在通过 npm 更新 myagent-ai ...");
541
541
  try {
542
- execFileSync("npm", ["install", "-g", PKG_NAME, "--prefer-online", "--force"], {
542
+ execFileSync("npm", ["install", "-g", PKG_NAME + "@latest", "--prefer-online", "--force"], {
543
543
  encoding: "utf8", stdio: "inherit", timeout: 120000,
544
544
  });
545
545
  } catch (e) {
@@ -764,6 +764,43 @@ function main() {
764
764
  if (cmd === "reinstall") { cmdReinstall(pkgDir); return; }
765
765
  if (cmd === "install") { cmdInstall(pkgDir); return; }
766
766
 
767
+ // [v1.22.0] CLI 子命令 — 轻量级,不需要完整启动 myagent
768
+ const CLI_CMDS = ["ocr", "analyze-image", "transcribe", "search", "read-url", "send-file", "help", "-h", "--help"];
769
+ if (CLI_CMDS.includes(cmd)) {
770
+ const venvPython = getVenvPython(getVenvDir());
771
+ const venvDir = getVenvDir();
772
+ const cliScript = path.join(pkgDir, "scripts", "cli.py");
773
+ if (!fs.existsSync(cliScript)) {
774
+ console.error("\x1b[31mCLI 脚本不存在: " + cliScript + "\x1b[0m");
775
+ process.exit(1);
776
+ }
777
+ let pyExe;
778
+ let env;
779
+ if (fs.existsSync(venvPython)) {
780
+ pyExe = venvPython;
781
+ env = {
782
+ ...process.env,
783
+ VIRTUAL_ENV: venvDir,
784
+ PATH: `${path.join(venvDir, IS_WIN ? "Scripts" : "bin")}:${process.env.PATH}`,
785
+ };
786
+ } else {
787
+ pyExe = findSystemPython();
788
+ if (!pyExe) {
789
+ console.error("\x1b[31mPython 未找到,请先运行: myagent-ai install\x1b[0m");
790
+ process.exit(1);
791
+ }
792
+ env = process.env;
793
+ }
794
+ try {
795
+ execFileSync(pyExe, [cliScript, ...args], {
796
+ stdio: "inherit", cwd: pkgDir, env, timeout: 300000,
797
+ });
798
+ } catch (e) {
799
+ process.exit(e.status || 1);
800
+ }
801
+ return;
802
+ }
803
+
767
804
  // 默认: 直接启动
768
805
  cmdRun(pkgDir, args);
769
806
  }