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:
+11
-13
@@ -16,21 +16,17 @@ test('autoBindProject: 已选有效项目保持不变', () => {
|
||||
assert.equal(S.autoBindProject('A', [{ name: 'A' }]), 'A');
|
||||
});
|
||||
|
||||
test('shouldHideUploadSelect: 未绑定任何项目时显示下拉', () => {
|
||||
test('shouldHideUploadSelect: 始终 false(选项目时也允许补传其它类型)', () => {
|
||||
assert.equal(S.shouldHideUploadSelect(null, null), false);
|
||||
assert.equal(S.shouldHideUploadSelect(null, 'A'), false);
|
||||
assert.equal(S.shouldHideUploadSelect('A', null), false);
|
||||
assert.equal(S.shouldHideUploadSelect('A', 'A'), false);
|
||||
});
|
||||
|
||||
test('shouldHideUploadSelect: 已绑定项目(draft 或 active)时隐藏下拉', () => {
|
||||
assert.equal(S.shouldHideUploadSelect(null, 'A'), true);
|
||||
assert.equal(S.shouldHideUploadSelect('A', null), true);
|
||||
});
|
||||
|
||||
test('resolveUploadType: 绑定项目时强制 requirements', () => {
|
||||
assert.equal(S.resolveUploadType(null, 'A', 'template'), 'requirements');
|
||||
});
|
||||
|
||||
test('resolveUploadType: 未绑定项目时保留用户选择', () => {
|
||||
test('resolveUploadType: 始终尊重用户下拉选择(不再被项目绑定强制为 requirements)', () => {
|
||||
assert.equal(S.resolveUploadType(null, 'A', 'template'), 'template');
|
||||
assert.equal(S.resolveUploadType(null, null, 'template'), 'template');
|
||||
assert.equal(S.resolveUploadType('A', null, 'existing_system'), 'existing_system');
|
||||
});
|
||||
|
||||
test('computeDrawerSnapshot: 去空白并以单元分隔符连接', () => {
|
||||
@@ -46,6 +42,8 @@ test('buildWelcome: 无项目提示新建/选择项目', () => {
|
||||
assert.match(S.buildWelcome(null), /请新建项目|选择项目/);
|
||||
});
|
||||
|
||||
test('buildWelcome: 有项目提示由项目提供模板/规则/代码库', () => {
|
||||
assert.match(S.buildWelcome('A'), /项目「A」/);
|
||||
test('buildWelcome: 有项目时提示由项目提供模板/规则/代码库', () => {
|
||||
const txt = S.buildWelcome('A');
|
||||
assert.match(txt, /项目「A」/);
|
||||
assert.match(txt, /要件定义/);
|
||||
});
|
||||
|
||||
@@ -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):
|
||||
"""P0‑A:app.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):
|
||||
"""P0‑A:RagStore.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
|
||||
|
||||
@@ -59,3 +59,30 @@ def test_concurrent_add_and_search_no_crash():
|
||||
assert all(isinstance(r, str) for r in res)
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
def test_count_empty_scope_returns_zero():
|
||||
s = RagStore(":memory:")
|
||||
assert s.count("missing") == 0
|
||||
|
||||
|
||||
def test_count_after_add_returns_chunk_count():
|
||||
s = RagStore(":memory:")
|
||||
s.add("p1", ["a", "b", "c"], [_vec(0), _vec(1), _vec(2)])
|
||||
assert s.count("p1") == 3
|
||||
|
||||
|
||||
def test_count_scopes_are_isolated():
|
||||
s = RagStore(":memory:")
|
||||
s.add("p1", ["a"], [_vec(0)])
|
||||
s.add("p2", ["b", "c"], [_vec(1), _vec(2)])
|
||||
assert s.count("p1") == 1
|
||||
assert s.count("p2") == 2
|
||||
|
||||
|
||||
def test_count_after_reset_returns_zero():
|
||||
s = RagStore(":memory:")
|
||||
s.add("p1", ["a", "b"], [_vec(0), _vec(1)])
|
||||
assert s.count("p1") == 2
|
||||
s.reset_scope("p1")
|
||||
assert s.count("p1") == 0
|
||||
|
||||
@@ -12,6 +12,9 @@ import pytest
|
||||
|
||||
from genesis.server.store import SessionStore, ProjectsStore
|
||||
from genesis.server.service import GenesisService, ServiceStepError, FileTypeError
|
||||
from genesis.rag.embeddings import FakeEmbedder
|
||||
from genesis.rag.impact_rag import ImpactRAG
|
||||
from genesis.rag.store import RagStore
|
||||
|
||||
_SAMPLE = Path(__file__).resolve().parents[1] / "sample"
|
||||
|
||||
@@ -334,3 +337,34 @@ def test_confirm_impact_wrong_state_raises(tmp_path):
|
||||
# writing 态直接确认影响 → 报错(service 167)
|
||||
with pytest.raises(ServiceStepError):
|
||||
svc.confirm_impact(s.session_id)
|
||||
|
||||
|
||||
def test_rag_stats_without_rag_returns_disabled_zero(tmp_path):
|
||||
svc = GenesisService(
|
||||
store=SessionStore(db_path=str(tmp_path / "s.db")),
|
||||
data_root=str(tmp_path / "data"),
|
||||
engine=FakeEngine(),
|
||||
rag=None,
|
||||
)
|
||||
s = svc.create_session("u1")
|
||||
stats = svc.rag_stats(s.session_id)
|
||||
assert stats == {"session_id": s.session_id, "rag_enabled": False, "chunks": 0}
|
||||
|
||||
|
||||
def test_rag_stats_with_rag_reflects_indexed_chunks(tmp_path):
|
||||
svc = GenesisService(
|
||||
store=SessionStore(db_path=str(tmp_path / "s.db")),
|
||||
data_root=str(tmp_path / "data"),
|
||||
engine=FakeEngine(),
|
||||
rag=ImpactRAG(RagStore(str(tmp_path / "rag.db")), FakeEmbedder()),
|
||||
use_rag=False,
|
||||
)
|
||||
s = svc.create_session("u1")
|
||||
# 空 scope → 0 片段
|
||||
assert svc.rag_stats(s.session_id)["chunks"] == 0
|
||||
# 索引 2 个片段
|
||||
svc.rag.index(s.session_id, [("f.java", "a"), ("f2.java", "b")])
|
||||
stats = svc.rag_stats(s.session_id)
|
||||
assert stats["rag_enabled"] is True
|
||||
assert stats["chunks"] == 2
|
||||
assert stats["session_id"] == s.session_id
|
||||
|
||||
Reference in New Issue
Block a user