按"不考虑时间、考虑正确合理"原则逐块实施。
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
89 lines
2.4 KiB
Python
89 lines
2.4 KiB
Python
import math
|
|
from genesis.rag.store import RagStore
|
|
|
|
|
|
def _vec(*ones):
|
|
v = [0.0] * 8
|
|
for i in ones:
|
|
v[i] = 1.0
|
|
n = math.sqrt(sum(x * x for x in v))
|
|
return [x / n for x in v]
|
|
|
|
|
|
def test_search_returns_most_similar_chunk():
|
|
s = RagStore(":memory:")
|
|
s.reset_scope("p1")
|
|
s.add("p1", ["订单模块处理创建", "用户认证登录"], [_vec(0, 1), _vec(4, 5)])
|
|
res = s.search("p1", _vec(0, 1), k=1)
|
|
assert res == ["订单模块处理创建"]
|
|
# k<=0 健壮性
|
|
assert s.search("p1", _vec(0, 1), k=0) == []
|
|
s.close()
|
|
|
|
|
|
def test_reset_scope_clears():
|
|
s = RagStore(":memory:")
|
|
s.reset_scope("p1")
|
|
s.add("p1", ["a"], [_vec(0)])
|
|
s.reset_scope("p1")
|
|
assert s.search("p1", _vec(0), k=3) == []
|
|
# chunks 与 embeddings 长度不一致应抛 ValueError
|
|
try:
|
|
import pytest
|
|
with pytest.raises(ValueError):
|
|
s.add("p1", ["a", "b"], [_vec(0)])
|
|
finally:
|
|
s.close()
|
|
|
|
|
|
def test_concurrent_add_and_search_no_crash():
|
|
import threading
|
|
s = RagStore(":memory:")
|
|
s.reset_scope("p1")
|
|
|
|
def worker(i):
|
|
# 每个 worker 线程内交替执行 add 与 search,让读写真正并发跨线程运行
|
|
for j in range(5):
|
|
s.add("p1", [f"chunk-{i}-{j}"], [_vec(i % 8)])
|
|
res = s.search("p1", _vec(i % 8), k=3)
|
|
assert all(isinstance(r, str) for r in res)
|
|
|
|
try:
|
|
threads = [threading.Thread(target=worker, args=(i,)) for i in range(8)]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
res = s.search("p1", _vec(0), k=3)
|
|
assert len(res) <= 3
|
|
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
|