myagent-ai 1.5.2 → 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.2"
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.2"
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.2",
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.2")
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
 
@@ -312,6 +313,8 @@ function buildArgs(userArgs) {
312
313
  case "setup": return ["main.py", "--setup"];
313
314
  case "reinstall":
314
315
  return []; // 特殊处理
316
+ case "uninstall":
317
+ return []; // 特殊处理
315
318
  default:
316
319
  return ["main.py", ...userArgs];
317
320
  }
@@ -329,6 +332,71 @@ function main() {
329
332
  process.exit(1);
330
333
  }
331
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
+
332
400
  // 特殊命令: reinstall
333
401
  if (userArgs[0] === "reinstall") {
334
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 的智能配置助手,专门帮助用户完成初始配置和日常配置管理。你的名字叫"配置助手"。
@@ -561,14 +618,14 @@ class ApiServer:
561
618
  "updated_at": now,
562
619
  "system_prompt": "你是 MyAgent 默认助手,运行在本机模式。请用友好、专业的方式回答用户的问题。",
563
620
  }
564
- (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")
565
622
  for fn, default in [
566
623
  ("soul.md", "# 全权Agent\n\n## 性格\n专业、友好的AI助手\n"),
567
624
  ("identity.md", "# 全权Agent\n\n## 身份\nMyAgent 默认AI助手\n"),
568
625
  ("user.md", "# 用户信息\n\n## 用户偏好\n<!-- 在此处记录用户偏好 -->\n"),
569
626
  ]:
570
627
  if not (ad / fn).exists():
571
- (ad / fn).write_text(default)
628
+ (ad / fn).write_text(default, encoding="utf-8")
572
629
  logger.info("已创建默认 Agent (全权Agent)")
573
630
  self._ensure_config_helper()
574
631
 
@@ -578,7 +635,7 @@ class ApiServer:
578
635
  if not cfg_file.exists():
579
636
  return
580
637
  try:
581
- cfg = json.loads(cfg_file.read_text())
638
+ cfg = json.loads(cfg_file.read_text(encoding="utf-8"))
582
639
  except (json.JSONDecodeError, ValueError):
583
640
  logger.warning(f"Agent config JSON 解析失败,自动删除重建: {path}")
584
641
  try:
@@ -602,7 +659,7 @@ class ApiServer:
602
659
  "created_at": now,
603
660
  "updated_at": now,
604
661
  }
605
- cfg_file.write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
662
+ cfg_file.write_text(json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8")
606
663
  logger.info(f"已重建 Agent 配置: {path}")
607
664
  return
608
665
  changed = False
@@ -621,7 +678,7 @@ class ApiServer:
621
678
  cfg["name"] = intended_name
622
679
  changed = True
623
680
  if changed:
624
- 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")
625
682
 
626
683
  def _ensure_config_helper(self):
627
684
  """确保系统级「配置助手」agent 存在(不可删除、不可改名、核心字段不可修改)"""
@@ -646,14 +703,14 @@ class ApiServer:
646
703
  "updated_at": now,
647
704
  "system_prompt": CONFIG_HELPER_PROMPT,
648
705
  }
