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 +9 -4
- package/core/update_manager.py +1 -1
- package/core/version.py +9 -4
- package/install/install.ps1 +1 -1
- package/install/install.sh +1 -1
- package/install/uninstall.ps1 +142 -0
- package/install/uninstall.sh +149 -0
- package/package.json +1 -1
- package/setup.py +1 -1
- package/start.js +76 -4
- package/web/__pycache__/api_server.cpython-312.pyc +0 -0
- package/web/api_server.py +193 -84
- package/web/ui/chat.html +5 -3
- package/web/ui/index.html +58 -17
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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":
|
package/core/update_manager.py
CHANGED
|
@@ -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.
|
|
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'
|
|
28
|
-
|
|
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 ""
|
package/install/install.ps1
CHANGED
package/install/install.sh
CHANGED
|
@@ -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
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.
|
|
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
|
-
//
|
|
243
|
-
const checkLimit = Math.min(allModules.length,
|
|
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();
|
|
Binary file
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
651
|
-
|
|
738
|
+
pkg_root / "docs" / "配置使用说明.md",
|
|
739
|
+
pkg_root.parent / "docs" / "配置使用说明.md",
|
|
652
740
|
]
|
|
653
|
-
#
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2461
|
-
files = data.get("files", [])
|
|
2584
|
+
files = await _read_multipart_files(request)
|
|
2462
2585
|
if not files:
|
|
2463
|
-
|
|
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 /
|
|
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
|
-
|
|
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.
|
|
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
|
|
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){
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
1104
|
-
|
|
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');
|