fix(db): 会话列表按 updated_at 再按 id 排
兼容层 order() 改为追加,不再只留最后一键。 GET /api/sessions 与套餐列表的两次 order() 都会进 SQL。
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# BLOCKED
|
||||
|
||||
## 会话列表多键排序:无 Docker 未跑翻页重叠(2026-09-17,BUG-926)
|
||||
|
||||
- **无 Docker:** 本机 `docker` 不在 PATH。任务书要求 `npm run test:db` 插入三条不同 `updated_at` 的会话,`GET /api/sessions?limit=2` 返回最新两条且 `nextCursor` 翻页拿到第三条、无重叠。**未跑,不得写成通过。**
|
||||
- **替代证据:** `frontend/tests/local-postgres-order.test.ts`:两次 `order()` 生成 `order by "updated_at" desc, "id" desc`;单次 `order()` 输出不变;列表路由排序键与 `sessionCursorFilter` 键一致。
|
||||
|
||||
## 生时校正常驻条:缺「收窄进度」服务端字段(2026-09-16,分支 `codex/cend-rectification-20260916`,T7.1)
|
||||
|
||||
- **触发让步顺序第 5 条。** 任务书要常驻条写「当前区间、宽度、收窄进度、已答题数」四项。前三项里只有**收窄进度**没做,其余三项已上条。
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-17 — 会话列表按最近活动排
|
||||
|
||||
侧栏会话不再按编号乱序。置顶仍在最前,其余按你最近说过话的时间排。套餐列表也恢复按设定顺序。Skill 版本不变。
|
||||
|
||||
## 2026-09-17 — 校正会话上不再出现可用的普通输入框
|
||||
|
||||
在生时校正会话里,普通对话的输入框不再能用:打开校正面之前是禁用的「正在打开生时校正…」;打开失败时可以点「重新打开生时校正」。打在那里的字不会再发到普通咨询。Skill 版本不变。
|
||||
|
||||
@@ -12066,3 +12066,19 @@
|
||||
- 相关记录:BUG-924
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-926 | 会话列表只按 id 排,游标按 updated_at,首页不是最近活动
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-17
|
||||
- 最近更新:2026-09-17
|
||||
- 影响面:`local-postgres-client-core.ts` `order()`、`GET /api/sessions`、`GET /api/payment/packages`
|
||||
- 用户现象:侧栏列表每次进来都不一样;当天新建的校正会话不在首页 50 条里。
|
||||
- 触发条件:打开 `/` 或次级页拉会话列表。
|
||||
- 根因:兼容层 `order()` 只保留最后一次调用。`.order("updated_at").order("id")` 实际只按 `id`;游标却按 `updated_at, id`。套餐列表同样只按 `created_at`。
|
||||
- 修复:`ordering` 改为数组,多次 `order()` 追加,SQL 生成 `order by a, b`。路由调用未改。
|
||||
- 验证:`local-postgres-order.test.ts`。无 Docker,未跑 `test:db` 翻页重叠断言,见 `BLOCKED.md`。
|
||||
- 防复发:两次 `order()` 必须都出现在 SQL 里;列表排序键与 `sessionCursorFilter` 键一致。
|
||||
- 相关记录:BUG-553
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# PROGRESS · 会话列表单一数据源(2026-09-17)
|
||||
|
||||
工作树:`.worktrees/session-list-single-source-20260917`
|
||||
分支:`codex/session-list-single-source-20260917`
|
||||
基线:`origin/staging` @ `6d81062b`(composer-guard 已合入)
|
||||
|
||||
## 任务状态
|
||||
|
||||
| 任务 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| T1 多键排序 BUG-926 | 完成 | `order()` 追加;SQL `order by a, b`;套餐路由未改代码 |
|
||||
| T2 常驻外壳 layout BUG-927 | 未做 | 路由组重排 + SessionListProvider,本轮未开工 |
|
||||
| T3 空会话不入列 BUG-928 | 未做 | 按让步顺序在 T2 之后 |
|
||||
| T4 标题副标题 BUG-929 | 未做 | |
|
||||
| T5 记录 | 部分 | T1 的 BUG-926 / BLOCKED / CHANGELOG 已写 |
|
||||
|
||||
## 测试(T1)
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| `local-postgres-order.test.ts` + query-value | 7 passed / 0 failed |
|
||||
| `npm run test:db` | 无 Docker,见 `BLOCKED.md` |
|
||||
@@ -131,7 +131,7 @@
|
||||
| `TASK-rectification-p0-fix-20260915.md` | `PROGRESS-rectification-p0-fix-20260915.md` | **验收修复单**:`f51e494c` 六条缺陷全部实现且方式正确,但 `page.tsx` 从 1951 涨到 1964 行,撞了 `chart-view-route.test.ts` 的 `<= 1951` 上限(AGENTS.md §6 增长冻结)。全量 fail 32→33,就这一条。门禁红很可能是 staging 停在 `2d7698ea`、6 个提交未部署的原因。修法是把 BUG-705 的十来行接线搬出 page.tsx,不放宽上限 | 待验收 | `codex/rectification-p0-fix-20260915` |
|
||||
| `TASK-settings-dialog-size-and-nav-20260915.md` | — | **复发单**:设置弹窗四个分区尺寸仍随内容跳变(BUG-698,复发自 BUG-554——旧防复发只查「有没有写 height」,查不到「写了没生效」);首要嫌疑是 `.settings-modal` 的 `dvh` 没有 `vh` 回退,不支持时整条 `height` 作废退化成内容高度,需先复现确认。另按产品要求去掉分区菜单左侧强调条,并拆开与悬停共用的选中态 | 待领取 | `codex/settings-dialog-size-and-nav-20260915` |
|
||||
| `TASK-consult-followup-tool-contract-20260917.md` | `PROGRESS-consult-followup-tool-contract-20260917.md` | 真机:申报时段会话连发「?」「你在说什么鬼」都 `run.failed runtime_contract_incomplete`,回执无任何 `tool` 步骤。根因是 Agent 系统指令写明「简单追问可复用已有 packet / context、不调工具」,而 `contractReady()` 要求每次请求恰好一次成功排盘调用;「已有 packet」跨请求并不存在(缓存只在单次请求内)。本命与窗口两个 Agent 同构。**产品拍板方案 1**:每轮必调工具(BUG-922 删例外句 + BUG-923 第 0 步 `toolChoice: required`);否决「没调工具就走不扣点纯对话」。第一轮正经问题为何失败留 T4 取证(回执只在 web 容器日志) | 待验收 | `codex/consult-followup-tool-contract-20260917` |
|
||||
| `TASK-session-list-single-source-20260917.md` | — | 会话列表一处数据源:本地 PG 兼容层 `order()` 只保留最后一键,`/api/sessions` 实际按 `id` 排、与游标不一致;`/` 与次级页两份数据源、`/` 每次回来重启动(产品拍板:首页与三个次级页进同一路由组,侧栏外壳与列表 provider 常驻 layout,不做 store);空「新对话」落库堆积(首页 50 条里 28 条);标题类别在后、同名靠墙钟 HH:MM。串行在 composer-guard 单之后。BUG 段 926 起 | 待领取 | — |
|
||||
| `TASK-session-list-single-source-20260917.md` | `PROGRESS-session-list-single-source-20260917.md` | 会话列表一处数据源:本地 PG 兼容层 `order()` 只保留最后一键,`/api/sessions` 实际按 `id` 排、与游标不一致;`/` 与次级页两份数据源、`/` 每次回来重启动(产品拍板:首页与三个次级页进同一路由组,侧栏外壳与列表 provider 常驻 layout,不做 store);空「新对话」落库堆积(首页 50 条里 28 条);标题类别在后、同名靠墙钟 HH:MM。串行在 composer-guard 单之后。BUG 段 926 起 | 执行中(T1 已合入) | `codex/session-list-single-source-20260917` |
|
||||
|
||||
### 个人报告
|
||||
|
||||
|
||||
@@ -39,6 +39,15 @@ function identifier(value: string): string {
|
||||
return `"${normalized}"`;
|
||||
}
|
||||
|
||||
export function formatOrderClause(
|
||||
ordering: readonly { column: string; ascending: boolean }[],
|
||||
): string {
|
||||
if (ordering.length === 0) return "";
|
||||
return ` order by ${ordering
|
||||
.map((item) => `${identifier(item.column)} ${item.ascending ? "asc" : "desc"}`)
|
||||
.join(", ")}`;
|
||||
}
|
||||
|
||||
export function upsertConflictColumns(options?: { onConflict?: string }): string[] {
|
||||
return (options?.onConflict ?? "")
|
||||
.split(",")
|
||||
@@ -184,8 +193,7 @@ class LocalPostgresQueryBuilder implements PromiseLike<QueryResult> {
|
||||
private selectedColumns: string[] | null = null;
|
||||
private mutation: Mutation | null = null;
|
||||
private readonly filters: Filter[] = [];
|
||||
private ordering: Readonly<{ column: string; ascending: boolean }> | null =
|
||||
null;
|
||||
private ordering: Array<{ column: string; ascending: boolean }> = [];
|
||||
private rowLimit: number | null = null;
|
||||
private abort: AbortSignal | null = null;
|
||||
private cardinality: "many" | "single" | "maybeSingle" = "many";
|
||||
@@ -289,7 +297,7 @@ class LocalPostgresQueryBuilder implements PromiseLike<QueryResult> {
|
||||
|
||||
order(column: string, options: { ascending?: boolean } = {}) {
|
||||
identifier(column);
|
||||
this.ordering = { column, ascending: options.ascending !== false };
|
||||
this.ordering.push({ column, ascending: options.ascending !== false });
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -389,9 +397,7 @@ class LocalPostgresQueryBuilder implements PromiseLike<QueryResult> {
|
||||
.join(", ");
|
||||
sql = `select ${selected} from public.${identifier(this.table)}`;
|
||||
sql += this.filterClause(parameters, types);
|
||||
if (this.ordering) {
|
||||
sql += ` order by ${identifier(this.ordering.column)} ${this.ordering.ascending ? "asc" : "desc"}`;
|
||||
}
|
||||
sql += formatOrderClause(this.ordering);
|
||||
if (this.rowLimit !== null) sql += ` limit ${this.rowLimit}`;
|
||||
} else if (
|
||||
this.mutation.kind === "insert" ||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { formatOrderClause } from "../src/lib/db/local-postgres-client-core.ts";
|
||||
|
||||
const listRoute = readFileSync(new URL("../src/app/api/sessions/route.ts", import.meta.url), "utf8");
|
||||
const cursor = readFileSync(new URL("../src/lib/session-cursor.ts", import.meta.url), "utf8");
|
||||
const packagesRoute = readFileSync(new URL("../src/app/api/payment/packages/route.ts", import.meta.url), "utf8");
|
||||
const core = readFileSync(new URL("../src/lib/db/local-postgres-client-core.ts", import.meta.url), "utf8");
|
||||
|
||||
test("a single order() still emits one key", () => {
|
||||
assert.equal(
|
||||
formatOrderClause([{ column: "id", ascending: false }]),
|
||||
' order by "id" desc',
|
||||
);
|
||||
assert.equal(
|
||||
formatOrderClause([{ column: "sort_order", ascending: true }]),
|
||||
' order by "sort_order" asc',
|
||||
);
|
||||
});
|
||||
|
||||
test("two order() calls keep both keys in call order", () => {
|
||||
assert.equal(
|
||||
formatOrderClause([
|
||||
{ column: "updated_at", ascending: false },
|
||||
{ column: "id", ascending: false },
|
||||
]),
|
||||
' order by "updated_at" desc, "id" desc',
|
||||
);
|
||||
assert.equal(
|
||||
formatOrderClause([
|
||||
{ column: "sort_order", ascending: true },
|
||||
{ column: "created_at", ascending: true },
|
||||
]),
|
||||
' order by "sort_order" asc, "created_at" asc',
|
||||
);
|
||||
assert.match(core, /this\.ordering\.push\(\{ column, ascending: options\.ascending !== false \}\)/);
|
||||
assert.match(core, /sql \+= formatOrderClause\(this\.ordering\);/);
|
||||
});
|
||||
|
||||
test("session list sort keys match the cursor filter keys", () => {
|
||||
assert.match(listRoute, /\.order\("updated_at", \{ ascending: false \}\)\s*\.order\("id", \{ ascending: false \}\)/);
|
||||
assert.match(cursor, /updated_at\.lt\.\$\{updatedAt\},and\(updated_at\.eq\.\$\{updatedAt\},id\.lt\.\$\{id\}\)/);
|
||||
assert.match(packagesRoute, /\.order\("sort_order"\)\s*\.order\("created_at"\)/);
|
||||
});
|
||||
Reference in New Issue
Block a user