649
- (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")
650
707
  for fn, default in [
651
708
  ("soul.md", "# 配置助手\n\n## 性格\n专业、友好的配置助手\n"),
652
709
  ("identity.md", "# 配置助手\n\n## 身份\nMyAgent 内置智能配置助手\n"),
653
710
  ("user.md", "# 用户信息\n\n## 用户偏好\n<!-- 在此处记录用户偏好 -->\n"),
654
711
  ]:
655
712
  if not (ad / fn).exists():
656
- (ad / fn).write_text(default)
713
+ (ad / fn).write_text(default, encoding="utf-8")
657
714
  logger.info("已创建系统级配置助手 Agent")
658
715
  # 创建快捷方式 p -> 配置助手(符号链接)
659
716
  p_dir = self._agent_dir("p")
@@ -713,7 +770,7 @@ class ApiServer:
713
770
  if not cfg_file.exists():
714
771
  return None
715
772
  try:
716
- return json.loads(cfg_file.read_text())
773
+ return json.loads(cfg_file.read_text(encoding="utf-8"))
717
774
  except (json.JSONDecodeError, ValueError):
718
775
  logger.warning(f"Agent config JSON 解析失败: {path}")
719
776
  return None
@@ -722,7 +779,7 @@ class ApiServer:
722
779
  """写入 agent 配置"""
723
780
  ad = self._agent_dir(path)
724
781
  ad.mkdir(parents=True, exist_ok=True)
725
- (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")
726
783
 
727
784
  def _deep_merge(self, base: dict, override: dict) -> None:
728
785
  """深度合并 override 到 base"""
@@ -790,7 +847,7 @@ class ApiServer:
790
847
  # 跳过符号链接目录(如 p -> 配置助手),避免重复处理
791
848
  if d.is_symlink():
792
849
  try:
793
- cfg = json.loads(cfg_file.read_text())
850
+ cfg = json.loads(cfg_file.read_text(encoding="utf-8"))
794
851
  except (json.JSONDecodeError, ValueError):
795
852
  continue
796
853
  else:
@@ -799,7 +856,7 @@ class ApiServer:
799
856
  if not cfg_file.exists():
800
857
  continue
801
858
  try:
802
- cfg = json.loads(cfg_file.read_text())
859
+ cfg = json.loads(cfg_file.read_text(encoding="utf-8"))
803
860
  except (json.JSONDecodeError, ValueError):
804
861
  logger.warning(f"Agent config JSON 解析失败,跳过: {agent_path}")
805
862
  continue
@@ -914,14 +971,14 @@ class ApiServer:
914
971
  cfg["work_dir"] = data["work_dir"]
915
972
 
916
973
  ad.mkdir(parents=True, exist_ok=True)
917
- (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")
918
975
  for fn, default in [
919
976
  ("soul.md", f"# {name}\n\n## 性格\n专业AI助手\n"),
920
977
  ("identity.md", f"# {name}\n\n## 身份\nAI助手\n"),
921
978
  ("user.md", f"# {name} 用户信息\n\n## 用户偏好\n<!-- 在此处记录用户偏好 -->\n"),
922
979
  ]:
923
980
  if not (ad / fn).exists():
924
- (ad / fn).write_text(default)
981
+ (ad / fn).write_text(default, encoding="utf-8")
925
982
 
926
983
  logger.info(f"创建 Agent: {name} (sandbox模式)")
927
984
  return web.json_response({"ok": True, "path": name, "name": name, "avatar_color": cfg["avatar_color"]})
@@ -974,14 +1031,14 @@ class ApiServer:
974
1031
  cfg["backup_model_ids"] = [x for x in data["backup_model_ids"] if isinstance(x, str) and x.strip()]
975
1032
 
976
1033
  ad.mkdir(parents=True, exist_ok=True)
977
- (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")
978
1035
  for fn, default in [
979
1036
  ("soul.md", f"# {name}\n\n## 上级: {parent_path}\n## 性格\n专业AI助手\n"),
980
1037
  ("identity.md", f"# {name}\n\n## 身份\n{parent_path} 的子 Agent\n"),
981
1038
  ("user.md", f"# {name} 用户信息\n\n## 用户偏好\n<!-- 在此处记录用户偏好 -->\n"),
982
1039
  ]:
983
1040
  if not (ad / fn).exists():
984
- (ad / fn).write_text(default)
1041
+ (ad / fn).write_text(default, encoding="utf-8")
985
1042
 
986
1043
  logger.info(f"创建子 Agent: {child_path} (sandbox模式)")
987
1044
  return web.json_response({"ok": True, "path": child_path, "name": name, "parent": parent_path, "avatar_color": cfg["avatar_color"]})
@@ -998,7 +1055,7 @@ class ApiServer:
998
1055
  for d in sorted(parent_dir.iterdir()):
999
1056
  if d.is_dir() and (d / "config.json").exists():
1000
1057
  try:
1001
- cfg = json.loads((d / "config.json").read_text())
1058
+ cfg = json.loads((d / "config.json").read_text(encoding="utf-8"))
1002
1059
  except (json.JSONDecodeError, ValueError):
1003
1060
  logger.warning(f"Agent config JSON 解析失败,跳过: {parent_path}/{d.name}")
1004
1061
  continue
@@ -1016,12 +1073,12 @@ class ApiServer:
1016
1073
  if not (ad / "config.json").exists():
1017
1074
  return web.json_response({"error": "not found"}, status=404)
1018
1075
  try:
1019
- cfg = json.loads((ad / "config.json").read_text())
1076
+ cfg = json.loads((ad / "config.json").read_text(encoding="utf-8"))
1020
1077
  except (json.JSONDecodeError, ValueError):
1021
1078
  return web.json_response({"error": "config.json 解析失败"}, status=500)
1022
- soul = (ad / "soul.md").read_text() if (ad / "soul.md").exists() else ""
1023
- identity = (ad / "identity.md").read_text() if (ad / "identity.md").exists() else ""
1024
- 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 ""
1025
1082
  # 列出子 agent
1026
1083
  children = []
1027
1084
  for d in sorted(ad.iterdir()):
@@ -1055,7 +1112,7 @@ class ApiServer:
1055
1112
  if not (ad / "config.json").exists():
1056
1113
  return web.json_response({"error": "not found"}, status=404)
1057
1114
  try:
1058
- cfg = json.loads((ad / "config.json").read_text())
1115
+ cfg = json.loads((ad / "config.json").read_text(encoding="utf-8"))
1059
1116
  except (json.JSONDecodeError, ValueError):
1060
1117
  return web.json_response({"error": "config.json 解析失败"}, status=500)
1061
1118
 
@@ -1093,10 +1150,10 @@ class ApiServer:
1093
1150
  del data["system"]
1094
1151
  # 自动更新 updated_at
1095
1152
  cfg["updated_at"] = datetime.datetime.now().isoformat()
1096
- (ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
1097
- if "soul" in data and not is_system: (ad / "soul.md").write_text(data["soul"])
1098
- if "identity" in data and not is_system: (ad / "identity.md").write_text(data["identity"])
1099
- 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")
1100
1157
  logger.info(f"更新 Agent: {path}")
1101
1158
  return web.json_response({"ok": True})
1102
1159
 
@@ -1130,12 +1187,12 @@ class ApiServer:
1130
1187
  p = self._agent_dir(path) / "soul.md"
1131
1188
  if not p.parent.exists() or not (p.parent / "config.json").exists():
1132
1189
  return web.json_response({"error": "not found"}, status=404)
1133
- 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 ""})
1134
1191
 
1135
1192
  async def handle_set_soul(self, request):
1136
1193
  data = await request.json(); ad = self._agent_dir(request.match_info["name"])
1137
1194
  ad.mkdir(parents=True, exist_ok=True)
1138
- (ad / "soul.md").write_text(data.get("soul", ""))
1195
+ (ad / "soul.md").write_text(data.get("soul", ""), encoding="utf-8")
1139
1196
  return web.json_response({"ok": True})
1140
1197
 
1141
1198
  async def handle_get_identity(self, request):
@@ -1143,12 +1200,12 @@ class ApiServer:
1143
1200
  p = self._agent_dir(path) / "identity.md"
1144
1201
  if not p.parent.exists() or not (p.parent / "config.json").exists():
1145
1202
  return web.json_response({"error": "not found"}, status=404)
1146
- 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 ""})
1147
1204
 
1148
1205
  async def handle_set_identity(self, request):
1149
1206
  data = await request.json(); ad = self._agent_dir(request.match_info["name"])
1150
1207
  ad.mkdir(parents=True, exist_ok=True)
1151
- (ad / "identity.md").write_text(data.get("identity", ""))
1208
+ (ad / "identity.md").write_text(data.get("identity", ""), encoding="utf-8")
1152
1209
  return web.json_response({"ok": True})
1153
1210
 
1154
1211
  async def handle_get_user(self, request):
@@ -1156,12 +1213,12 @@ class ApiServer:
1156
1213
  p = self._agent_dir(path) / "user.md"
1157
1214
  if not p.parent.exists() or not (p.parent / "config.json").exists():
1158
1215
  return web.json_response({"error": "not found"}, status=404)
1159
- 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 ""})
1160
1217
 
