myagent-ai 1.5.1 → 1.5.3

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.
package/core/llm.py CHANGED
@@ -209,12 +209,15 @@ class LLMClient:
209
209
  # 客户端初始化
210
210
  # ------------------------------------------------------------------
211
211
 
212
+ # 所有使用 OpenAI 兼容接口的提供商
213
+ _OPENAI_COMPATIBLE_PROVIDERS = ("openai", "custom", "modelscope", "deepseek", "moonshot", "qwen", "dashscope")
214
+
212
215
  def _ensure_client(self):
213
216
  """延迟初始化 LLM 客户端"""
214
217
  if self._client is not None:
215
218
  return
216
219
 
217
- if self.provider in ("openai", "custom"):
220
+ if self.provider in self._OPENAI_COMPATIBLE_PROVIDERS:
218
221
  self._init_openai()
219
222
  elif self.provider == "anthropic":
220
223
  self._init_anthropic()
@@ -223,7 +226,9 @@ class LLMClient:
223
226
  elif self.provider == "zhipu":
224
227
  self._init_zhipu()
225
228
  else:
226
- raise ValueError(f"不支持的 LLM 提供商: {self.provider}")
229
+ # 未知提供商默认尝试 OpenAI 兼容接口(大多数 API 都是 OpenAI 兼容的)
230
+ logger.warning(f"未知 LLM 提供商 '{self.provider}',尝试 OpenAI 兼容接口")
231
+ self._init_openai()
227
232
 
228
233
  def _init_openai(self):
229
234
  """初始化 OpenAI / 兼容客户端"""
@@ -375,7 +380,7 @@ class LLMClient:
375
380
  request_kwargs.update(kwargs)
376
381
 
377
382
  try:
378
- if self.provider in ("openai", "custom", "zhipu"):
383
+ if self.provider in self._OPENAI_COMPATIBLE_PROVIDERS or self.provider == "zhipu":
379
384
  response = await self._run_with_retry(self._chat_openai, request_kwargs)
380
385
  elif self.provider == "anthropic":
