Compare commits

...

2 Commits

Author SHA1 Message Date
b430fb9301 fix deployment and frontend build regressions 2026-05-21 15:30:24 +08:00
root
31f1b510c3 docs: add Hermes project review report 2026-05-20 16:39:31 +08:00
7 changed files with 511 additions and 99 deletions

View File

@@ -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 版本 >= 20npm 版本 >= 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"
```
--- ---

View File

@@ -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`

View File

@@ -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 (

View File

@@ -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)
}
}

View File

@@ -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:

View File

@@ -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') {

View 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. 顶层缺 READMEsource-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