1161
1218
  async def handle_set_user(self, request):
1162
1219
  data = await request.json(); ad = self._agent_dir(request.match_info["name"])
1163
1220
  ad.mkdir(parents=True, exist_ok=True)
1164
- (ad / "user.md").write_text(data.get("user", ""))
1221
+ (ad / "user.md").write_text(data.get("user", ""), encoding="utf-8")
1165
1222
  return web.json_response({"ok": True})
1166
1223
 
1167
1224
  # --- Executor ---
@@ -1184,7 +1241,7 @@ class ApiServer:
1184
1241
  "sandbox_image", "sandbox_network", "sandbox_memory"):
1185
1242
  if k in data:
1186
1243
  exe[k] = data[k]
1187
- 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")
1188
1245
  # 热更新内存配置
1189
1246
  update_keys = {k: v for k, v in data.items() if hasattr(self.core.config_mgr.config.executor, k)}
1190
1247
  self.core.config_mgr.update_executor(**update_keys)
@@ -1411,7 +1468,7 @@ class ApiServer:
1411
1468
  agent_info = {"name": name, "avatar_color": _agent_color(name)}
1412
1469
  if (ad / "config.json").exists():
1413
1470
  try:
1414
- agent_info.update(json.loads((ad / "config.json").read_text()))
1471
+ agent_info.update(json.loads((ad / "config.json").read_text(encoding="utf-8")))
1415
1472
  except (json.JSONDecodeError, ValueError):