381
386
  response = await self._run_with_retry(
@@ -581,7 +586,7 @@ class LLMClient:
581
586
  request_kwargs.update(kwargs)
582
587
 
583
588
  try:
584
- if self.provider in ("openai", "custom", "zhipu"):
589
+ if self.provider in self._OPENAI_COMPATIBLE_PROVIDERS or self.provider == "zhipu":
585
590
  async for chunk in self._stream_openai(request_kwargs):
586
591
  yield chunk
587
592
  elif self.provider == "anthropic":
@@ -317,7 +317,7 @@ class UpdateManager:
317
317
  if not pkg_json.exists():
318
318
  return ""
319
319
 
320
- pkg_data = json.loads(pkg_json.read_text())
320
+ pkg_data = json.loads(pkg_json.read_text(encoding="utf-8"))
321
321
  pkg_name = pkg_data.get("name", "")
322
322
  if not pkg_name:
323
323
  return ""
package/core/version.py CHANGED
@@ -11,11 +11,11 @@ import subprocess
11
11
  from pathlib import Path
12
12
 
13
13
  # ── 基线版本(与 setup.py / package.json 保持一致) ──
14
- BASE_VERSION = "1.5.0"
14
+ BASE_VERSION = "1.5.3"
15
15
 
16
16
 
17
17
  def _version_from_git() -> str:
18
- """尝试从 git describe 获取版本号"""
18
+ """尝试从 git describe 获取版本号(必须是 x.y.z 格式)"""
19
19
  try:
20
20
  result = subprocess.run(
21
21
  ["git", "describe", "--tags", "--always", "--dirty"],
@@ -24,8 +24,13 @@ def _version_from_git() -> str:
24
24
  )
25
25
  tag = result.stdout.strip()
26
26
  if tag:
27
- # 去掉 'v' 前缀,保留 dirty 标记
28
- return tag.lstrip("v")
27
+ # 去掉 'v' 前缀
28
+ tag = tag.lstrip("v")
29
+ # 只接受 x.y.z 格式的版本号,拒绝 commit hash
30
+ if tag and tag[0].isdigit() and "." in tag:
31
+ # 去掉 dirty 标记和 build metadata
32
+ clean = tag.split("-dirty")[0].split("+")[0]
33
+ return clean
29
34
  except Exception:
30
35
  pass
31
36
  return ""
@@ -9,7 +9,7 @@ param(
9
9
 
10
10
  $ErrorActionPreference = "Stop"
11
11
  $PKG_NAME = "myagent-ai"
12
- $PKG_VERSION = "1.5.1"
12
+ $PKG_VERSION = "1.5.3"
13
13
 
14
14
  # Allow running scripts for the current process
15
15
  if ($PSVersionTable.PSVersion.Major -ge 5) {
@@ -21,7 +21,7 @@ NO_DEPS=false
21
21
  DRY_RUN=false
22
22
  SCRIPT_URL="https://raw.githubusercontent.com/ctz168/myagent/main/install/install.sh"
23
23
  PKG_NAME="myagent-ai"
24
- PKG_VERSION="1.5.1"
24
+ PKG_VERSION="1.5.3"
25
25
 
26
26
  for arg in "$@"; do
27
27
  case "$arg" in
@@ -0,0 +1,142 @@
1
+ # MyAgent Uninstaller for Windows
2
+ # Usage: powershell -c "irm https://raw.githubusercontent.com/ctz168/myagent/main/install/uninstall.ps1 | iex"
3
+ # powershell -c "& ([scriptblock]::Create((irm https://raw.githubusercontent.com/ctz168/myagent/main/install/uninstall.ps1))) -Purge"
4
+ # powershell -c "& ([scriptblock]::Create((irm https://raw.githubusercontent.com/ctz168/myagent/main/install/uninstall.ps1))) -Force"
5
+
6
+ param(
7
+ [switch]$Purge,
8
+ [switch]$Force,
9
+ [switch]$DryRun
10
+ )
11
+
12
+ $ErrorActionPreference = "Stop"
13
+ $PKG_NAME = "myagent-ai"
14
+ $DATA_DIR = "$env:USERPROFILE\.myagent"
15
+
16
+ # Allow running scripts for the current process
17
+ if ($PSVersionTable.PSVersion.Major -ge 5) {
18
+ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
19
+ }
20
+
21
+ Write-Host ""
22
+ Write-Host " MyAgent Uninstaller" -ForegroundColor Cyan
23
+ Write-Host ""
24
+
25
+ if ($DryRun) {
26
+ Write-Host "[OK] Dry run" -ForegroundColor Green
27
+ exit 0
28
+ }
29
+
30
+ # ── Step 1: 停止运行中的 MyAgent 进程 ──────────────
31
+ Write-Host "[*] Stopping MyAgent processes..." -ForegroundColor Yellow
32
+
33
+ # 尝试通过 API 优雅关闭
34
+ try {
35
+ $null = Invoke-WebRequest -Uri "http://127.0.0.1:8767/api/shutdown" -Method POST -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop
36
+ Write-Host "[OK] MyAgent 服务已优雅关闭" -ForegroundColor Green
37
+ Start-Sleep -Seconds 2
38
+ } catch {
39
+ # 服务未运行,继续
40
+ }
41
+
42
+ # 检查并终止残留进程(Python 进程中含 myagent/main.py 的)
43
+ $procs = @()
44
+ try {
45
+ $procs += Get-Process -Name "python*" -ErrorAction SilentlyContinue | Where-Object {
46
+ try {
47
+ $_.CommandLine -match "myagent|main\.py"
48
+ } catch {}
49
+ }
50
+ } catch {}
51
+ try {
52
+ $procs += Get-Process -Name "node" -ErrorAction SilentlyContinue | Where-Object {
53
+ try {
54
+ $_.CommandLine -match "myagent|start\.js"
55
+ } catch {}
56
+ }
57
+ } catch {}
58
+
59
+ if ($procs.Count -gt 0) {
60
+ Write-Host " 发现 $($procs.Count) 个相关进程" -ForegroundColor Gray
61
+ foreach ($proc in $procs) {
62
+ try {
63
+ Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
64
+ } catch {}
65
+ }
66
+ Start-Sleep -Seconds 1
67
+
68
+ # 强制终止仍在运行的
69
+ $procs2 = @()
70
+ try {
71
+ $procs2 += Get-Process -Name "python*" -ErrorAction SilentlyContinue | Where-Object {
72
+ try { $_.CommandLine -match "myagent|main\.py" } catch {}
73
+ }
74
+ $procs2 += Get-Process -Name "node" -ErrorAction SilentlyContinue | Where-Object {
75
+ try { $_.CommandLine -match "myagent|start\.js" } catch {}
76
+ }
77
+ } catch {}
78
+ foreach ($proc in $procs2) {
79
+ try { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } catch {}
80
+ }
81
+ Write-Host "[OK] MyAgent 进程已停止" -ForegroundColor Green
82
+ } else {
83
+ Write-Host "[OK] 没有运行中的 MyAgent 进程" -ForegroundColor Green
84
+ }
85
+
86
+ # ── Step 2: 卸载 npm 全局包 ─────────────────────
87
+ Write-Host "[*] Uninstalling $PKG_NAME from npm global..." -ForegroundColor Yellow
88
+
89
+ $installed = $false
90
+ try {
91
+ $listResult = npm list -g $PKG_NAME 2>$null
92
+ if ($listResult -match $PKG_NAME) {
93
+ npm uninstall -g $PKG_NAME
94
+ Write-Host "[OK] $PKG_NAME 已从 npm 全局卸载" -ForegroundColor Green
95
+ $installed = $true
96
+ }
97
+ } catch {}
98
+
99
+ if (-not $installed) {
100
+ Write-Host "[i] $PKG_NAME 未通过 npm 全局安装(可能是源码方式安装)" -ForegroundColor Gray
101
+ }
102
+
103
+ # ── Step 3: 清理可选数据(-Purge) ─────────────
104
+ if ($Purge) {
105
+ Write-Host ""
106
+ Write-Host "[*] 清除所有 MyAgent 数据..." -ForegroundColor Yellow
107
+
108
+ if (Test-Path $DATA_DIR) {
109
+ $size = (Get-ChildItem -Path $DATA_DIR -Recurse -Force -ErrorAction SilentlyContinue |
110
+ Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
111
+ $sizeMB = [math]::Round($size / 1MB, 2)
112
+ Write-Host " 数据目录: $DATA_DIR" -ForegroundColor Gray
113
+ Write-Host " 数据大小: ${sizeMB} MB" -ForegroundColor Gray
114
+
115
+ Remove-Item -Path $DATA_DIR -Recurse -Force -ErrorAction SilentlyContinue
116
+ Write-Host "[OK] 数据目录已删除: $DATA_DIR (${sizeMB} MB)" -ForegroundColor Green
117
+ } else {
118
+ Write-Host "[i] 数据目录不存在: $DATA_DIR" -ForegroundColor Gray
119
+ }
120
+
121
+ # 清理可能的日志文件
122
+ @("$env:USERPROFILE\.myagent.log", "$env:USERPROFILE\myagent.log") | ForEach-Object {
123
+ if (Test-Path $_) {
124
+ Remove-Item -Force $_ -ErrorAction SilentlyContinue
125
+ Write-Host "[i] 已删除日志: $_" -ForegroundColor Gray
126
+ }
127
+ }
128
+
129
+ Write-Host "[OK] 所有数据已清除" -ForegroundColor Green
130
+ } else {
131
+ Write-Host ""
132
+ Write-Host "[i] 保留数据和虚拟环境: $DATA_DIR" -ForegroundColor Gray
133
+ Write-Host "[i] 如需彻底清除,请使用: irm ... | iex (添加 -Purge 参数)" -ForegroundColor Gray
134
+ Write-Host "[i] 或手动删除: Remove-Item -Recurse -Force $DATA_DIR" -ForegroundColor Gray
135
+ }
136
+
137
+ Write-Host ""
138
+ Write-Host " 卸载完成!" -ForegroundColor Green
139
+ Write-Host ""
140
+ Write-Host "[i] 如需重新安装: npm install -g $PKG_NAME" -ForegroundColor Gray
141
+ Write-Host "[i] 或使用一键安装: irm https://raw.githubusercontent.com/ctz168/myagent/main/install/install.ps1 | iex" -ForegroundColor Gray
142
+ Write-Host ""
@@ -0,0 +1,149 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # MyAgent Uninstaller for macOS and Linux
5
+ # 卸载脚本
6
+ #
7
+ # 用法:
8
+ # curl -fsSL https://raw.githubusercontent.com/ctz168/myagent/main/install/uninstall.sh | bash
9
+ # curl -fsSL ... | bash -s -- --purge # 同时删除数据和虚拟环境
10
+ # curl -fsSL ... | bash -s -- --dry-run # 预览模式
11
+
12
+ BOLD='\033[1m'
13
+ ACCENT='\033[36m'
14
+ INFO='\033[90m'
15
+ SUCCESS='\033[32m'
16
+ WARN='\033[33m'
17
+ ERROR='\033[31m'
18
+ NC='\033[0m'
19
+
20
+ PURGE=false
21
+ DRY_RUN=false
22
+ FORCE=false
23
+ SCRIPT_URL="https://raw.githubusercontent.com/ctz168/myagent/main/install/uninstall.sh"
24
+ PKG_NAME="myagent-ai"
25
+ DATA_DIR="$HOME/.myagent"
26
+
27
+ for arg in "$@"; do
28
+ case "$arg" in
29
+ --purge) PURGE=true ;;
30
+ --dry-run) DRY_RUN=true ;;
31
+ --force|-f) FORCE=true ;;
32
+ --help|-h)
33
+ echo "MyAgent Uninstaller"
34
+ echo ""
35
+ echo "Usage: curl -fsSL $SCRIPT_URL | bash [-s -- OPTIONS]"
36
+ echo ""
37
+ echo "Options:"
38
+ echo " --purge 同时删除所有数据(配置、记忆、Agent、虚拟环境等)"
39
+ echo " --force 跳过确认提示"
40
+ echo " --dry-run 预览卸载过程,不实际执行"
41
+ echo ""
42
+ echo "默认只卸载 npm 包,保留数据和虚拟环境。"
43
+ echo "使用 --purge 可清除所有 MyAgent 相关文件。"
44
+ exit 0
45
+ ;;
46
+ *) echo "Unknown option: $arg (use --help)"; exit 2 ;;
47
+ esac
48
+ done
49
+
50
+ info() { echo -e "${INFO}[i]${NC} $*"; }
51
+ success() { echo -e "${SUCCESS}[✓]${NC} $*"; }
52
+ warn() { echo -e "${WARN}[!]${NC} $*"; }
53
+ err() { echo -e "${ERROR}[✗]${NC} $*" >&2; }
54
+ step() { echo -e "${ACCENT}[*]${NC} $*"; }
55
+
56
+ echo ""
57
+ echo -e " ${BOLD}${ACCENT}MyAgent${NC} Uninstaller"
58
+ echo ""
59
+
60
+ if $DRY_RUN; then
61
+ success "Dry run mode"
62
+ info "Will: stop process -> npm uninstall -> optionally purge data"
63
+ exit 0
64
+ fi
65
+
66
+ # ── Step 1: 停止运行中的 MyAgent 进程 ──────────────
67
+ step "Stopping MyAgent processes..."
68
+
69
+ stopped=false
70
+
71
+ # 尝试通过 API 优雅关闭
72
+ if curl -sf http://127.0.0.1:8767/api/shutdown -X POST >/dev/null 2>&1; then
73
+ success "MyAgent 服务已优雅关闭"
74
+ stopped=true
75
+ sleep 2
76
+ fi
77
+
78
+ # 检查并终止残留进程
79
+ pids=$(pgrep -f "myagent" 2>/dev/null || true)
80
+ if [ -n "$pids" ]; then
81
+ info "发现残留进程: $pids"
82
+ for pid in $pids; do
83
+ kill "$pid" 2>/dev/null || true
84
+ done
85
+ sleep 1
86
+ # 强制终止
87
+ pids=$(pgrep -f "myagent" 2>/dev/null || true)
88
+ if [ -n "$pids" ]; then
89
+ warn "部分进程需要强制终止"
90
+ kill -9 $pids 2>/dev/null || true
91
+ fi
92
+ success "MyAgent 进程已停止"
93
+ else
94
+ success "没有运行中的 MyAgent 进程"
95
+ fi
96
+
97
+ # ── Step 2: 卸载 npm 全局包 ─────────────────────
98
+ step "Uninstalling $PKG_NAME from npm global..."
99
+
100
+ if npm list -g "$PKG_NAME" >/dev/null 2>&1; then
101
+ npm uninstall -g "$PKG_NAME"
102
+ success "$PKG_NAME 已从 npm 全局卸载"
103
+ else
104
+ info "$PKG_NAME 未通过 npm 全局安装(可能是源码方式安装)"
105
+ fi
106
+
107
+ # ── Step 3: 清理可选数据(--purge) ─────────────
108
+ if $PURGE; then
109
+ echo ""
110
+ step "清除所有 MyAgent 数据..."
111
+
112
+ if [ -d "$DATA_DIR" ]; then
113
+ info "数据目录: $DATA_DIR"
114
+
115
+ # 统计大小
116
+ data_size=$(du -sh "$DATA_DIR" 2>/dev/null | cut -f1)
117
+ info "数据大小: $data_size"
118
+
119
+ rm -rf "$DATA_DIR"
120
+ success "数据目录已删除: $DATA_DIR ($data_size)"
121
+ else
122
+ info "数据目录不存在: $DATA_DIR"
123
+ fi
124
+
125
+ # 清理可能的日志文件
126
+ for log_file in \
127
+ "$HOME/.myagent.log" \
128
+ "$HOME/myagent.log" \
129
+ "/tmp/myagent-"*; do
130
+ if [ -e "$log_file" ]; then
131
+ rm -f "$log_file"
132
+ info "已删除日志: $log_file"
133
+ done
134
+ 2>/dev/null || true
135
+
136
+ success "所有数据已清除"
137
+ else
138
+ echo ""
139
+ info "保留数据和虚拟环境: $DATA_DIR"
140
+ info "如需彻底清除,请使用: curl -fsSL $SCRIPT_URL | bash -s -- --purge"
141
+ info "或手动删除: rm -rf $DATA_DIR"
142
+ fi
143
+
144
+ echo ""
145
+ echo -e " ${BOLD}${SUCCESS}卸载完成!${NC}"
146
+ echo ""
147
+ info "如需重新安装: npm install -g $PKG_NAME"
148
+ info "或使用一键安装: curl -fsSL https://raw.githubusercontent.com/ctz168/myagent/main/install/install.sh | bash"
149
+ echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/setup.py CHANGED
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  _version_path = Path(__file__).parent / "core" / "version.py"
10
10
  _version_vars = {}
