feat(ui): 前端整改 —— RAG 入口产品化、放开 select、高级字段折叠、字段级红框

按"不考虑时间、考虑正确合理"原则逐块实施。

P0-B 放开 select
  - chat_state.js: shouldHideUploadSelect 始终 false; resolveUploadType 尊重用户选择
  - chat.html: 选项目时 select 不再隐藏 + 不再强改 file_type=requirements
  - 客户端 file_type↔扩展名校验(existing_system=.zip, requirements=.xlsx 等)
  - 项目已预置同类型时 confirm() 显式覆盖确认(不藏起入口)

P0-C 高级字段折叠 + 字段级红框
  - 项目抽屉主面板仅留 项目名/显示名;4 个服务器路径字段收进 details.advanced
  - saveDrawerProject 错误时按 ProjectConfigError label 关键字(模板/做成说明书/
    既有系统代码库/既有设计文档目录/项目名)给对应输入加 .invalid 3s 清除
  - 删除按钮 pf-delete 改用 hidden 而非 style.display

P0-A RAG 入口产品化
  - 后端 RagStore.count(scope) 线程安全读加锁
  - GenesisService.rag_stats(sid) 含 except 兜底(count 抛错返回 0)
  - GET /api/sessions/{sid}/rag-stats 端点
  - 前端顶栏:RAG 开关 + RAG 已索引 N 片段 状态徽标 + 开始影响调查按钮
  - 上传 existing_system 后自动 refreshRagStats 刷新徽标
  - 开关持久化到 localStorage(genesis_rag_enabled)

P1-D loadSession 不再隐式覆盖 draftProject
  - activeProject vs draftProject 分层;不一致时由 renderProjectMismatchHint
    提示用户主动"切到该项目"或"保留当前项目"
  - 保留 写入 sessionStorage 标记,避免每次 load 都提示

P1-F 删除抽屉项目 fallback 收敛
  - 先记 deleted 再清 drawerSelected,修复"先 null 后比较"恒假 bug
  - buildWelcome 唯一来源(chat_state.js 单点,前端内联重复移除)

P2-H 杂项
  - send() in-flight 锁防双击
  - input maxlength=2000
  - 启动恢复前校验项目存在 + loading 占位
  - select 关联 label for
  - esc 转义加 引号
  - sid 全程 encodeURIComponent
  - avatar 按 name hash 选色 + 中文首字(Array.from)+ aria-label 含名字

测试:619 passed / 99.01% 99.0% 达标(+8 覆盖 store.count/service.rag_stats/
rag-stats 端点/rag_stats 异常);Node chat_state 9 passed
This commit is contained in:
lhl
2026-08-30 21:16:07 +08:00
parent cc8850cffe
commit 520c9e25b0
10 changed files with 529 additions and 96 deletions
+54
View File
@@ -222,3 +222,57 @@ def test_http_start_impact_use_rag_e2e(tmp_path):
ir = client.get(f"/api/sessions/{sid}/impact-result").json()
# 经 HTTP 真实走到 RAG 路径,摘要标记 rag_enabled
assert ir.get("rag_enabled") is True
def test_http_rag_stats_endpoint(tmp_path):
"""P0Aapp.py /api/sessions/{sid}/rag-stats 仅读端点:上传既有系统后 chunks>0。"""
store = SessionStore(db_path=str(tmp_path / "s.db"))
rag = ImpactRAG(RagStore(str(tmp_path / "rag.db")), FakeEmbedder())
app = create_app(
store=store,
data_root=str(tmp_path / "data"),
engine="fake",
rag=rag,
)
client = TestClient(app)
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
# 上传前:chunks=0
s0 = client.get(f"/api/sessions/{sid}/rag-stats").json()
assert s0["rag_enabled"] is True
assert s0["chunks"] == 0
assert s0["session_id"] == sid
# 上传既有系统 → index_dir 触发 → chunks>0
client.post(f"/api/sessions/{sid}/files",
data={"file_type": "existing_system"},
files={"file": ("existing.zip", _make_existing_zip())})
s1 = client.get(f"/api/sessions/{sid}/rag-stats").json()
assert s1["chunks"] > 0
# 未上传既有系统 → chunks=0 且 200(与 impact-result 行为一致)
def test_rag_stats_count_raises_returns_zero(tmp_path):
"""P0ARagStore.count 抛错时 service.rag_stats 返回 chunks=0,不上抛。"""
store = SessionStore(db_path=str(tmp_path / "s.db"))
rag = ImpactRAG(RagStore(str(tmp_path / "rag.db")), FakeEmbedder())
svc = GenesisService(
store=store,
data_root=str(tmp_path / "data"),
engine=FakeEngine(),
rag=rag,
use_rag=False,
)
s = svc.create_session("u1")
# monkey-patch 一次 count 抛错
original = rag.store.count
def boom(_scope):
raise RuntimeError("db locked")
rag.store.count = boom
try:
stats = svc.rag_stats(s.session_id)
assert stats["chunks"] == 0
assert stats["rag_enabled"] is True
finally:
rag.store.count = original