Document and harden project handoff

This commit is contained in:
wuyang
2026-07-12 14:54:03 +08:00
parent ab758e774e
commit 8e4ff3779b
17 changed files with 1030 additions and 32 deletions
+41 -7
View File
@@ -4,6 +4,13 @@
## Run
```bash
web/manage.sh start
web/manage.sh status
```
前台调试仍可直接运行:
```bash
python3 web/app.py --host 100.114.68.27 --port 18080
```
@@ -30,17 +37,40 @@ https://lab.k1412.top/
## AI Policy
- 默认摘要模型:`ChatGPT-5.6:fast`
- 默认翻译模型:`ChatGPT-5.6:light`
- 深度分析模型:`ChatGPT-5.6:large`,只在前端手动选择时使用
- Atlas 解释:`ChatGPT-5.6:fast`
- 主题导读、阅读路径、主题比较:`ChatGPT-5.6:large`,只在前端手动确认后调用
- Search Agent 查询理解:`ChatGPT-5.6:light`
- Search Agent 结果简报、搜索追问、论文对话:`ChatGPT-5.6:fast`
- 默认摘要模型:`ChatGPT-5.6:Terra`
- 默认翻译模型:`ChatGPT-5.6:Luna`
- 深度分析模型:`ChatGPT-5.6:Sol`,只在前端手动选择时使用
- Atlas 解释:`ChatGPT-5.6:Terra`
- 主题导读、阅读路径、主题比较:`ChatGPT-5.6:Sol`,只在前端手动确认后调用
- Search Agent 查询理解:`ChatGPT-5.6:Luna`
- Search Agent 结果简报、搜索追问、论文对话:`ChatGPT-5.6:Terra`
- Ollama 请求单并发执行,并缓存结果到 `web/cache/ai/`
- 应用侧不下发 `num_ctx` / `keep_alive`,上下文长度和模型常驻由 Ollama 服务端统一控制
- arXiv 摘要缓存到 `web/cache/arxiv/`
模型 tier 可以覆盖:
```bash
OLLAMA_MODEL_LIGHT=ChatGPT-5.6:Luna \
OLLAMA_MODEL_FAST=ChatGPT-5.6:Terra \
OLLAMA_MODEL_LARGE=ChatGPT-5.6:Sol \
web/manage.sh restart
```
`/api/health` 会返回可用模型、默认模型和缺失的默认标签。维护时先看健康接口,不要假设旧 alias 仍存在。
## Service Management
```bash
web/manage.sh start
web/manage.sh stop
web/manage.sh restart
web/manage.sh status
web/manage.sh logs
```
`manage.sh` 会把仓库中的 `agent-knowledge-atlas.service` 链接到用户级 systemd,并把可覆盖环境写入忽略提交的 `web/cache/service.env`。日志使用 systemd journal。它不替代 TLS、反向代理或鉴权。
## Knowledge Design
先由后端从本地语料轻量计算:
@@ -62,3 +92,7 @@ https://lab.k1412.top/
- 搜索意图理解、关键词扩展、结果简报
- 基于搜索结果继续追问
- 基于单篇论文继续对话式研读
## Product Boundary
当前 Atlas 是资料探索原型,不是已经验证的学习产品。用户对“只浏览论文、图谱和 AI 摘要”的实用性评价很低,因为它没有保存个人理解、主张、反证、应用和复习状态。后续产品方向见 `docs/08-next-work.md`,不要把下一阶段继续收敛为图谱视觉优化。
+22
View File
@@ -0,0 +1,22 @@
[Unit]
Description=Agent Knowledge Atlas
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=%h/Code/agent
Environment=PYTHONUNBUFFERED=1
Environment=OLLAMA_URL=http://192.168.1.10:11434
Environment=OLLAMA_MODEL_LIGHT=ChatGPT-5.6:Luna
Environment=OLLAMA_MODEL_FAST=ChatGPT-5.6:Terra
Environment=OLLAMA_MODEL_LARGE=ChatGPT-5.6:Sol
Environment=HOST=100.114.68.27
Environment=PORT=18080
EnvironmentFile=-%h/Code/agent/web/cache/service.env
ExecStart=/usr/bin/python3 %h/Code/agent/web/app.py --host ${HOST} --port ${PORT}
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target
+28 -13
View File
@@ -31,7 +31,7 @@ CACHE_ROOT = WEB_ROOT / "cache"
ARXIV_CACHE = CACHE_ROOT / "arxiv"
AI_CACHE = CACHE_ROOT / "ai"
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://192.168.1.10:11434").rstrip("/")
ASSET_VERSION = "atlas-20260709-01"
ASSET_VERSION = "atlas-20260712-01"
def detect_app_version() -> str:
@@ -51,18 +51,22 @@ def detect_app_version() -> str:
APP_VERSION = detect_app_version()
MODEL_LIGHT = os.environ.get("OLLAMA_MODEL_LIGHT", "ChatGPT-5.6:Luna")
MODEL_FAST = os.environ.get("OLLAMA_MODEL_FAST", "ChatGPT-5.6:Terra")
MODEL_LARGE = os.environ.get("OLLAMA_MODEL_LARGE", "ChatGPT-5.6:Sol")
DEFAULT_MODELS = {
"translate": "ChatGPT-5.6:light",
"summary": "ChatGPT-5.6:fast",
"deep": "ChatGPT-5.6:large",
"atlas": "ChatGPT-5.6:fast",
"topic": "ChatGPT-5.6:large",
"path": "ChatGPT-5.6:large",
"compare": "ChatGPT-5.6:large",
"search_plan": "ChatGPT-5.6:light",
"search_summary": "ChatGPT-5.6:fast",
"search_followup": "ChatGPT-5.6:fast",
"paper_chat": "ChatGPT-5.6:fast",
"translate": MODEL_LIGHT,
"summary": MODEL_FAST,
"deep": MODEL_LARGE,
"atlas": MODEL_FAST,
"topic": MODEL_LARGE,
"path": MODEL_LARGE,
"compare": MODEL_LARGE,
"search_plan": MODEL_LIGHT,
"search_summary": MODEL_FAST,
"search_followup": MODEL_FAST,
"paper_chat": MODEL_FAST,
}
NS = {
@@ -1376,12 +1380,20 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
def handle_health(self) -> None:
papers, _ = load_papers()
ollama_ok = False
available_models: list[str] = []
try:
request = urllib.request.Request(f"{OLLAMA_URL}/api/tags")
with urllib.request.urlopen(request, timeout=3) as response:
ollama_ok = response.status == 200
except (urllib.error.URLError, TimeoutError):
payload = json.loads(response.read().decode("utf-8"))
available_models = sorted(
str(item.get("name"))
for item in payload.get("models", [])
if item.get("name")
)
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError):
ollama_ok = False
missing_default_models = sorted(set(DEFAULT_MODELS.values()) - set(available_models))
write_json(
self,
{
@@ -1391,6 +1403,9 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
"asset_version": ASSET_VERSION,
"ollama_url": OLLAMA_URL,
"ollama_ok": ollama_ok,
"ollama_ready": ollama_ok and not missing_default_models,
"available_models": available_models,
"missing_default_models": missing_default_models,
"default_models": DEFAULT_MODELS,
},
)
Executable
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
UNIT_NAME="agent-knowledge-atlas.service"
UNIT_SOURCE="$ROOT/web/$UNIT_NAME"
UNIT_TARGET="$HOME/.config/systemd/user/$UNIT_NAME"
ENV_FILE="$ROOT/web/cache/service.env"
HOST="${HOST:-100.114.68.27}"
PORT="${PORT:-18080}"
HEALTH_URL="${HEALTH_URL:-http://$HOST:$PORT/api/health}"
write_environment() {
mkdir -p "$(dirname "$ENV_FILE")"
{
printf 'HOST=%s\n' "$HOST"
printf 'PORT=%s\n' "$PORT"
printf 'OLLAMA_URL=%s\n' "${OLLAMA_URL:-http://192.168.1.10:11434}"
printf 'OLLAMA_MODEL_LIGHT=%s\n' "${OLLAMA_MODEL_LIGHT:-ChatGPT-5.6:Luna}"
printf 'OLLAMA_MODEL_FAST=%s\n' "${OLLAMA_MODEL_FAST:-ChatGPT-5.6:Terra}"
printf 'OLLAMA_MODEL_LARGE=%s\n' "${OLLAMA_MODEL_LARGE:-ChatGPT-5.6:Sol}"
} >"$ENV_FILE"
}
install_unit() {
mkdir -p "$(dirname "$UNIT_TARGET")"
ln -sfn "$UNIT_SOURCE" "$UNIT_TARGET"
write_environment
systemctl --user daemon-reload
}
wait_for_health() {
for _ in $(seq 1 30); do
if curl -fsS --max-time 2 "$HEALTH_URL" >/dev/null 2>&1; then
return 0
fi
sleep 0.5
done
return 1
}
start_server() {
install_unit
systemctl --user enable --now "$UNIT_NAME"
if ! wait_for_health; then
echo "Server did not become healthy" >&2
journalctl --user -u "$UNIT_NAME" -n 60 --no-pager >&2 || true
return 1
fi
echo "Agent Knowledge Atlas started"
echo "Health: $HEALTH_URL"
}
stop_server() {
if systemctl --user is-active --quiet "$UNIT_NAME"; then
systemctl --user stop "$UNIT_NAME"
echo "Agent Knowledge Atlas stopped"
else
echo "Agent Knowledge Atlas is not running"
fi
}
status_server() {
systemctl --user is-active "$UNIT_NAME"
curl -fsS --max-time 5 "$HEALTH_URL"
echo
}
case "${1:-status}" in
install)
install_unit
systemctl --user enable "$UNIT_NAME"
echo "Installed $UNIT_NAME"
;;
start) start_server ;;
stop) stop_server ;;
restart)
install_unit
systemctl --user restart "$UNIT_NAME"
wait_for_health
echo "Agent Knowledge Atlas restarted"
;;
status) status_server ;;
logs) journalctl --user -u "$UNIT_NAME" -n "${LINES:-80}" --no-pager ;;
*)
echo "Usage: $0 {install|start|stop|restart|status|logs}" >&2
exit 2
;;
esac
+15 -5
View File
@@ -141,7 +141,12 @@ async function loadHealth() {
try {
const health = await api("/api/health");
state.health = health;
elements.healthLine.textContent = `${health.paper_count} papers · ${health.ollama_ok ? "Ollama online" : "Ollama offline"}`;
const ollamaState = health.ollama_ready
? "Ollama ready"
: health.ollama_ok
? "Ollama model mismatch"
: "Ollama offline";
elements.healthLine.textContent = `${health.paper_count} papers · ${ollamaState}`;
} catch (error) {
elements.healthLine.textContent = error.message;
}
@@ -1103,10 +1108,15 @@ function setAiBusy(isBusy, label = "") {
}
}
function isLargeModel(model) {
const configuredLarge = state.health?.default_models?.deep;
return model === configuredLarge || /:(large|sol)$/i.test(model);
}
async function runPaperAi(mode) {
if (!state.selectedPaper) return;
const model = state.health?.default_models?.[mode] || (mode === "translate" ? "ChatGPT-5.6:light" : "ChatGPT-5.6:fast");
if (model.includes(":large") && !window.confirm("将调用 large 模型,占用更多 GPU。继续?")) return;
const model = state.health?.default_models?.[mode] || (mode === "translate" ? "ChatGPT-5.6:Luna" : "ChatGPT-5.6:Terra");
if (isLargeModel(model) && !window.confirm("将调用 large 模型,占用更多 GPU。继续?")) return;
setAiBusy(true, `${mode} · ${model}`);
try {
const payload = await api("/api/ai", {
@@ -1124,8 +1134,8 @@ async function runPaperAi(mode) {
}
async function runAtlasAi(mode, topic = "", topicB = "") {
const model = state.health?.default_models?.[mode] || "ChatGPT-5.6:fast";
if (model.includes(":large") && !window.confirm("将调用 large 模型做高维综合,占用更多 GPU。继续?")) return;
const model = state.health?.default_models?.[mode] || "ChatGPT-5.6:Terra";
if (isLargeModel(model) && !window.confirm("将调用 large 模型做高维综合,占用更多 GPU。继续?")) return;
setAiBusy(true, `${mode} · ${model}`);
try {
const payload = await api("/api/atlas/ai", {
+2 -2
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Agent Knowledge Atlas</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%23cfe8e3'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-size='24' font-family='Arial' font-weight='700' fill='%23064c47'%3EAK%3C/text%3E%3C/svg%3E" />
<link rel="stylesheet" href="/static/styles.css?v=atlas-20260709-01" />
<link rel="stylesheet" href="/static/styles.css?v=atlas-20260712-01" />
</head>
<body>
<main class="atlas-app">
@@ -79,6 +79,6 @@
</footer>
</main>
<script src="/static/app.js?v=atlas-20260709-01"></script>
<script src="/static/app.js?v=atlas-20260712-01"></script>
</body>
</html>