forked from niuniu/llm-intelligence
Compare commits
2 Commits
6a2cd3f159
...
b430fb9301
| Author | SHA1 | Date | |
|---|---|---|---|
| b430fb9301 | |||
|
|
31f1b510c3 |
105
DEPLOYMENT.md
105
DEPLOYMENT.md
@@ -1,8 +1,8 @@
|
|||||||
# LLM Intelligence Hub - 部署指南
|
# LLM Intelligence Hub - 部署指南
|
||||||
|
|
||||||
> 版本: v1.0
|
> 版本: v1.1
|
||||||
> 日期: 2026-05-10
|
> 日期: 2026-05-21
|
||||||
> 适用版本: Phase 1
|
> 适用版本: Phase 1 / Phase 2 基础部署
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
- Go 1.22+
|
- Go 1.22+
|
||||||
- Node.js 20+
|
- Node.js 20+
|
||||||
- PostgreSQL 16+
|
- PostgreSQL 16+
|
||||||
- Docker 或 Podman (可选)
|
- Docker / Docker Compose
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 快速开始
|
## 本地开发启动
|
||||||
|
|
||||||
### 1. 克隆仓库
|
### 1. 克隆仓库
|
||||||
```bash
|
```bash
|
||||||
@@ -29,13 +29,14 @@ git clone <repo-url> llm-intelligence
|
|||||||
cd llm-intelligence
|
cd llm-intelligence
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 配置数据库
|
### 2. 初始化数据库
|
||||||
```bash
|
```bash
|
||||||
# 创建数据库
|
|
||||||
createdb llm_intelligence
|
createdb llm_intelligence
|
||||||
|
|
||||||
# 运行迁移
|
|
||||||
psql llm_intelligence < db/migrations/001_phase1_core_tables.sql
|
psql llm_intelligence < db/migrations/001_phase1_core_tables.sql
|
||||||
|
psql llm_intelligence < db/migrations/002_sprint1_complete_schema.sql
|
||||||
|
psql llm_intelligence < db/migrations/003_phase2_region_pricing_metadata.sql
|
||||||
|
psql llm_intelligence < db/migrations/004_backfill_models_batch_id.sql
|
||||||
|
psql llm_intelligence < db/migrations/005_subscription_plan.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 配置环境变量
|
### 3. 配置环境变量
|
||||||
@@ -50,29 +51,40 @@ export FEISHU_WEBHOOK="your-webhook-url" # 可选
|
|||||||
go run cmd/server/main.go
|
go run cmd/server/main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. 启动前端 (开发)
|
### 5. 启动前端开发服务
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. 配置定时任务
|
|
||||||
```bash
|
|
||||||
crontab -e
|
|
||||||
# 添加: 0 8 * * * cd /path/to/llm-intelligence && bash scripts/run_daily.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Docker 部署
|
## Docker 部署
|
||||||
|
|
||||||
```bash
|
当前容器镜像已经内置前端静态资源,`app` 服务会同时提供页面和 API。
|
||||||
# 构建
|
|
||||||
docker build -t llm-hub .
|
|
||||||
|
|
||||||
# 或 docker-compose
|
### 使用 compose 启动完整环境
|
||||||
docker-compose up -d
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
启动后访问:
|
||||||
|
- Web UI: `http://localhost:8080/`
|
||||||
|
- Health: `http://localhost:8080/health`
|
||||||
|
- API: `http://localhost:8080/api/v1/models`
|
||||||
|
|
||||||
|
### 只构建镜像
|
||||||
|
```bash
|
||||||
|
docker build -t llm-hub .
|
||||||
|
```
|
||||||
|
|
||||||
|
运行示例:
|
||||||
|
```bash
|
||||||
|
docker run --rm -p 8080:8080 \
|
||||||
|
-e DATABASE_URL="postgres://llm_hub:changeme@host.docker.internal:5432/llm_intelligence?sslmode=disable" \
|
||||||
|
-e OPENROUTER_API_KEY="your-api-key" \
|
||||||
|
llm-hub
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -81,38 +93,59 @@ docker-compose up -d
|
|||||||
|
|
||||||
| 变量 | 必填 | 说明 |
|
| 变量 | 必填 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| DATABASE_URL | ✅ | PostgreSQL 连接串 |
|
| `DATABASE_URL` | 是 | PostgreSQL 连接串 |
|
||||||
| OPENROUTER_API_KEY | ✅ | OpenRouter API Key |
|
| `OPENROUTER_API_KEY` | 是 | OpenRouter API Key |
|
||||||
| FEISHU_WEBHOOK | ❌ | 飞书告警 Webhook |
|
| `FEISHU_WEBHOOK` | 否 | 飞书告警 Webhook |
|
||||||
| API_PORT | ❌ | 默认 8080 |
|
| `PORT` | 否 | 服务端监听端口,默认 `8080` |
|
||||||
|
| `FRONTEND_DIST_DIR` | 否 | 自定义静态资源目录,默认自动查找 `frontend/dist` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 验证安装
|
## 验证安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 数据库连接
|
|
||||||
curl http://localhost:8080/health
|
curl http://localhost:8080/health
|
||||||
|
curl http://localhost:8080/api/v1/models
|
||||||
|
```
|
||||||
|
|
||||||
# 采集器测试
|
前端构建校验:
|
||||||
go run scripts/fetch_openrouter.go
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
# 日报生成
|
Go 测试校验:
|
||||||
go run scripts/generate_daily_report.go
|
```bash
|
||||||
|
go test ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
|
|
||||||
### Q: 数据库迁移失败?
|
### Q: 前端构建失败?
|
||||||
确保 PostgreSQL 已启动,且用户有创建表的权限。
|
确认:
|
||||||
|
- Node.js >= 20
|
||||||
|
- `frontend/package-lock.json` 与 `npm ci` 一致
|
||||||
|
- 本地没有依赖已删除的 `frontend/src/data/latest_models.json`
|
||||||
|
|
||||||
### Q: 前端构建失败?
|
### Q: `docker-compose up -d` 后页面空白?
|
||||||
检查 Node.js 版本 >= 20,npm 版本 >= 10。
|
先执行:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
### Q: 采集器返回模拟数据?
|
然后检查:
|
||||||
未提供 OPENROUTER_API_KEY 时使用模拟数据,提供 Key 后获取真实数据。
|
```bash
|
||||||
|
docker-compose logs -f app
|
||||||
|
curl http://localhost:8080/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: API 返回 `database not configured`?
|
||||||
|
说明 `DATABASE_URL` 未注入或格式不正确,先执行:
|
||||||
|
```bash
|
||||||
|
echo "$DATABASE_URL"
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
51
RUNBOOK.md
51
RUNBOOK.md
@@ -1,8 +1,7 @@
|
|||||||
# LLM Intelligence Hub - 运维手册
|
# LLM Intelligence Hub - 运维手册
|
||||||
|
|
||||||
> 版本: v1.0
|
> 版本: v1.1
|
||||||
> 日期: 2026-05-10
|
> 日期: 2026-05-21
|
||||||
> 适用版本: Phase 1
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -10,7 +9,7 @@
|
|||||||
|
|
||||||
### 启动全部服务
|
### 启动全部服务
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker-compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
### 停止服务
|
### 停止服务
|
||||||
@@ -28,6 +27,12 @@ docker-compose logs -f db
|
|||||||
|
|
||||||
## 日常巡检
|
## 日常巡检
|
||||||
|
|
||||||
|
### 应用健康
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
curl http://localhost:8080/api/v1/models
|
||||||
|
```
|
||||||
|
|
||||||
### 数据库健康
|
### 数据库健康
|
||||||
```bash
|
```bash
|
||||||
psql "$DATABASE_URL" -c "SELECT COUNT(*) FROM models WHERE deleted_at IS NULL"
|
psql "$DATABASE_URL" -c "SELECT COUNT(*) FROM models WHERE deleted_at IS NULL"
|
||||||
@@ -61,13 +66,19 @@ df -h /tmp
|
|||||||
|
|
||||||
### 日报未生成
|
### 日报未生成
|
||||||
1. 检查 cron: `crontab -l | grep llm-intelligence`
|
1. 检查 cron: `crontab -l | grep llm-intelligence`
|
||||||
2. 手动运行: `bash scripts/run_daily.sh`
|
2. 手动执行: `bash scripts/run_daily.sh`
|
||||||
3. 检查降级报告: `ls reports/daily/*.md | tail -1`
|
3. 检查最近日报: `ls reports/daily/*.md | tail -1`
|
||||||
|
|
||||||
### 前端无法访问
|
### 前端无法访问
|
||||||
1. 检查 Nginx: `docker-compose ps nginx`
|
1. 检查应用容器: `docker-compose ps app`
|
||||||
2. 检查 dist: `ls frontend/dist/`
|
2. 检查首页响应: `curl -I http://localhost:8080/`
|
||||||
3. 检查端口: `netstat -tlnp | grep 80`
|
3. 检查 API 响应: `curl http://localhost:8080/api/v1/models`
|
||||||
|
4. 查看应用日志: `docker-compose logs -f app`
|
||||||
|
|
||||||
|
### 静态资源 404
|
||||||
|
1. 重新构建镜像: `docker-compose up -d --build`
|
||||||
|
2. 本地校验前端构建: `cd frontend && npm run build`
|
||||||
|
3. 确认容器内含有前端产物: `docker-compose exec app ls /app/frontend/dist`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -83,7 +94,7 @@ bash scripts/backup.sh
|
|||||||
gunzip < backup_file.sql.gz | psql "$DATABASE_URL"
|
gunzip < backup_file.sql.gz | psql "$DATABASE_URL"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 定时备份 (cron)
|
### 定时备份
|
||||||
```bash
|
```bash
|
||||||
0 2 * * * cd /path/to/llm-intelligence && bash scripts/backup.sh >> /tmp/backup.log 2>&1
|
0 2 * * * cd /path/to/llm-intelligence && bash scripts/backup.sh >> /tmp/backup.log 2>&1
|
||||||
```
|
```
|
||||||
@@ -94,24 +105,14 @@ gunzip < backup_file.sql.gz | psql "$DATABASE_URL"
|
|||||||
|
|
||||||
| 指标 | 告警阈值 | 检查命令 |
|
| 指标 | 告警阈值 | 检查命令 |
|
||||||
|------|----------|----------|
|
|------|----------|----------|
|
||||||
| 模型数 | < 300 | `SELECT COUNT(*) FROM models` |
|
| 模型数 | `< 300` | `SELECT COUNT(*) FROM models` |
|
||||||
| 采集成功率 | < 95% | `SELECT success_rate FROM collector_stats` |
|
| 采集成功率 | `< 95%` | `SELECT success_rate FROM collector_stats` |
|
||||||
| 数据库连接 | 失败 | `pg_isready` |
|
| 数据库连接 | 失败 | `pg_isready` |
|
||||||
| 磁盘空间 | > 80% | `df -h` |
|
| 磁盘空间 | `> 80%` | `df -h` |
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 扩容指南
|
|
||||||
|
|
||||||
### 垂直扩容
|
|
||||||
增加 PostgreSQL 内存和 CPU。
|
|
||||||
|
|
||||||
### 水平扩容
|
|
||||||
使用读写分离或分片(Phase 2+)。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 联系信息
|
## 联系信息
|
||||||
|
|
||||||
- 维护者: 宰相
|
- 维护者: 宅相
|
||||||
- 项目路径: /home/long/project/llm-intelligence
|
- 项目路径: `D:\project\llm-intelligence`
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
@@ -75,7 +78,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := newMux(db, fetchModels, fetchSubscriptionPlans)
|
mux := newMux(db, fetchModels, fetchSubscriptionPlans, resolveFrontendDistDir())
|
||||||
|
|
||||||
log.Printf("server listening on :%s", addr)
|
log.Printf("server listening on :%s", addr)
|
||||||
if err := http.ListenAndServe(":"+addr, mux); err != nil {
|
if err := http.ListenAndServe(":"+addr, mux); err != nil {
|
||||||
@@ -83,7 +86,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMux(db *sql.DB, fetchModelsFn modelFetcher, fetchPlansFn subscriptionPlanFetcher) *http.ServeMux {
|
func newMux(db *sql.DB, fetchModelsFn modelFetcher, fetchPlansFn subscriptionPlanFetcher, frontendDistDir string) *http.ServeMux {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if db == nil {
|
if db == nil {
|
||||||
@@ -122,9 +125,65 @@ func newMux(db *sql.DB, fetchModelsFn modelFetcher, fetchPlansFn subscriptionPla
|
|||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, apiEnvelope{Data: plans})
|
writeJSON(w, http.StatusOK, apiEnvelope{Data: plans})
|
||||||
})
|
})
|
||||||
|
if frontendDistDir != "" {
|
||||||
|
mux.Handle("/", frontendHandler(frontendDistDir))
|
||||||
|
}
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveFrontendDistDir() string {
|
||||||
|
candidates := []string{}
|
||||||
|
if custom := os.Getenv("FRONTEND_DIST_DIR"); custom != "" {
|
||||||
|
candidates = append(candidates, custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates = append(candidates,
|
||||||
|
filepath.Join("frontend", "dist"),
|
||||||
|
filepath.Join(filepath.Dir(os.Args[0]), "frontend", "dist"),
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
indexPath := filepath.Join(candidate, "index.html")
|
||||||
|
info, err := os.Stat(indexPath)
|
||||||
|
if err == nil && !info.IsDir() {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func frontendHandler(frontendDistDir string) http.Handler {
|
||||||
|
indexPath := filepath.Join(frontendDistDir, "index.html")
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanPath := path.Clean("/" + r.URL.Path)
|
||||||
|
if cleanPath == "/" {
|
||||||
|
http.ServeFile(w, r, indexPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
relativePath := strings.TrimPrefix(cleanPath, "/")
|
||||||
|
assetPath := filepath.Join(frontendDistDir, filepath.FromSlash(relativePath))
|
||||||
|
if info, err := os.Stat(assetPath); err == nil && !info.IsDir() {
|
||||||
|
http.ServeFile(w, r, assetPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if filepath.Ext(relativePath) != "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.ServeFile(w, r, indexPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func fetchModels(ctx context.Context, db *sql.DB) ([]modelResponse, error) {
|
func fetchModels(ctx context.Context, db *sql.DB) ([]modelResponse, error) {
|
||||||
rows, err := db.QueryContext(ctx, `
|
rows, err := db.QueryContext(ctx, `
|
||||||
WITH latest_prices AS (
|
WITH latest_prices AS (
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,7 +23,7 @@ func TestSubscriptionPlansHandlerReturnsEnvelope(t *testing.T) {
|
|||||||
{
|
{
|
||||||
PlanFamily: "token_plan",
|
PlanFamily: "token_plan",
|
||||||
PlanCode: "token-plan-lite",
|
PlanCode: "token-plan-lite",
|
||||||
PlanName: "通用 Token Plan Lite",
|
PlanName: "General Token Plan Lite",
|
||||||
Tier: "Lite",
|
Tier: "Lite",
|
||||||
Provider: "Tencent",
|
Provider: "Tencent",
|
||||||
ProviderCN: "腾讯",
|
ProviderCN: "腾讯",
|
||||||
@@ -38,6 +41,7 @@ func TestSubscriptionPlansHandlerReturnsEnvelope(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
)
|
)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/subscription-plans", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/subscription-plans", nil)
|
||||||
@@ -76,3 +80,89 @@ func TestSubscriptionPlansHandlerReturnsEnvelope(t *testing.T) {
|
|||||||
t.Fatalf("unexpected model scope length: %d", len(got.ModelScope))
|
t.Fatalf("unexpected model scope length: %d", len(got.ModelScope))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFrontendHandlerServesIndexAssetsAndSpaFallback(t *testing.T) {
|
||||||
|
distDir := t.TempDir()
|
||||||
|
writeTestFile(t, filepath.Join(distDir, "index.html"), "<html>dashboard</html>")
|
||||||
|
writeTestFile(t, filepath.Join(distDir, "assets", "app.js"), "console.log('ok');")
|
||||||
|
|
||||||
|
mux := newMux(&sql.DB{}, noOpModelsFetcher, noOpPlansFetcher, distDir)
|
||||||
|
|
||||||
|
t.Run("root serves index", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(rec.Body.String(), "dashboard") {
|
||||||
|
t.Fatalf("expected index response, got %q", rec.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("asset serves file", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/assets/app.js", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(rec.Body.String(), "console.log") {
|
||||||
|
t.Fatalf("expected asset response, got %q", rec.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("spa route falls back to index", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/explorer/detail", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(rec.Body.String(), "dashboard") {
|
||||||
|
t.Fatalf("expected SPA fallback, got %q", rec.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing asset returns not found", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/assets/missing.js", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("expected status 404, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("api routes keep precedence", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/models", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func noOpModelsFetcher(context.Context, *sql.DB) ([]modelResponse, error) {
|
||||||
|
return []modelResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func noOpPlansFetcher(context.Context, *sql.DB) ([]subscriptionPlanResponse, error) {
|
||||||
|
return []subscriptionPlanResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTestFile(t *testing.T, path string, contents string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatalf("mkdir %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
||||||
|
t.Fatalf("write %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,15 +27,5 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|
||||||
nginx:
|
|
||||||
image: nginx:alpine
|
|
||||||
volumes:
|
|
||||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
- ./frontend/dist:/usr/share/nginx/html:ro
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
depends_on:
|
|
||||||
- app
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|||||||
@@ -110,31 +110,35 @@ export function normalizeModel(raw: any): Model | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadFallbackModels() {
|
function normalizeModelList(raw: any) {
|
||||||
// latest_models.json is a local runtime snapshot when present.
|
const arr: any[] = Array.isArray(raw) ? raw : (raw?.models || [])
|
||||||
// models.json is the committed fixture fallback kept in the repo.
|
return arr
|
||||||
const sources = [
|
|
||||||
() => import('../data/latest_models.json'),
|
|
||||||
() => import('../data/models.json'),
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const load of sources) {
|
|
||||||
try {
|
|
||||||
const module = await load()
|
|
||||||
const raw = module.default as any
|
|
||||||
const arr: any[] = Array.isArray(raw) ? raw : (raw.models || [])
|
|
||||||
const normalized = arr
|
|
||||||
.map(normalizeModel)
|
.map(normalizeModel)
|
||||||
.filter((model: Model | null): model is Model => model !== null)
|
.filter((model: Model | null): model is Model => model !== null)
|
||||||
if (normalized.length > 0) {
|
|
||||||
return normalized
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadRuntimeSnapshot() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/latest_models.json', { cache: 'no-store' })
|
||||||
|
if (!response.ok) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = await response.json()
|
||||||
|
return normalizeModelList(raw)
|
||||||
} catch {
|
} catch {
|
||||||
// 继续尝试下一个回退源
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
export async function loadFallbackModels() {
|
||||||
|
const snapshot = await loadRuntimeSnapshot()
|
||||||
|
if (snapshot.length > 0) {
|
||||||
|
return snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
const module = await import('../data/models.json')
|
||||||
|
return normalizeModelList(module.default)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatPrice(model: Model, kind: 'input' | 'output') {
|
export function formatPrice(model: Model, kind: 'input' | 'output') {
|
||||||
|
|||||||
235
reports/review/2026-05-20-hermes-project-review.md
Normal file
235
reports/review/2026-05-20-hermes-project-review.md
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
# LLM Intelligence 项目 Review 报告
|
||||||
|
|
||||||
|
- 审查时间:2026-05-20
|
||||||
|
- 审查人:Hermes Agent
|
||||||
|
- 审查对象:D:\project\llm-intelligence
|
||||||
|
- 审查方式:仓库结构盘点 + 关键代码抽样 + 配置/验证链路审查 + counter-evidence/calibration
|
||||||
|
|
||||||
|
## 1. 结论摘要
|
||||||
|
|
||||||
|
总体判断:这是一个“文档/规划活跃,但工程闭环和验证闭环明显不足”的项目。
|
||||||
|
|
||||||
|
成熟度判断:
|
||||||
|
- 当前级别:demo-grade
|
||||||
|
- 不建议给出 production-candidate 或“可稳定上线”的结论
|
||||||
|
|
||||||
|
主导问题:
|
||||||
|
1. 基线不稳定
|
||||||
|
2. 运行/验证环境不自洽
|
||||||
|
3. 文档声称的完成度高于当前可复现度
|
||||||
|
4. 前后端/脚本/部署链路存在多处断裂
|
||||||
|
|
||||||
|
## 2. 审查范围与限制
|
||||||
|
|
||||||
|
已检查:
|
||||||
|
- git 基线状态
|
||||||
|
- 顶层文档与 truth-map 候选
|
||||||
|
- Go 服务端主入口与主要查询逻辑
|
||||||
|
- 前端 Explorer / Dashboard / models 辅助库
|
||||||
|
- docker-compose.yml / Dockerfile / nginx.conf / healthcheck.sh
|
||||||
|
- verify 脚本与 verification_executor.go
|
||||||
|
- 前端测试执行结果
|
||||||
|
|
||||||
|
受限项:
|
||||||
|
- 当前环境中 go 不存在,因此 Go 测试未能实际跑通
|
||||||
|
- 数据库验证未完整复现,因为 verify shell 脚本先被行尾格式问题拦住
|
||||||
|
|
||||||
|
## 3. 基线稳定性
|
||||||
|
|
||||||
|
git status 显示当前工作区存在大面积修改,覆盖:
|
||||||
|
- 顶层文档
|
||||||
|
- Go 服务端
|
||||||
|
- migration
|
||||||
|
- frontend
|
||||||
|
- scripts
|
||||||
|
- tests
|
||||||
|
|
||||||
|
这意味着:
|
||||||
|
- 任何历史“验证通过”“Phase 1 完成”的说法,都不能直接当作当前真相
|
||||||
|
- 当前 review 只能对当前工作区快照负责,不能继承旧报告的高置信结论
|
||||||
|
|
||||||
|
判定:P1 级问题。
|
||||||
|
|
||||||
|
## 4. Truth Map / Source of Truth
|
||||||
|
|
||||||
|
仓库顶层没有 README.md。
|
||||||
|
|
||||||
|
当前 truth candidates 主要包括:
|
||||||
|
- PRD.md
|
||||||
|
- TECHNICAL_DESIGN.md
|
||||||
|
- IMPLEMENTATION_PLAN.md
|
||||||
|
- IMPLEMENTATION_PLAN_v1.1.md
|
||||||
|
- RUNBOOK.md
|
||||||
|
- DEPLOYMENT.md
|
||||||
|
- TASKS.md
|
||||||
|
- GOALS.md
|
||||||
|
- VERIFICATION_REPORT_Sprint1-3.md
|
||||||
|
|
||||||
|
判断:
|
||||||
|
- PRD.md / TECHNICAL_DESIGN.md:更像 target design + 部分当前叙述混合体
|
||||||
|
- RUNBOOK.md / DEPLOYMENT.md:试图充当 current ops truth,但可信度不足
|
||||||
|
- VERIFICATION_REPORT_Sprint1-3.md:更像历史验证叙事,不足以代表当前 truth
|
||||||
|
- 代码与当前可执行环境,优先级高于历史报告
|
||||||
|
|
||||||
|
问题:source-of-truth fragmented。
|
||||||
|
|
||||||
|
## 5. 五层成熟度判断
|
||||||
|
|
||||||
|
### 5.1 文档成熟度
|
||||||
|
|
||||||
|
优点:
|
||||||
|
- 文档密度高,主题覆盖广
|
||||||
|
- 技术设计、产品需求、部署、运维、验收、验证报告较齐全
|
||||||
|
|
||||||
|
问题:
|
||||||
|
- current truth 与 target design 混杂
|
||||||
|
- 顶层缺少统一入口文档
|
||||||
|
- 文档中仍有明显历史/Linux 路径痕迹,如 /home/long/project/llm-intelligence
|
||||||
|
|
||||||
|
结论:
|
||||||
|
- 文档本身:中上
|
||||||
|
- 文档作为当前真相载体:中下
|
||||||
|
|
||||||
|
### 5.2 执行成熟度
|
||||||
|
|
||||||
|
后端锚点:cmd/server/main.go
|
||||||
|
|
||||||
|
优点:
|
||||||
|
- API 入口清晰:/health、/api/v1/models、/api/v1/subscription-plans
|
||||||
|
- 查询结构整体直白
|
||||||
|
|
||||||
|
问题:
|
||||||
|
- 健康检查把“进程活着”和“数据库可用”混在一起
|
||||||
|
- 数据库未配置时整个 API 直接 503
|
||||||
|
- 与前端 fallback 的产品语义不统一
|
||||||
|
- 服务端缺少更完整的超时与边界处理
|
||||||
|
|
||||||
|
前端锚点:
|
||||||
|
- frontend/src/pages/Explorer.tsx
|
||||||
|
- frontend/src/pages/Dashboard.tsx
|
||||||
|
- frontend/src/lib/models.ts
|
||||||
|
|
||||||
|
优点:
|
||||||
|
- Explorer 支持筛选/排序/分页
|
||||||
|
- Dashboard 对模型和套餐做了分开展示
|
||||||
|
- 有静态 fallback 数据方案
|
||||||
|
|
||||||
|
问题:
|
||||||
|
- Explorer 对 fetch 未先检查 response.ok
|
||||||
|
- modality 筛选口径与设计不一致
|
||||||
|
- Dashboard 的“国内厂商”文案与真实统计口径不一致
|
||||||
|
|
||||||
|
结论:执行成熟度中下。
|
||||||
|
|
||||||
|
### 5.3 验证成熟度
|
||||||
|
|
||||||
|
反证非常明显:
|
||||||
|
|
||||||
|
1. Go 测试不可复现
|
||||||
|
- 实测:go test ./...
|
||||||
|
- 结果:go: command not found
|
||||||
|
|
||||||
|
2. 前端测试当前失败
|
||||||
|
- 实测:npm test -- --run
|
||||||
|
- 结果:缺失 @rollup/rollup-linux-x64-gnu
|
||||||
|
|
||||||
|
3. verify shell 脚本当前直接失败
|
||||||
|
- 实测:bash scripts/verify_phase1.sh
|
||||||
|
- 结果:$'\r': command not found、pipefail\r: invalid option name
|
||||||
|
|
||||||
|
结论:
|
||||||
|
- 验证设计意图:中上
|
||||||
|
- 当前可复现性:低
|
||||||
|
- 不能给出“验证闭环成熟”的结论
|
||||||
|
|
||||||
|
### 5.4 运维成熟度
|
||||||
|
|
||||||
|
检查文件:
|
||||||
|
- docker-compose.yml
|
||||||
|
- Dockerfile
|
||||||
|
- nginx.conf
|
||||||
|
- healthcheck.sh
|
||||||
|
- RUNBOOK.md
|
||||||
|
|
||||||
|
问题:
|
||||||
|
- docker-compose.yml 中 DATABASE_URL 看起来像遮罩占位值,不像真实可运行配置
|
||||||
|
- Dockerfile 中前端产物与 compose/nginx 实际消费路径脱节
|
||||||
|
- healthcheck.sh 将“日报存在”混入基础健康判定
|
||||||
|
- RUNBOOK.md 仍带个人化/历史路径
|
||||||
|
|
||||||
|
结论:有雏形,但未形成可信部署闭环。
|
||||||
|
|
||||||
|
### 5.5 生产成熟度
|
||||||
|
|
||||||
|
综合结论:
|
||||||
|
- 文档成熟度:中上
|
||||||
|
- review/治理成熟度:中
|
||||||
|
- 执行成熟度:中下
|
||||||
|
- 验证成熟度:低
|
||||||
|
- 生产成熟度:低
|
||||||
|
|
||||||
|
最终成熟度带:demo-grade
|
||||||
|
|
||||||
|
主导 drift 类型:
|
||||||
|
- validation drift
|
||||||
|
- execution drift
|
||||||
|
- source-of-truth drift
|
||||||
|
|
||||||
|
## 6. 最高风险的假成熟信号
|
||||||
|
|
||||||
|
1. 文档很多、报告很多,但当前环境下基础验证链路并不稳
|
||||||
|
2. 前端 fallback 可能掩盖后端/数据库不可用问题
|
||||||
|
3. RUNBOOK / DEPLOYMENT / compose / healthcheck 存在,但没有形成可一键复现的统一现实
|
||||||
|
4. verification_executor 看起来成熟,但底层 shell 验证资产自身未持续通过
|
||||||
|
|
||||||
|
## 7. 问题清单
|
||||||
|
|
||||||
|
### P1
|
||||||
|
1. 工作区大面积脏修改,导致历史验证/完成度结论失去当前高置信度
|
||||||
|
2. 验证链路不可复现:当前环境无 go,前端测试失败,verify shell 脚本 CRLF 不兼容
|
||||||
|
3. docker-compose.yml 中 app 的 DATABASE_URL 形态可疑,像占位值,不像可运行配置
|
||||||
|
4. Dockerfile 产物路径与 compose/nginx 消费路径脱节,前端部署闭环不完整
|
||||||
|
5. 顶层缺 README,source-of-truth 分散,文档与代码现实存在漂移
|
||||||
|
6. 健康检查、前端 fallback、后端 503 策略未形成一致服务语义
|
||||||
|
|
||||||
|
### P2
|
||||||
|
1. Explorer 未显式检查 response.ok
|
||||||
|
2. modality 筛选与设计模型不一致
|
||||||
|
3. Dashboard 文案“国内厂商”与真实统计口径不符
|
||||||
|
4. writeJSON 错误处理不干净
|
||||||
|
5. 服务端缺少更完整的超时配置
|
||||||
|
6. RUNBOOK.md 中路径/环境信息陈旧
|
||||||
|
|
||||||
|
### P3
|
||||||
|
1. 上下文窗口展示粗糙
|
||||||
|
2. 部分前端/文案细节仍有占位感
|
||||||
|
|
||||||
|
## 8. 建议整改顺序
|
||||||
|
|
||||||
|
第一阶段:先修真相和验证,不要先补新功能
|
||||||
|
1. 补顶层 README.md
|
||||||
|
2. 统一 shell 脚本为 LF,并增加环境 preflight
|
||||||
|
3. 前端依赖重装并跑通 npm test / npm build
|
||||||
|
4. 修复 compose 的数据库配置
|
||||||
|
5. 打通前端构建/运行链路
|
||||||
|
|
||||||
|
第二阶段:修服务语义
|
||||||
|
6. 拆分 liveness / readiness
|
||||||
|
7. 统一“API 不可用时前端是否允许 fallback”的产品语义
|
||||||
|
8. 明确“无 DB 时系统是否仍算部分可用”
|
||||||
|
|
||||||
|
第三阶段:再继续扩展功能
|
||||||
|
9. 修正 modality / 搜索 /指标口径等一致性问题
|
||||||
|
10. 再扩展多源采集与更复杂报告能力
|
||||||
|
|
||||||
|
## 9. 最终 plain-language verdict
|
||||||
|
|
||||||
|
一句话评价:
|
||||||
|
|
||||||
|
这是一个“文档和治理意图明显超前于工程闭环”的项目。
|
||||||
|
|
||||||
|
更直白地说:
|
||||||
|
- 它不像一堆随手拼的代码,说明作者有产品化和治理意识;
|
||||||
|
- 但它还没有进入“可以被高置信度地认定为稳定可运行、稳定可验证、稳定可部署”的阶段。
|
||||||
|
|
||||||
|
最终评级:demo-grade
|
||||||
Reference in New Issue
Block a user