11
11
  exec(_version_path.read_text(), _version_vars)
12
- __version__ = _version_vars.get("BASE_VERSION", "1.5.1")
12
+ __version__ = _version_vars.get("BASE_VERSION", "1.5.3")
13
13
 
14
14
  setup(
15
15
  name="myagent",
package/start.js CHANGED
@@ -16,6 +16,7 @@
16
16
  * myagent-ai server # API 服务模式
17
17
  * myagent-ai setup # 配置向导
18
18
  * myagent-ai reinstall # 重新安装依赖到 venv
19
+ * myagent-ai uninstall # 卸载 MyAgent(删除npm包+数据)
19
20
  */
20
21
  "use strict";
21
22
 
@@ -239,8 +240,8 @@ function installDeps(venvPython, venvPip, pkgDir) {
239
240
  let missing = [];
240
241
  console.log(" \x1b[36m[*]\x1b[0m 检查依赖...");
241
242
 
242
- // 只检查前 20 个,避免启动太慢(关键依赖都在前面)
243
- const checkLimit = Math.min(allModules.length, 20);
243
+ // 检查所有模块(不超过 30 个),托盘和 GUI 依赖也必须检测
244
+ const checkLimit = Math.min(allModules.length, 30);
244
245
  for (let i = 0; i < checkLimit; i++) {
245
246
  const mod = allModules[i];
246
247
  try {
@@ -253,7 +254,7 @@ function installDeps(venvPython, venvPip, pkgDir) {
253
254
  }
254
255
 
255
256
  if (missing.length === 0) {
256
- console.log(` \x1b[32m[✓]\x1b[0m ${checkLimit} 个依赖检查通过`);
257
+ console.log(` \x1b[32m[✓]\x1b[0m 全部 ${checkLimit} 个依赖检查通过`);
257
258
  return;
258
259
  }
259
260
 
@@ -277,8 +278,9 @@ function installDeps(venvPython, venvPip, pkgDir) {
277
278
  });
278
279
  console.log(" \x1b[32m[✓]\x1b[0m 依赖安装完成");
279
280
  installed = true;
280
- } catch (_) {
281
+ } catch (err) {
281
282
  console.log(" \x1b[33m[!]\x1b[0m requirements.txt 安装失败,尝试直接安装核心依赖...");
283
+ console.log(` \x1b[90m[i]\x1b[0m 错误: ${err.message}`);
282
284
  }
283
285
  }
284
286
 
@@ -293,6 +295,9 @@ function installDeps(venvPython, venvPip, pkgDir) {
293
295
  } catch (_) {
294
296
  console.error(" \x1b[31m[✗]\x1b[0m 依赖安装失败,请手动运行:");
295
297
  console.error(` ${venvPython} -m pip install -r "${reqFile}"`);
298
+ console.error("");
299
+ console.error(" 或使用重新安装命令: myagent-ai reinstall");
300
+ console.error("");
296
301
  }
297
302
  }
298
303
  }
@@ -308,6 +313,8 @@ function buildArgs(userArgs) {
308
313
  case "setup": return ["main.py", "--setup"];
309
314
  case "reinstall":
310
315
  return []; // 特殊处理
316
+ case "uninstall":
317
+ return []; // 特殊处理
311
318
  default:
312
319
  return ["main.py", ...userArgs];
313
320
  }
@@ -325,6 +332,71 @@ function main() {
325
332
  process.exit(1);
326
333
  }
327
334
 
335
+ // 特殊命令: uninstall
336
+ if (userArgs[0] === "uninstall") {
337
+ const venvDir = getVenvDir();
338
+ const purgeArg = userArgs.includes("--purge") || userArgs.includes("-p");
339
+ const dataDir = getDataDir();
340
+
341
+ // Step 1: 停止服务
342
+ console.log(" \x1b[36m[*]\x1b[0m 停止 MyAgent 服务...");
343
+ try {
344
+ const http = require("http");
345
+ const req = http.request({ hostname: "127.0.0.1", port: 8767, path: "/api/shutdown", method: "POST", timeout: 5000 }, () => {
346
+ // 忽略响应
347
+ });
348
+ req.on("error", () => {});
349
+ req.end();
350
+ console.log(" \x1b[32m[✓]\x1b[0m 已发送关闭请求");
351
+ setTimeout(() => {}, 2000);
352
+ } catch (_) {
353
+ console.log(" \x1b[90m[i]\x1b[0m 服务未运行");
354
+ }
355
+
356
+ // Step 2: 终止 Python/Node 进程
357
+ try {
358
+ if (IS_WIN) {
359
+ execSync("taskkill /F /IM python.exe /FI \"WINDOWTITLE eq myagent*\"", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 });
360
+ } else {
361
+ execSync("pkill -f 'myagent|main.py' 2>/dev/null; sleep 1", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000 });
362
+ }
363
+ } catch (_) {}
364
+ console.log(" \x1b[32m[✓]\x1b[0m 进程已停止");
365
+
366
+ // Step 3: npm uninstall
367
+ console.log(" \x1b[36m[*]\x1b[0m 卸载 npm 全局包...");
368
+ try {
369
+ execFileSync("npm", ["uninstall", "-g", PKG_NAME], {
370
+ encoding: "utf8", stdio: "inherit", timeout: 60000,
371
+ });
372
+ console.log(" \x1b[32m[✓]\x1b[0m npm 全局包已卸载");
373
+ } catch (_) {
374
+ console.log(" \x1b[90m[i]\x1b[0m npm 全局包未找到");
375
+ }
376
+
377
+ // Step 4: 清理数据(--purge)
378
+ if (purgeArg) {
379
+ console.log("");
380
+ console.log(" \x1b[36m[*]\x1b[0m 清除所有数据...");
381
+ if (fs.existsSync(dataDir)) {
382
+ fs.rmSync(dataDir, { recursive: true, force: true });
383
+ console.log(" \x1b[32m[✓]\x1b[0m 数据目录已删除: " + dataDir);
384
+ } else {
385
+ console.log(" \x1b[90m[i]\x1b[0m 数据目录不存在");
386
+ }
387
+ } else {
388
+ console.log("");
389
+ console.log(" \x1b[90m[i]\x1b[0m 保留数据目录: " + dataDir);
390
+ console.log(" \x1b[90m[i]\x1b[0m 如需彻底清除: myagent-ai uninstall --purge");
391
+ }
392
+
393
+ console.log("");
394
+ console.log(" \x1b[32m[✓]\x1b[0m 卸载完成!");
395
+ console.log(" \x1b[90m[i]\x1b[0m 重新安装: npm install -g myagent-ai");
396
+ console.log("");
397
+ return;
398
+ }
399
+
328
400
  // 特殊命令: reinstall
