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 +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 +68 -0
- package/web/api_server.py +139 -71
- 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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2502
|
-
files = data.get("files", [])
|
|
2584
|
+
files = await _read_multipart_files(request)
|
|
2503
2585
|
if not files:
|
|
2504
|
-
|
|
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 /
|
|
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
|
-
|
|
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.
|
|
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');
|