1416
1473
  pass
1417
1474
  return web.json_response({**agent_info, "sessions": sessions})
@@ -1468,7 +1525,7 @@ class ApiServer:
1468
1525
  for k in ("provider", "model", "base_url", "temperature", "max_tokens", "timeout", "max_retries"):
1469
1526
  if k in data: llm[k] = data[k]
1470
1527
  if data.get("api_key"): llm["api_key"] = data["api_key"]
1471
- 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")
1472
1529
  # 2. 热更新内存中的配置
1473
1530
  update_keys = {k: v for k, v in data.items() if k != "api_key" or data.get("api_key")}
1474
1531
  self.core.config_mgr.update_llm(**update_keys)
@@ -1488,8 +1545,41 @@ class ApiServer:
1488
1545
  return web.json_response({"ok": True, "hot_reload": True})
1489
1546
 
1490
1547
  async def handle_test_llm(self, request):
1548
+ """POST /api/llm/test - 测试 LLM 连接,支持使用请求体中的临时配置"""
1491
1549
  try:
1492
- 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")])
1493
1583
  return web.json_response({"ok": True, "response": msg.content[:100] if msg.content else ""})
1494
1584
  except Exception as e:
1495
1585
  return web.json_response({"ok": False, "error": str(e)})
@@ -1650,7 +1740,7 @@ class ApiServer:
1650
1740
  cfg_path = self.core.config_mgr._config_file
1651
1741
  cfg_data = _safe_load_json(cfg_path) if cfg_path.exists() else {}
1652
1742
  cfg_data["disabled_skills"] = disabled
1653
- 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")
1654
1744
 
1655
1745
  def _load_disabled_skills(self):
1656
1746
  """从配置文件加载禁用技能列表"""
@@ -1673,7 +1763,7 @@ class ApiServer:
1673
1763
  cfg_path = self.core.config_mgr._config_file
1674
1764
  cfg_data = _safe_load_json(cfg_path) if cfg_path.exists() else {}
1675
1765
  cfg_data["workspace"] = path
1676
- 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")
1677
1767
  return web.json_response({"ok": True})
1678
1768
 
1679
1769
  async def handle_list_workdir(self, request):
@@ -1744,7 +1834,7 @@ class ApiServer:
1744
1834
  for k in ("enabled", "server_url", "max_friends", "auto_accept"):
1745
1835
  if k in data:
1746
1836
  comm[k] = data[k]
1747
- 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")
1748
1838
  # Hot-reload in memory
1749
1839
  if "enabled" in data:
1750
1840
  self.core.config.communication.enabled = data["enabled"]
@@ -2392,7 +2482,7 @@ class ApiServer:
2392
2482
  return web.json_response(files)
2393
2483
 
2394
2484
  async def handle_upload_org_knowledge(self, request):
2395
- """POST /api/organization/knowledge/upload - 上传文件到组织知识库"""
2485
+ """POST /api/organization/knowledge/upload - 上传文件到组织知识库(支持文件和文件夹上传)"""
2396
2486
  # 权限检查