329
401
  if (userArgs[0] === "reinstall") {
330
402
  const venvDir = getVenvDir();
package/web/api_server.py CHANGED
@@ -20,11 +20,68 @@ logger = get_logger("myagent.api")
20
20
  def _safe_load_json(filepath, default=None):
21
21
  """安全读取 JSON 文件,解析失败返回默认值"""
22
22
  try:
23
- return json.loads(filepath.read_text())
23
+ return json.loads(filepath.read_text(encoding="utf-8"))
24
24
  except (json.JSONDecodeError, ValueError, OSError):
25
25
  logger.warning(f"JSON 文件读取/解析失败: {filepath}")
26
26
  return default if default is not None else {}
27
27
 
28
+ # 允许上传的文件扩展名
29
+ _KB_ALLOWED_EXTENSIONS = {".md", ".txt", ".json", ".csv", ".py", ".js", ".html", ".htm",
30
+ ".xml", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf",
31
+ ".sh", ".bat", ".ps1", ".log", ".sql", ".r", ".java", ".cpp",
32
+ ".c", ".h", ".go", ".rs", ".ts", ".tsx", ".jsx", ".vue", ".svelte"}
33
+
34
+ async def _read_multipart_files(request):
35
+ """从 multipart/form-data 请求中读取文件列表,返回 [{"name": str, "content": str}]"""
36
+ files = []
37
+ SUPPORTED_EXTENSIONS = _KB_ALLOWED_EXTENSIONS
38
+ try:
39
+ reader = await request.multipart()
40
+ while True:
41
+ field = await reader.next()
42
+ if field is None:
43
+ break
44
+ if field.name == "files":
45
+ # 文件字段
46
+ filename = field.filename or "unknown"
47
+ # 使用 webkitRelativePath 作为文件名(文件夹上传时浏览器会设置此属性)
48
+ rel_path = field.headers.get("X-File-Path", "")
49
+ if rel_path:
50
+ filename = rel_path
51
+ # 安全校验
52
+ if ".." in filename or filename.startswith("/"):
53
+ continue
54
+ ext = Path(filename).suffix.lower()
55
+ if ext and ext not in SUPPORTED_EXTENSIONS:
56
+ continue
57
+ # 读取文件内容
58
+ try:
59
+ chunks = []
60
+ while True:
61
+ chunk = await field.read_chunk()
62
+ if not chunk:
63
+ break
64
+ chunks.append(chunk)
65
+ content = b"".join(chunks).decode("utf-8", errors="replace")
66
+ files.append({"name": filename, "content": content})
67
+ except Exception as e:
68
+ logger.warning(f"读取上传文件失败 {filename}: {e}")
69
+ except Exception as e:
70
+ # 如果不是 multipart 请求,尝试 JSON 格式
71
+ logger.debug(f"Multipart 读取失败,尝试 JSON: {e}")
72
+ try:
73
+ data = await request.json()
74
+ raw_files = data.get("files", [])
75
+ if not raw_files:
76
+ fname = data.get("filename", data.get("name", ""))
77
+ fcontent = data.get("content", "")
78
+ if fname and fcontent is not None:
79
+ raw_files = [{"name": fname, "content": fcontent}]
80
+ return raw_files
81
+ except Exception:
82
+ pass
83
+ return files
84
+
28
85
 
29
86
 
30
87
  CONFIG_HELPER_PROMPT = """你是 MyAgent 的智能配置助手,专门帮助用户完成初始配置和日常配置管理。你的名字叫"配置助手"。
@@ -541,6 +598,10 @@ class ApiServer:
541
598
  def _ensure_default_agent(self):
542
599
  """确保默认 agent 存在(名为「全权Agent」,目录名保持 default)"""
543
600
  ad = self._agent_dir("default")
601
+ # 先尝试迁移(会自动修复损坏的 config.json)
602
+ if (ad / "config.json").exists():
603
+ self._migrate_agent_config("default", "全权Agent")
604
+ # 迁移后再次检查(损坏文件可能已被删除)
544
605
  if not (ad / "config.json").exists():
545
606
  ad.mkdir(parents=True, exist_ok=True)
546
607
  now = datetime.datetime.now().isoformat()
@@ -557,29 +618,49 @@ class ApiServer:
557
618
  "updated_at": now,
558
619
  "system_prompt": "你是 MyAgent 默认助手,运行在本机模式。请用友好、专业的方式回答用户的问题。",
559
620
  }
560
- (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
621
+ (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8")
561
622
  for fn, default in [
562
623
  ("soul.md", "# 全权Agent\n\n## 性格\n专业、友好的AI助手\n"),
563
624
  ("identity.md", "# 全权Agent\n\n## 身份\nMyAgent 默认AI助手\n"),
564
625
  ("user.md", "# 用户信息\n\n## 用户偏好\n<!-- 在此处记录用户偏好 -->\n"),
565
626
  ]:
566
627
  if not (ad / fn).exists():
567
- (ad / fn).write_text(default)
628
+ (ad / fn).write_text(default, encoding="utf-8")
568
629
  logger.info("已创建默认 Agent (全权Agent)")
569
- else:
570
- # 迁移: 确保旧 default agent 有新字段
571
- self._migrate_agent_config("default", "全权Agent")
572
630
  self._ensure_config_helper()
573
631
 
574
632
  def _migrate_agent_config(self, path: str, intended_name: str = None):
575
- """为旧 agent config 添加缺失的 id, created_at, updated_at 字段"""
633
+ """为旧 agent config 添加缺失的 id, created_at, updated_at 字段;JSON 损坏则自动重建"""
576
634
  cfg_file = self._agent_dir(path) / "config.json"
577
635
  if not cfg_file.exists():
578
636
  return
579
637
  try:
580
- cfg = json.loads(cfg_file.read_text())
638
+ cfg = json.loads(cfg_file.read_text(encoding="utf-8"))
581
639
  except (json.JSONDecodeError, ValueError):
582
- logger.warning(f"Agent config JSON 解析失败,跳过迁移: {path}")
640
+ logger.warning(f"Agent config JSON 解析失败,自动删除重建: {path}")
641
+ try:
642
+ cfg_file.unlink()
643
+ except OSError:
644
+ pass
645
+ # 如果是 default 或配置助手,由 _ensure_default_agent / _ensure_config_helper 重建
646
+ if path in ("default", "配置助手", "p"):
647
+ return
648
+ # 其他 agent:创建一个最小 config
649
+ ad = self._agent_dir(path)
650
+ if ad.exists():
651
+ now = datetime.datetime.now().isoformat()
652
+ cfg = {
653
+ "id": uuid.uuid4().hex[:12],
654
+ "name": path,
655
+ "description": "",
656
+ "avatar_emoji": "🤖",
657
+ "execution_mode": "sandbox",
658
+ "enabled": True,
659
+ "created_at": now,
660
+ "updated_at": now,
661
+ }
662
+ cfg_file.write_text(json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8")
663
+ logger.info(f"已重建 Agent 配置: {path}")
583
664
  return
584
665
  changed = False
585
666
  now = datetime.datetime.now().isoformat()
@@ -597,11 +678,15 @@ class ApiServer:
597
678
  cfg["name"] = intended_name
598
679
  changed = True
599
680
  if changed:
600
- cfg_file.write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
681
+ cfg_file.write_text(json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8")
601
682
 
602
683
  def _ensure_config_helper(self):
603
684
  """确保系统级「配置助手」agent 存在(不可删除、不可改名、核心字段不可修改)"""
604
685
  ad = self._agent_dir("配置助手")
686
+ # 先尝试迁移(会自动修复损坏的 config.json)
687
+ if (ad / "config.json").exists():
688
+ self._migrate_agent_config("配置助手")
689
+ # 迁移后再次检查(损坏文件可能已被删除)
605
690
  if not (ad / "config.json").exists():
606
691
  ad.mkdir(parents=True, exist_ok=True)
607
692
  now = datetime.datetime.now().isoformat()
@@ -618,14 +703,14 @@ class ApiServer:
618
703
  "updated_at": now,
619
704
  "system_prompt": CONFIG_HELPER_PROMPT,
620
705
  }
621
- (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
706
+ (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8")
622
707
  for fn, default in [
623
708
  ("soul.md", "# 配置助手\n\n## 性格\n专业、友好的配置助手\n"),
624
709
  ("identity.md", "# 配置助手\n\n## 身份\nMyAgent 内置智能配置助手\n"),
625
710
  ("user.md", "# 用户信息\n\n## 用户偏好\n<!-- 在此处记录用户偏好 -->\n"),
626
711
  ]:
627
712
  if not (ad / fn).exists():
628
- (ad / fn).write_text(default)
713
+ (ad / fn).write_text(default, encoding="utf-8")
629
714
  logger.info("已创建系统级配置助手 Agent")
630
715
  # 创建快捷方式 p -> 配置助手(符号链接)
631
716
  p_dir = self._agent_dir("p")
@@ -645,12 +730,15 @@ class ApiServer:
645
730
  kb_dir.mkdir(parents=True, exist_ok=True)
646
731
  target = kb_dir / "配置使用说明.md"
647
732
 
648
- # 查找源文件(优先级:项目 docs/ > download/ > 当前目录)
733
+ # 查找源文件
734
+ # __file__ = .../myagent-ai/web/api_server.py
735
+ # parent.parent = .../myagent-ai (包根目录,docs/ 在这里)
736
+ pkg_root = Path(__file__).resolve().parent.parent
649
737
  source_candidates = [
650
- Path(__file__).resolve().parent.parent.parent / "docs" / "配置使用说明.md",
651
- Path(__file__).resolve().parent.parent.parent / "download" / "配置使用说明.md",
738
+ pkg_root / "docs" / "配置使用说明.md",
739
+ pkg_root.parent / "docs" / "配置使用说明.md",
652
740
  ]
653
- # 如果通过 npm 安装,docs 可能在包目录下
741
+ # npm 全局安装时也尝试通过 import 定位
654
742
  try:
655
743
  import myagent
656
744
  pkg_dir = Path(myagent.__file__).resolve().parent
@@ -674,7 +762,7 @@ class ApiServer:
674
762
  if hasattr(self, '_agent_rags') and "配置助手" in self._agent_rags:
675
763
  self._agent_rags["配置助手"].build_index()
676
764
  else:
677
- logger.warning("未找到 配置使用说明.md 源文件,配置助手知识库未绑定")
765
+ logger.warning(f"未找到 配置使用说明.md 源文件,已搜索: {[str(c) for c in source_candidates]}")
678
766
 
679
767
  def _read_agent_config(self, path: str) -> dict | None:
680
768
  """读取 agent 配置"""
@@ -682,7 +770,7 @@ class ApiServer:
682
770
  if not cfg_file.exists():
683
771
  return None
684
772
  try:
685
- return json.loads(cfg_file.read_text())
773
+ return json.loads(cfg_file.read_text(encoding="utf-8"))
686
774
  except (json.JSONDecodeError, ValueError):
687
775
  logger.warning(f"Agent config JSON 解析失败: {path}")
688
776
  return None
@@ -691,7 +779,7 @@ class ApiServer:
691
779
  """写入 agent 配置"""
692
780
  ad = self._agent_dir(path)
693
781
  ad.mkdir(parents=True, exist_ok=True)
694
- (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
782
+ (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8")
695
783
 
696
784
  def _deep_merge(self, base: dict, override: dict) -> None:
697
785
  """深度合并 override 到 base"""
@@ -756,12 +844,22 @@ class ApiServer:
756
844
  continue
757
845
  agent_path = f"{prefix}{d.name}" if not prefix else f"{prefix}/{d.name}"
758
846
  # 迁移: 为缺少 id/created_at/updated_at 的旧 agent 补全字段
759
- self._migrate_agent_config(agent_path)
760
- try:
761
- cfg = json.loads(cfg_file.read_text())
762
- except (json.JSONDecodeError, ValueError):
763
- logger.warning(f"Agent config JSON 解析失败,跳过: {agent_path}")
764
- continue
847
+ # 跳过符号链接目录(如 p -> 配置助手),避免重复处理
848
+ if d.is_symlink():
849
+ try:
850
+ cfg = json.loads(cfg_file.read_text(encoding="utf-8"))
851
+ except (json.JSONDecodeError, ValueError):
852
+ continue
853
+ else:
854
+ self._migrate_agent_config(agent_path)
855
+ # 迁移可能删除了损坏的文件
856
+ if not cfg_file.exists():
857
+ continue
858
+ try:
859
+ cfg = json.loads(cfg_file.read_text(encoding="utf-8"))
860
+ except (json.JSONDecodeError, ValueError):
861
+ logger.warning(f"Agent config JSON 解析失败,跳过: {agent_path}")
862
+ continue
765
863
  agent = {"path": agent_path, "name": d.name, **cfg}
766
864
  agent["avatar_color"] = cfg.get("avatar_color") or _agent_color(d.name)
767
865
  agent["depth"] = agent_path.count("/")
@@ -873,14 +971,14 @@ class ApiServer:
873
971
  cfg["work_dir"] = data["work_dir"]
874
972
 
875
973
  ad.mkdir(parents=True, exist_ok=True)
876
- (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
974
+ (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8")
877
975
  for fn, default in [
878
976
  ("soul.md", f"# {name}\n\n## 性格\n专业AI助手\n"),
879
977
  ("identity.md", f"# {name}\n\n## 身份\nAI助手\n"),
880
978
  ("user.md", f"# {name} 用户信息\n\n## 用户偏好\n<!-- 在此处记录用户偏好 -->\n"),
881
979
  ]:
882
980
  if not (ad / fn).exists():
883
- (ad / fn).write_text(default)
981
+ (ad / fn).write_text(default, encoding="utf-8")
884
982
 
885
983
  logger.info(f"创建 Agent: {name} (sandbox模式)")
886
984
  return web.json_response({"ok": True, "path": name, "name": name, "avatar_color": cfg["avatar_color"]})
@@ -933,14 +1031,14 @@ class ApiServer:
933
1031
  cfg["backup_model_ids"] = [x for x in data["backup_model_ids"] if isinstance(x, str) and x.strip()]
934
1032
 
935
1033
  ad.mkdir(parents=True, exist_ok=True)
936
- (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
1034
+ (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8")
937
1035
  for fn, default in [
938
1036
  ("soul.md", f"# {name}\n\n## 上级: {parent_path}\n## 性格\n专业AI助手\n"),
939
1037
  ("identity.md", f"# {name}\n\n## 身份\n{parent_path} 的子 Agent\n"),
940
1038
  ("user.md", f"# {name} 用户信息\n\n## 用户偏好\n<!-- 在此处记录用户偏好 -->\n"),
941
1039
  ]:
942
1040
  if not (ad / fn).exists():
943
- (ad / fn).write_text(default)
1041
+ (ad / fn).write_text(default, encoding="utf-8")
944
1042
 
945
1043
  logger.info(f"创建子 Agent: {child_path} (sandbox模式)")
946
1044
  return web.json_response({"ok": True, "path": child_path, "name": name, "parent": parent_path, "avatar_color": cfg["avatar_color"]})
@@ -957,7 +1055,7 @@ class ApiServer:
957
1055
  for d in sorted(parent_dir.iterdir()):
958
1056
  if d.is_dir() and (d / "config.json").exists():
959
1057
  try:
960
- cfg = json.loads((d / "config.json").read_text())
1058
+ cfg = json.loads((d / "config.json").read_text(encoding="utf-8"))
961
1059
  except (json.JSONDecodeError, ValueError):
962
1060
  logger.warning(f"Agent config JSON 解析失败,跳过: {parent_path}/{d.name}")
963
1061
  continue
@@ -975,12 +1073,12 @@ class ApiServer:
975
1073
  if not (ad / "config.json").exists():
976
1074
  return web.json_response({"error": "not found"}, status=404)
977
1075
  try:
978
- cfg = json.loads((ad / "config.json").read_text())
1076
+ cfg = json.loads((ad / "config.json").read_text(encoding="utf-8"))
979
1077
  except (json.JSONDecodeError, ValueError):
980
1078
  return web.json_response({"error": "config.json 解析失败"}, status=500)
981
- soul = (ad / "soul.md").read_text() if (ad / "soul.md").exists() else ""
982
- identity = (ad / "identity.md").read_text() if (ad / "identity.md").exists() else ""
983
- user = (ad / "user.md").read_text() if (ad / "user.md").exists() else ""
1079
+ soul = (ad / "soul.md").read_text(encoding="utf-8") if (ad / "soul.md").exists() else ""
1080
+ identity = (ad / "identity.md").read_text(encoding="utf-8") if (ad / "identity.md").exists() else ""
1081
+ user = (ad / "user.md").read_text(encoding="utf-8") if (ad / "user.md").exists() else ""
984
1082
  # 列出子 agent
985
1083
  children = []
986
1084
  for d in sorted(ad.iterdir()):
@@ -1014,7 +1112,7 @@ class ApiServer:
1014
1112
  if not (ad / "config.json").exists():
1015
1113
  return web.json_response({"error": "not found"}, status=404)
1016
1114
  try:
1017
- cfg = json.loads((ad / "config.json").read_text())
1115
+ cfg = json.loads((ad / "config.json").read_text(encoding="utf-8"))
1018
1116
  except (json.JSONDecodeError, ValueError):
1019
1117
  return web.json_response({"error": "config.json 解析失败"}, status=500)
1020
1118
 
@@ -1052,10 +1150,10 @@ class ApiServer:
1052
1150
  del data["system"]
1053
1151
  # 自动更新 updated_at
1054
1152
  cfg["updated_at"] = datetime.datetime.now().isoformat()
1055
- (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
1056
- if "soul" in data and not is_system: (ad / "soul.md").write_text(data["soul"])
1057
- if "identity" in data and not is_system: (ad / "identity.md").write_text(data["identity"])
1058
- if "user" in data: (ad / "user.md").write_text(data["user"])
1153
+ (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8")
1154
+ if "soul" in data and not is_system: (ad / "soul.md").write_text(data["soul"], encoding="utf-8")
1155
+ if "identity" in data and not is_system: (ad / "identity.md").write_text(data["identity"], encoding="utf-8")
1156
+ if "user" in data: (ad / "user.md").write_text(data["user"], encoding="utf-8")
1059
1157
  logger.info(f"更新 Agent: {path}")
1060
1158
  return web.json_response({"ok": True})
1061
1159
 
@@ -1089,12 +1187,12 @@ class ApiServer:
1089
1187
  p = self._agent_dir(path) / "soul.md"
1090
1188
  if not p.parent.exists() or not (p.parent / "config.json").exists():
1091
1189
  return web.json_response({"error": "not found"}, status=404)
1092
- return web.json_response({"soul": p.read_text() if p.exists() else ""})
1190
+ return web.json_response({"soul": p.read_text(encoding="utf-8") if p.exists() else ""})
1093
1191
 
1094
1192
  async def handle_set_soul(self, request):
1095
1193
  data = await request.json(); ad = self._agent_dir(request.match_info["name"])
1096
1194
  ad.mkdir(parents=True, exist_ok=True)
1097
- (ad / "soul.md").write_text(data.get("soul", ""))
1195
+ (ad / "soul.md").write_text(data.get("soul", ""), encoding="utf-8")
1098
1196
  return web.json_response({"ok": True})
1099
1197
 
1100
1198
  async def handle_get_identity(self, request):
@@ -1102,12 +1200,12 @@ class ApiServer:
1102
1200
  p = self._agent_dir(path) / "identity.md"
1103
1201
  if not p.parent.exists() or not (p.parent / "config.json").exists():
1104
1202
  return web.json_response({"error": "not found"}, status=404)
1105
- return web.json_response({"identity": p.read_text() if p.exists() else ""})
1203
+ return web.json_response({"identity": p.read_text(encoding="utf-8") if p.exists() else ""})
1106
1204
 
1107
1205
  async def handle_set_identity(self, request):
1108
1206
  data = await request.json(); ad = self._agent_dir(request.match_info["name"])
1109
1207
  ad.mkdir(parents=True, exist_ok=True)
1110
- (ad / "identity.md").write_text(data.get("identity", ""))
1208
+ (ad / "identity.md").write_text(data.get("identity", ""), encoding="utf-8")
1111
1209
  return web.json_response({"ok": True})
1112
1210
 
1113
1211
  async def handle_get_user(self, request):
@@ -1115,12 +1213,12 @@ class ApiServer:
1115
1213
  p = self._agent_dir(path) / "user.md"
1116
1214
  if not p.parent.exists() or not (p.parent / "config.json").exists():
1117
1215
  return web.json_response({"error": "not found"}, status=404)
1118
- return web.json_response({"user": p.read_text() if p.exists() else ""})
1216
+ return web.json_response({"user": p.read_text(encoding="utf-8") if p.exists() else ""})
1119
1217
 
1120
1218
  async def handle_set_user(self, request):
1121
1219
  data = await request.json(); ad = self._agent_dir(request.match_info["name"])
1122
1220
  ad.mkdir(parents=True, exist_ok=True)
1123
- (ad / "user.md").write_text(data.get("user", ""))
1221
+ (ad / "user.md").write_text(data.get("user", ""), encoding="utf-8")
1124
1222
  return web.json_response({"ok": True})
1125
1223
 
1126
1224
  # --- Executor ---
@@ -1143,7 +1241,7 @@ class ApiServer:
1143
1241
  "sandbox_image", "sandbox_network", "sandbox_memory"):
1144
1242
  if k in data:
1145
1243
  exe[k] = data[k]
1146
- cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False))
1244
+ cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False), encoding="utf-8")
1147
1245
  # 热更新内存配置
1148
1246
  update_keys = {k: v for k, v in data.items() if hasattr(self.core.config_mgr.config.executor, k)}
1149
1247
  self.core.config_mgr.update_executor(**update_keys)
@@ -1370,7 +1468,7 @@ class ApiServer:
1370
1468
  agent_info = {"name": name, "avatar_color": _agent_color(name)}
1371
1469
  if (ad / "config.json").exists():
1372
1470
  try:
1373
- agent_info.update(json.loads((ad / "config.json").read_text()))
1471
+ agent_info.update(json.loads((ad / "config.json").read_text(encoding="utf-8")))
1374
1472
  except (json.JSONDecodeError, ValueError):
1375
1473
  pass
1376
1474
  return web.json_response({**agent_info, "sessions": sessions})
@@ -1427,7 +1525,7 @@ class ApiServer:
1427
1525
  for k in ("provider", "model", "base_url", "temperature", "max_tokens", "timeout", "max_retries"):
1428
1526
  if k in data: llm[k] = data[k]
1429
1527
  if data.get("api_key"): llm["api_key"] = data["api_key"]
1430
- cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False))
1528
+ cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False), encoding="utf-8")
1431
1529
  # 2. 热更新内存中的配置
1432
1530
  update_keys = {k: v for k, v in data.items() if k != "api_key" or data.get("api_key")}
1433
1531
  self.core.config_mgr.update_llm(**update_keys)
@@ -1447,8 +1545,41 @@ class ApiServer:
1447
1545
  return web.json_response({"ok": True, "hot_reload": True})
1448
1546
 
1449
1547
  async def handle_test_llm(self, request):
1548
+ """POST /api/llm/test - 测试 LLM 连接,支持使用请求体中的临时配置"""
1450
1549
  try:
1451
- msg = await self.core.llm.chat([Message(role="user", content="Hi, reply OK")])
1550
+ # 尝试从请求体读取临时配置
1551
+ temp_client = None
1552
+ try:
1553
+ data = await request.json()
1554
+ api_key = data.get("api_key", "")
1555
+ base_url = data.get("base_url", "")
1556
+ model = data.get("model", "")
1557
+ if api_key or base_url or model:
1558
+ # 使用请求体中的配置创建临时客户端测试
1559
+ from core.llm import LLMClient
1560
+ # 推断 provider
1561
+ provider = "openai"
1562
+ if "zhipu" in base_url or "bigmodel" in base_url:
1563
+ provider = "zhipu"
1564
+ elif "anthropic" in base_url:
1565
+ provider = "anthropic"
1566
+ elif "ollama" in base_url or "localhost" in base_url or "127.0.0.1" in base_url:
1567
+ provider = "ollama"
1568
+ temp_client = LLMClient(
1569
+ provider=provider,
1570
+ api_key=api_key or self.core.llm.api_key if self.core.llm else "",
1571
+ base_url=base_url or self.core.llm.base_url if self.core.llm else "",
1572
+ model=model or self.core.llm.model if self.core.llm else "gpt-4",
1573
+ timeout=30,
1574
+ max_retries=1,
1575
+ )
1576
+ except Exception:
1577
+ pass # 如果没有请求体或解析失败,使用当前配置
1578
+
1579
+ client = temp_client or self.core.llm
1580
+ if not client:
1581
+ return web.json_response({"ok": False, "error": "没有配置 LLM 客户端"})
1582
+ msg = await client.chat([Message(role="user", content="Hi, reply OK")])
1452
1583
  return web.json_response({"ok": True, "response": msg.content[:100] if msg.content else ""})
1453
1584
  except Exception as e:
1454
1585
  return web.json_response({"ok": False, "error": str(e)})
@@ -1609,7 +1740,7 @@ class ApiServer:
1609
1740
  cfg_path = self.core.config_mgr._config_file
1610
1741
  cfg_data = _safe_load_json(cfg_path) if cfg_path.exists() else {}
1611
1742
  cfg_data["disabled_skills"] = disabled
1612
- cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False))
1743
+ cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False), encoding="utf-8")
1613
1744
 
1614
1745
  def _load_disabled_skills(self):
1615
1746
  """从配置文件加载禁用技能列表"""
@@ -1632,7 +1763,7 @@ class ApiServer:
1632
1763
  cfg_path = self.core.config_mgr._config_file
1633
1764
  cfg_data = _safe_load_json(cfg_path) if cfg_path.exists() else {}
1634
1765
  cfg_data["workspace"] = path
1635
- cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False))
1766
+ cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False), encoding="utf-8")
1636
1767
  return web.json_response({"ok": True})
1637
1768
 
1638
1769
  async def handle_list_workdir(self, request):
@@ -1703,7 +1834,7 @@ class ApiServer:
1703
1834
  for k in ("enabled", "server_url", "max_friends", "auto_accept"):
1704
1835
  if k in data:
1705
1836
  comm[k] = data[k]
1706
- cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False))
1837
+ cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False), encoding="utf-8")
1707
1838
  # Hot-reload in memory
1708
1839
  if "enabled" in data:
1709
1840
  self.core.config.communication.enabled = data["enabled"]
@@ -2351,7 +2482,7 @@ class ApiServer:
2351
2482
  return web.json_response(files)
2352
2483
 
2353
2484
  async def handle_upload_org_knowledge(self, request):
2354
- """POST /api/organization/knowledge/upload - 上传文件到组织知识库"""
2485
+ """POST /api/organization/knowledge/upload - 上传文件到组织知识库(支持文件和文件夹上传)"""
2355
2486
  # 权限检查
2356
2487
  agent = request.query.get("agent", "default")
2357
2488
  if not self._check_org_admin(agent):
@@ -2360,16 +2491,9 @@ class ApiServer:
2360
2491
  status=403,
2361
2492
  )
2362
2493
 
2363
- data = await request.json()
2364
- # 支持单个文件或批量文件
2365
- files = data.get("files", [])
2494
+ files = await _read_multipart_files(request)
2366
2495
  if not files:
2367
- # 兼容单文件格式
2368
- filename = data.get("filename", data.get("name", ""))
2369
- content = data.get("content", "")
2370
- if not filename or content is None:
2371
- return web.json_response({"ok": False, "error": "缺少 filename 或 content"}, status=400)
2372
- files = [{"name": filename, "content": content}]
2496
+ return web.json_response({"ok": False, "error": "没有上传文件"}, status=400)
2373
2497
 
2374
2498
  org_mgr = self._get_org_manager()
2375
2499
  results = []
@@ -2455,16 +2579,11 @@ class ApiServer:
2455
2579
  return web.json_response(files)
2456
2580
 
2457
2581
  async def handle_upload_agent_knowledge(self, request):
2458
- """POST /api/agents/{name}/knowledge/upload - 上传到 Agent 知识库"""
2582
+ """POST /api/agents/{name}/knowledge/upload - 上传到 Agent 知识库(支持文件和文件夹上传)"""
2459
2583
  agent_path = request.match_info["name"]
2460
- data = await request.json()
2461
- files = data.get("files", [])
2584
+ files = await _read_multipart_files(request)
2462
2585
  if not files:
2463
- filename = data.get("filename", data.get("name", ""))
2464
- content = data.get("content", "")
2465
- if not filename or content is None:
2466
- return web.json_response({"ok": False, "error": "缺少 filename 或 content"}, status=400)
2467
- files = [{"name": filename, "content": content}]
2586
+ return web.json_response({"ok": False, "error": "没有上传文件"}, status=400)
2468
2587
 
2469
2588
  kb_dir = self._get_agent_knowledge_dir(agent_path)
2470
2589
  kb_dir.mkdir(parents=True, exist_ok=True)
@@ -2473,7 +2592,6 @@ class ApiServer:
2473
2592
  name = f.get("name", "")
2474
2593
  content = f.get("content", "")
2475
2594
  # 安全校验
2476
- safe_name = Path(name).name
2477
2595
  if ".." in name or name.startswith("/"):
2478
2596
  results.append({"ok": False, "message": "非法文件名"})
2479
2597
  continue
@@ -2483,7 +2601,7 @@ class ApiServer:
2483
2601
  parent.mkdir(parents=True, exist_ok=True)
2484
2602
  target = parent / parts[-1]
2485
2603
  else:
2486
- target = kb_dir / safe_name
2604
+ target = kb_dir / Path(name).name
2487
2605
  try:
2488
2606
  target.write_text(content, encoding="utf-8")
2489
2607
  results.append({"ok": True, "path": name})
@@ -3198,20 +3316,11 @@ class ApiServer:
3198
3316
  return web.json_response(files)
3199
3317
 
3200
3318
  async def handle_upload_dept_knowledge(self, request):
3201
- """POST /api/departments/{path}/knowledge/upload - 上传到部门知识库"""
3319
+ """POST /api/departments/{path}/knowledge/upload - 上传到部门知识库(支持文件和文件夹上传)"""
3202
3320
  path = request.match_info["path"]
3203
- try:
3204
- data = await request.json()
3205
- except Exception:
3206
- return web.json_response({"error": "invalid JSON"}, status=400)
3207
- files = data.get("files", [])
3321
+ files = await _read_multipart_files(request)
3208
3322
  if not files:
3209
- # 兼容单文件格式
3210
- filename = data.get("filename", data.get("name", ""))
3211
- content = data.get("content", "")
3212
- if not filename or content is None:
3213
- return web.json_response({"ok": False, "error": "缺少 filename 或 content"}, status=400)
3214
- files = [{"name": filename, "content": content}]
3323
+ return web.json_response({"ok": False, "error": "没有上传文件"}, status=400)
3215
3324
  dm = self._get_dept_manager()
3216
3325
  results = []
3217
3326
  for f in files:
package/web/ui/chat.html CHANGED
@@ -2753,11 +2753,13 @@ function toggleAgentPanel() {
2753
2753
  }
2754
2754
 
2755
2755
  // ── Agent Create/Edit Modal ──
2756
- function showCreateAgentModal() {
2756
+ async function showCreateAgentModal() {
2757
+ await loadModels();
2757
2758
  showAgentModal(null, null);
2758
2759
  }
2759
2760
 
2760
- function showCreateChildModal(parentPath) {
2761
+ async function showCreateChildModal(parentPath) {
2762
+ await loadModels();
2761
2763
  showAgentModal(null, parentPath);
2762
2764
  }
2763
2765
 
@@ -5264,7 +5266,7 @@ async function setupTestConnection() {
5264
5266
  method: 'POST',
5265
5267
  body: JSON.stringify({ api_key: apiKey, base_url: baseUrl, model: model })
5266
5268
  });
5267
- if (result.success) {
5269
+ if (result.ok) {
5268
5270
  statusEl.className = 'setup-status show success';
5269
5271
  statusEl.innerHTML = '✅ 连接成功!模型响应正常';
5270
5272
  } else {
package/web/ui/index.html CHANGED
@@ -217,7 +217,7 @@ async function renderDashboard(){
217
217
  // ========== Agents (FULL REWRITE) ==========
218
218
  async function renderAgents(){
219
219
  const [agents,models,depts]=await Promise.all([api('/api/agents'),api('/api/models'),api('/api/departments')]);
220
- allAgentsCache=agents||[];allModelsCache=models||[];allDeptsCache=Array.isArray(depts)?depts:(depts?.tree||[]);
220
+ allAgentsCache=Array.isArray(agents)?agents:[];allModelsCache=Array.isArray(models)?models:[];allDeptsCache=Array.isArray(depts)?depts:(depts?.tree||[]);
221
221
  const deptMap={};(function buildDeptMap(list,pfx){
222
222
  for(const d of list||[]){const path=pfx?(pfx+'/'+d.name):d.name;deptMap[path]={name:d.name,path};buildDeptMap(d.children||[],path)}
223
223
  })(allDeptsCache,'');
@@ -311,6 +311,7 @@ async function doCreateAgent(){
311
311
 
312
312
  // Edit Agent Modal
313
313
  async function openEditAgentModal(path){
314
+ window._currentEditAgentPath=path;
314
315
  const a=await api(`/api/agents/${encodeURIComponent(path)}`);
315
316
  if(a.error){showToast(a.error,'danger');return}
316
317
  const isSys=!!a.system;
@@ -432,7 +433,7 @@ async function loadAgentKB(){
432
433
  const files=Array.isArray(kb)?kb:(kb?.files||[]);
433
434
  let html=`<div class="flex justify-between items-center mb-16">
434
435
  <h4 style="font-size:14px;color:var(--text2)">知识库文件 (${files.length})</h4>
435
- <button class="btn btn-sm btn-primary" onclick="uploadAgentKB('${escHtml(path)}')">上传文件</button></div>`;
436
+ <button class="btn btn-sm btn-primary" onclick="uploadAgentKB('${escHtml(path)}',false)">上传文件</button> <button class="btn btn-sm btn-secondary" onclick="uploadAgentKB('${escHtml(path)}',true)">📁 上传文件夹</button></div>`;
436
437
  if(files.length===0){html+='<div class="empty">暂无知识库文件</div>';}
437
438
  else{html+='<table><tr><th>文件名</th><th>大小</th><th></th></tr>';
438
439
  for(const f of files){html+=`<tr><td>${escHtml(f.name||f.filename||'')}</td><td>${f.size||'-'}</td><td><button class="btn btn-sm btn-danger" onclick="deleteAgentKB('${escHtml(path)}','${escHtml(f.name||f.filename||'')}')">删除</button></td></tr>`}
@@ -440,7 +441,25 @@ async function loadAgentKB(){
440
441
  $('kbContent').innerHTML=html;
441
442
  }
442
443
 
443
- async function uploadAgentKB(path){showToast('请通过 API 上传文件','info')}
444
+ async function uploadAgentKB(path,folderMode){
445
+ const input=document.createElement('input');input.type='file';input.multiple=true;
446
+ if(folderMode){input.webkitdirectory=true;}
447
+ input.onchange=async()=>{
448
+ const fd=new FormData();
449
+ for(const f of input.files){
450
+ if(f.webkitRelativePath){fd.append('files',f,{headers:{'X-File-Path':f.webkitRelativePath}});}
451
+ else{fd.append('files',f);}
452
+ }
453
+ showToast('正在上传...','info');
454
+ const r=await fetch(API+`/api/agents/${encodeURIComponent(path)}/knowledge/upload`,{method:'POST',body:fd});
455
+ const data=await r.json();
456
+ if(data.error){showToast(data.error,'danger');return}
457
+ const total=data.results?data.results.length:0;
458
+ const okCount=data.results?data.results.filter(x=>x.ok).length:0;
459
+ showToast(`上传完成: ${okCount}/${total} 文件成功`,okCount===total?'success':'warning');
460
+ loadAgentKB(path);
461
+ };input.click();
462
+ }
444
463
  async function deleteAgentKB(path,filename){
445
464
  if(!confirm('确认删除 '+filename+'?'))return;
446
465
  await api(`/api/agents/${encodeURIComponent(path)}/knowledge?filename=${encodeURIComponent(filename)}`,{method:'DELETE'});
@@ -467,14 +486,6 @@ async function viewSessionMsgs(sid){
467
486
  $('sessionsContent').innerHTML=html;
468
487
  }
469
488
 
470
- // Monkey-patch openEditAgentModal to store path
471
- const _origOpenEdit=openEditAgentModal;
472
- window._currentEditAgentPath='';
473
- function openEditAgentModal(path){
474
- window._currentEditAgentPath=path;
475
- _origOpenEdit(path);
476
- }
477
-
478
489
  function confirmDeleteAgent(path,name){
479
490
  showConfirm('删除 Agent','确认删除 Agent "'+escHtml(name)+'" 吗?此操作不可恢复。',async()=>{
480
491
  const r=await api(`/api/agents/${encodeURIComponent(path)}`,{method:'DELETE'});
@@ -959,7 +970,7 @@ async function renderOrganization(){
959
970
  // 知识库
960
971
  html+=`<div class="card"><div class="flex justify-between items-center mb-16">
961
972
  <h3 style="margin:0">组织知识库</h3>
962
- <button class="btn btn-sm btn-primary" onclick="uploadOrgKnowledge()">上传文件</button></div>
973
+ <button class="btn btn-sm btn-primary" onclick="uploadOrgKnowledge(false)">上传文件</button> <button class="btn btn-sm btn-secondary" onclick="uploadOrgKnowledge(true)">📁 上传文件夹</button></div>
963
974
  <div id="orgKBList"><div class="empty">加载中...</div></div></div>`;
964
975
  $('content').innerHTML=html;
965
976
  loadOrgKnowledge();
@@ -984,15 +995,24 @@ async function loadOrgKnowledge(){
984
995
  }
985
996
  html+='</table>';el.innerHTML=html;
986
997
  }
987
- function uploadOrgKnowledge(){
998
+ function uploadOrgKnowledge(folderMode){
988
999
  const input=document.createElement('input');input.type='file';input.multiple=true;
1000
+ if(folderMode){input.webkitdirectory=true;}
989
1001
  input.onchange=async()=>{
990
1002
  const fd=new FormData();
991
- for(const f of input.files)fd.append('files',f);
1003
+ for(const f of input.files){
1004
+ // 文件夹上传时保留相对路径
1005
+ if(f.webkitRelativePath){fd.append('files',f,{headers:{'X-File-Path':f.webkitRelativePath}});}
1006
+ else{fd.append('files',f);}
1007
+ }
1008
+ showToast('正在上传...','info');
992
1009
  const r=await fetch(API+'/api/organization/knowledge/upload',{method:'POST',body:fd});
993
1010
  const data=await r.json();
994
1011
  if(data.error){showToast(data.error,'danger');return}
995
- showToast('上传成功','success');loadOrgKnowledge();
1012
+ const total=data.results?data.results.length:0;
1013
+ const okCount=data.results?data.results.filter(x=>x.ok).length:0;
1014
+ showToast(`上传完成: ${okCount}/${total} 文件成功`,okCount===total?'success':'warning');
1015
+ loadOrgKnowledge();
996
1016
  };input.click();
997
1017
  }
998
1018
  async function viewOrgKBFile(path){
@@ -1100,8 +1120,10 @@ async function saveDeptInfo(path){
1100
1120
  async function loadDeptKB(path){
1101
1121
  const files=await api(`/api/departments/${encodeURIComponent(path)}/knowledge`);
1102
1122
  const el=document.getElementById('deptKBArea');if(!el)return;
1103
- if(!files||!files.length){el.innerHTML='<div class="empty" style="margin-top:12px">暂无知识库文件</div>';return}
1104
- let html='<h4 style="margin-bottom:8px">部门知识库</h4><table><tr><th>文件</th><th>操作</th></tr>';
1123
+ let html='<h4 style="margin-bottom:8px">部门知识库</h4>';
1124
+ html+='<div style="margin-bottom:8px"><button class="btn btn-sm btn-primary" onclick="uploadDeptKB(\''+escHtml(path)+'\',false)">上传文件</button> <button class="btn btn-sm btn-secondary" onclick="uploadDeptKB(\''+escHtml(path)+'\',true)">📁 上传文件夹</button></div>';
1125
+ if(!files||!files.length){html+='<div class="empty" style="margin-top:12px">暂无知识库文件</div>';el.innerHTML=html;return}
1126
+ html+='<table><tr><th>文件</th><th>操作</th></tr>';
1105
1127
  for(const f of files){
1106
1128
  html+=`<tr><td>${escHtml(f.name||f.path)}</td>
1107
1129
  <td><button class="btn btn-sm btn-ghost" onclick="viewDeptKBFile('${escHtml(path)}','${escHtml(f.path||f.name)}')">查看</button>
@@ -1109,6 +1131,25 @@ async function loadDeptKB(path){
1109
1131
  }
1110
1132
  html+='</table>';el.innerHTML=html;
1111
1133
  }
1134
+ async function uploadDeptKB(path,folderMode){
1135
+ const input=document.createElement('input');input.type='file';input.multiple=true;
1136
+ if(folderMode){input.webkitdirectory=true;}
1137
+ input.onchange=async()=>{
1138
+ const fd=new FormData();
1139
+ for(const f of input.files){
1140
+ if(f.webkitRelativePath){fd.append('files',f,{headers:{'X-File-Path':f.webkitRelativePath}});}
1141
+ else{fd.append('files',f);}
1142
+ }
1143
+ showToast('正在上传...','info');
1144
+ const r=await fetch(API+`/api/departments/${encodeURIComponent(path)}/knowledge/upload`,{method:'POST',body:fd});
1145
+ const data=await r.json();
1146
+ if(data.error){showToast(data.error,'danger');return}
1147
+ const total=data.results?data.results.length:0;
1148
+ const okCount=data.results?data.results.filter(x=>x.ok).length:0;
1149
+ showToast(`上传完成: ${okCount}/${total} 文件成功`,okCount===total?'success':'warning');
1150
+ loadDeptKB(path);
1151
+ };input.click();
1152
+ }
1112
1153
 
1113
1154
  // Init
1114
1155
  showPage('dashboard');