2397
2487
  agent = request.query.get("agent", "default")
2398
2488
  if not self._check_org_admin(agent):
@@ -2401,16 +2491,9 @@ class ApiServer:
2401
2491
  status=403,
2402
2492
  )
2403
2493
 
2404
- data = await request.json()
2405
- # 支持单个文件或批量文件
2406
- files = data.get("files", [])
2494
+ files = await _read_multipart_files(request)
2407
2495
  if not files:
2408
- # 兼容单文件格式
2409
- filename = data.get("filename", data.get("name", ""))
2410
- content = data.get("content", "")
2411
- if not filename or content is None:
2412
- return web.json_response({"ok": False, "error": "缺少 filename 或 content"}, status=400)
2413
- files = [{"name": filename, "content": content}]
2496
+ return web.json_response({"ok": False, "error": "没有上传文件"}, status=400)
2414
2497
 
2415
2498
  org_mgr = self._get_org_manager()
2416
2499
  results = []
@@ -2496,16 +2579,11 @@ class ApiServer:
2496
2579
  return web.json_response(files)
2497
2580
 
2498
2581
  async def handle_upload_agent_knowledge(self, request):
2499
- """POST /api/agents/{name}/knowledge/upload - 上传到 Agent 知识库"""
2582
+ """POST /api/agents/{name}/knowledge/upload - 上传到 Agent 知识库(支持文件和文件夹上传)"""
2500
2583
  agent_path = request.match_info["name"]
2501
- data = await request.json()
2502
- files = data.get("files", [])
2584
+ files = await _read_multipart_files(request)
2503
2585
  if not files:
2504
- filename = data.get("filename", data.get("name", ""))
2505
- content = data.get("content", "")
2506
- if not filename or content is None:
2507
- return web.json_response({"ok": False, "error": "缺少 filename 或 content"}, status=400)
2508
- files = [{"name": filename, "content": content}]
2586
+ return web.json_response({"ok": False, "error": "没有上传文件"}, status=400)
2509
2587
 
2510
2588
  kb_dir = self._get_agent_knowledge_dir(agent_path)
2511
2589
  kb_dir.mkdir(parents=True, exist_ok=True)
@@ -2514,7 +2592,6 @@ class ApiServer:
2514
2592
  name = f.get("name", "")
2515
2593
  content = f.get("content", "")
2516
2594
  # 安全校验
2517
- safe_name = Path(name).name
2518
2595
  if ".." in name or name.startswith("/"):
2519
2596
  results.append({"ok": False, "message": "非法文件名"})
2520
2597
  continue
@@ -2524,7 +2601,7 @@ class ApiServer:
2524
2601
  parent.mkdir(parents=True, exist_ok=True)
2525
2602
  target = parent / parts[-1]
2526
2603
  else:
2527
- target = kb_dir / safe_name
2604
+ target = kb_dir / Path(name).name
2528
2605
  try:
2529
2606
  target.write_text(content, encoding="utf-8")
2530
2607
  results.append({"ok": True, "path": name})
@@ -3239,20 +3316,11 @@ class ApiServer:
3239
3316
  return web.json_response(files)
3240
3317
 
3241
3318
  async def handle_upload_dept_knowledge(self, request):
3242
- """POST /api/departments/{path}/knowledge/upload - 上传到部门知识库"""
3319
+ """POST /api/departments/{path}/knowledge/upload - 上传到部门知识库(支持文件和文件夹上传)"""
3243
3320
  path = request.match_info["path"]
3244
- try:
3245
- data = await request.json()
3246
- except Exception:
3247
- return web.json_response({"error": "invalid JSON"}, status=400)
3248
- files = data.get("files", [])
3321
+ files = await _read_multipart_files(request)
3249
3322
  if not files:
3250
- # 兼容单文件格式
3251
- filename = data.get("filename", data.get("name", ""))
3252
- content = data.get("content", "")
3253
- if not filename or content is None:
3254
- return web.json_response({"ok": False, "error": "缺少 filename 或 content"}, status=400)
3255
- files = [{"name": filename, "content": content}]
3323
+ return web.json_response({"ok": False, "error": "没有上传文件"}, status=400)
3256
3324
  dm = self._get_dept_manager()
3257
3325
  results = []
3258
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');