Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68670b2819 |
@@ -1,5 +1,11 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-29 — 咨询首字前少等最多 3 秒
|
||||
|
||||
- 判断「是不是寒暄」改为和扣点、记下问题同时进行,不再排在它们后面;真问题首字前最多快约 3 秒。
|
||||
- 计费不变:寒暄照旧先扣后退、免费;扣点失败时照旧提示,不会多扣。
|
||||
- Skill 版本不变(BUG-1499,issue #162)。
|
||||
|
||||
## 2026-10-29 — 咨询回答快约三分之一
|
||||
|
||||
- 咨询里写回答那一步,模型的思考量默认改为「低」:对比测试里思考量少约一半,一轮回答从约 34 秒降到约 22 秒,回答质量逐份并排读过没有变差(issue #134,产品选 A)。
|
||||
|
||||
@@ -20477,3 +20477,19 @@
|
||||
- 关联:BUG-1481(第一次记下这个失败)、BUG-1479(#130,负缓存与每测试目录)。
|
||||
- 复发自:BUG-1481 记下的同一项失败。当时的处理(每测试单独目录)没拦住,因为根因是后台任务跨测试写入,不在目录共用上。
|
||||
- 修复版本:分支 `codex/bug-1498-executor-isolation`。
|
||||
|
||||
|
||||
## BUG-1499 | 咨询首字前要白等分类:判断寒暄要等扣点、写入问题都做完才开始
|
||||
|
||||
- 状态:resolved(代码 + 回归测试;待部署)
|
||||
- 首次发现 / 最近更新:2026-10-28 / 2026-10-29(issue #118 代码体检第 7 条 → #135 → #162)
|
||||
- 影响面:`frontend/src/app/api/consult/route.ts::runTurn`、`frontend/src/lib/consultation-route-service.ts`(`beforeReserve` 上下文多带 `subject`)。
|
||||
- 现象:每个咨询问题首字前,「是不是寒暄」的分类(最多 3 秒)排在扣点、写入问题之后才开始,三步串行。
|
||||
- 触发条件:任何未带 entrypoint 的咨询轮次。
|
||||
- 根因:分类调用写在 `appendQuestion()` 之后;它只需要用户原话、历史、称呼和会话模型,不依赖扣点或写入的结果。
|
||||
- 修复:审核放行后,在 `beforeReserve` 里开始分类,与扣点、写入同时进行;写入完成后再等分类结果,按原来的寒暄 / 正常两条路走。用的模型是扣点选中的同一个 `sessionModel` 对象。计费顺序、寒暄先扣后退(`complete_consultation_free`)与金额都不变;扣点失败时分类结果丢弃,最多白调一次分类模型(有 3 秒上限,失败默认按正常问题)。新增计时 `prep.classify_wait`(写入后还要等分类多久)。
|
||||
- 验证:前端相关 71 条测试过;全量 5339 条里 25 条失败,与改动前同一批(Docker / 数据库 / rsync 环境),无新增;tsc 0 错、lint 0 错。
|
||||
- 防复发:`consultation-smalltalk.test.ts` 锁住分类在审核之后、扣点之前开始,且用扣点选中的同一个模型对象;寒暄分支仍走 `completeSmalltalk`。
|
||||
- 关联:BUG-976(寒暄先扣后退)、BUG-1074(开流前的串行链)、#134(同批提速)。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/consult-classify-parallel-20261029`。
|
||||
|
||||
@@ -547,3 +547,4 @@
|
||||
| `TASK-upstream-sync9-20261028.md` | `PROGRESS-upstream-sync9-20261028.md` | **第 1 次打回(快速门 1 败:Darakaraka 旧测试锁错值,见 `TASK-upstream-sync9-fix-20261028.md`)** · **#141 上游同步第九轮**:甲批九项已做完并推 `codex/upstream-sync9-20261028`;校正 20 条和 900 条与上一份回放逐条相同;乙批仍等 #137 | 待验收 | `codex/upstream-sync9-20261028` |
|
||||
| `TASK-upstream-sync9b-20261028.md` | `PROGRESS-upstream-sync9b-20261028.md` | **第 1 次打回(冻结契约排除清单删了第八轮两条,见 `TASK-upstream-sync9b-fix-20261028.md`)** · **#159 上游同步第九轮乙批(产品选 A)**:Sarpa Drekkana 补成五段、第 22 号 Drekkana 改为 D3 第 8 宫;改冻结文件 `varga.py`,校正 20 + 900 与上一份逐条相同后按 ERR-110 重新冻结。Narayana 小运强度口径(B2)不做,另问产品 | 待验收 | `codex/upstream-sync9b-20261028` |
|
||||
| — | — | **#134 咨询答题默认低思考量(产品选 A)**:`CONSULT_ANSWER_REASONING_EFFORT` 不设时默认 `low`,设 `off` 恢复原请求体(Claude 直接执行) | 已推 staging | `codex/answer-effort-default-20261029` |
|
||||
| — | — | **#162 咨询分类与扣点同时进行(产品同意)**:审核放行后分类与扣点、写入并行;计费与寒暄退点不变(BUG-1499,Claude 直接执行) | 已推 staging | `codex/consult-classify-parallel-20261029` |
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# 真机走查 · 咨询分类与扣点同时进行(issue #162)
|
||||
|
||||
| # | 操作 | 应该看到 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 用一张星盘问一个具体问题 | 正常回答;首字出来比以前快一点 |
|
||||
| 2 | 只发「你好」或「谢谢」 | 回一句寒暄;点数余额不变(先扣后退) |
|
||||
| 3 | 点数不足时发一个问题 | 照旧提示「咨询点数不足」,不会多扣 |
|
||||
@@ -537,6 +537,49 @@ export async function POST(request: Request) {
|
||||
},
|
||||
});
|
||||
let prepared: PreparedConsultationRoute<ModelSelection, ConsultationPlan>;
|
||||
// Issue #162: classification starts once moderation passes and runs beside
|
||||
// reserve + append instead of after them. Billing order is unchanged: a
|
||||
// smalltalk turn is still reserved, then refunded by completeSmalltalk.
|
||||
// The classifier fails open and is capped at 3 s, so an unused early call
|
||||
// (reserve refused, append failed) costs at most that one call.
|
||||
const earlyClassification: { turn: ReturnType<typeof classifyConsultationTurn> | null } = { turn: null };
|
||||
let classifyStarted = 0;
|
||||
function startClassification(name: string) {
|
||||
classifyStarted = Date.now();
|
||||
return classifyConsultationTurn({
|
||||
model: sessionModel,
|
||||
question: visibleQuestion,
|
||||
history: chatSession.messages,
|
||||
name,
|
||||
signal: request.signal,
|
||||
onObservation(observation) {
|
||||
if (!observation.late) {
|
||||
classificationUsage = observation.usage ?? {};
|
||||
classificationOutcome = observation.outcome;
|
||||
classificationDurationMs = observation.durationMs;
|
||||
}
|
||||
const inputTokens = observation.usage?.inputTokens ?? 0;
|
||||
const outputTokens = observation.usage?.outputTokens ?? 0;
|
||||
logAgentObservability({
|
||||
requestId, sessionId,
|
||||
agentVersion: "consultation-smalltalk-v1",
|
||||
modelVersion: String(sessionModel.configVersion),
|
||||
policyVersion: "consultation-smalltalk-v1",
|
||||
toolCalls: [],
|
||||
classification: { durationMs: observation.durationMs },
|
||||
contractPhases: [{
|
||||
phase: observation.late ? "classification.late_usage" : `classification.${observation.outcome}`,
|
||||
durationMs: observation.durationMs,
|
||||
status: observation.outcome === "smalltalk" || observation.outcome === "consult" ? "completed" : "failed",
|
||||
}],
|
||||
...(observation.usage ? { inputTokens, outputTokens,
|
||||
costMicrousd: Math.round((inputTokens * (sessionModel.inputCostMicrousdPerMillion ?? 0)
|
||||
+ outputTokens * (sessionModel.outputCostMicrousdPerMillion ?? 0)) / 1_000_000) } : {}),
|
||||
billingSettlementResult: "not_applicable",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
const prepareStarted = Date.now();
|
||||
try {
|
||||
prepared = await prepareConsultationRoute<ModelSelection, ConsultationPlan>({
|
||||
@@ -570,7 +613,7 @@ export async function POST(request: Request) {
|
||||
return data;
|
||||
},
|
||||
loadCandidateRange: (profileUserId) => loadReportCandidateRange(accounting, { userId: profileUserId }),
|
||||
beforeReserve: async ({ consultationMode, serverChart }) => {
|
||||
beforeReserve: async ({ consultationMode, serverChart, declaredWindow, subject }) => {
|
||||
if (serverChart && consultationMode !== "declared_birth_window") prewarmConsultPacketForTurn(serverChart.toolInput);
|
||||
prepPhases.push({
|
||||
phase: "prep.subject",
|
||||
@@ -579,6 +622,9 @@ export async function POST(request: Request) {
|
||||
});
|
||||
const inputModeration = await settleInputModeration();
|
||||
if (inputModeration?.verdict === "block") throw new ConsultationInputBlockedError(inputModeration);
|
||||
if (parsed.data.entrypoint === undefined) {
|
||||
earlyClassification.turn = startClassification(subject?.name || serverChart?.name || declaredWindow?.name || "");
|
||||
}
|
||||
return createConsultationPlan({
|
||||
userIntent: parsed.data.question,
|
||||
theme: consultationTheme,
|
||||
@@ -718,42 +764,14 @@ export async function POST(request: Request) {
|
||||
.eq("id", sessionId)
|
||||
.eq("user_id", userId)
|
||||
.maybeSingle();
|
||||
const classifyStarted = Date.now();
|
||||
const turn = parsed.data.entrypoint === undefined
|
||||
? await classifyConsultationTurn({
|
||||
model: selectedModel,
|
||||
question: visibleQuestion,
|
||||
history: chatSession.messages,
|
||||
name: subjectName,
|
||||
signal: request.signal,
|
||||
onObservation(observation) {
|
||||
if (!observation.late) {
|
||||
classificationUsage = observation.usage ?? {};
|
||||
classificationOutcome = observation.outcome;
|
||||
classificationDurationMs = observation.durationMs;
|
||||
}
|
||||
const inputTokens = observation.usage?.inputTokens ?? 0;
|
||||
const outputTokens = observation.usage?.outputTokens ?? 0;
|
||||
logAgentObservability({
|
||||
requestId, sessionId,
|
||||
agentVersion: "consultation-smalltalk-v1",
|
||||
modelVersion: String(selectedModel.configVersion),
|
||||
policyVersion: "consultation-smalltalk-v1",
|
||||
toolCalls: [],
|
||||
classification: { durationMs: observation.durationMs },
|
||||
contractPhases: [{
|
||||
phase: observation.late ? "classification.late_usage" : `classification.${observation.outcome}`,
|
||||
durationMs: observation.durationMs,
|
||||
status: observation.outcome === "smalltalk" || observation.outcome === "consult" ? "completed" : "failed",
|
||||
}],
|
||||
...(observation.usage ? { inputTokens, outputTokens,
|
||||
costMicrousd: Math.round((inputTokens * (selectedModel.inputCostMicrousdPerMillion ?? 0)
|
||||
+ outputTokens * (selectedModel.outputCostMicrousdPerMillion ?? 0)) / 1_000_000) } : {}),
|
||||
billingSettlementResult: "not_applicable",
|
||||
});
|
||||
},
|
||||
})
|
||||
: { kind: "consult" as const };
|
||||
// Started in beforeReserve, beside reserve + append (issue #162).
|
||||
const classifyWaitStarted = Date.now();
|
||||
const turn = earlyClassification.turn ? await earlyClassification.turn : { kind: "consult" as const };
|
||||
prepPhases.push({
|
||||
phase: "prep.classify_wait",
|
||||
durationMs: Math.max(0, Date.now() - classifyWaitStarted),
|
||||
status: "completed",
|
||||
});
|
||||
prepPhases.push({
|
||||
phase: "prep.classify",
|
||||
durationMs: classificationDurationMs ?? Math.max(0, Date.now() - classifyStarted),
|
||||
|
||||
@@ -138,6 +138,8 @@ type ConsultationPreReserveContext = Readonly<{
|
||||
serverChart: ServerChartConsultation | null;
|
||||
declaredWindow: DeclaredBirthWindowConsultation | null;
|
||||
generalDailyReference: GeneralDailyReference | null;
|
||||
/** The bound subject, so pre-reserve work can address it by name (issue #162). */
|
||||
subject: PreparedConsultationSubject | null;
|
||||
}>;
|
||||
|
||||
type PrepareConsultationRouteInput<Reservation> = Readonly<{
|
||||
@@ -615,6 +617,7 @@ export async function prepareConsultationRoute<Reservation, GuardResult>(
|
||||
serverChart,
|
||||
declaredWindow,
|
||||
generalDailyReference,
|
||||
subject,
|
||||
}) as Awaited<GuardResult>
|
||||
: undefined;
|
||||
const reservation = await input.reserve();
|
||||
|
||||
@@ -279,7 +279,11 @@ test("the consult route opens the stream before subject reads, reservation, ques
|
||||
"prepareConsultationRoute<ModelSelection, ConsultationPlan>({",
|
||||
"consultSettlement.reserveCredit",
|
||||
"consultSettlement.appendQuestion",
|
||||
"await classifyConsultationTurn({",
|
||||
// 原值: "await classifyConsultationTurn({"
|
||||
// 新值: 分类调用与等待结果两处都在流内
|
||||
// 原因: issue #162 分类提前到 beforeReserve 开始,结果在写入问题之后等待
|
||||
"return classifyConsultationTurn({",
|
||||
"await earlyClassification.turn",
|
||||
"insufficient ? 402 : 503",
|
||||
'error: "暂时无法生成解读"',
|
||||
]) {
|
||||
|
||||
@@ -77,5 +77,8 @@ test("a consult turn prewarms the card packet with the same identity the card re
|
||||
{ apiBase: "http://engine.test", fetch: (async () => { throw new Error("down"); }) as unknown as typeof fetch },
|
||||
);
|
||||
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
|
||||
assert.match(route, /beforeReserve: async \(\{ consultationMode, serverChart \}\) => \{\n\s+if \(serverChart && consultationMode !== "declared_birth_window"\) prewarmConsultPacketForTurn\(serverChart\.toolInput\);/);
|
||||
// 原值: beforeReserve 只解构 consultationMode、serverChart
|
||||
// 新值: 同时解构 declaredWindow、subject;预热仍是第一句
|
||||
// 原因: issue #162 beforeReserve 多取 declaredWindow、subject,用来在审核后提前开始分类
|
||||
assert.match(route, /beforeReserve: async \(\{ consultationMode, serverChart, declaredWindow, subject \}\) => \{\n\s+if \(serverChart && consultationMode !== "declared_birth_window"\) prewarmConsultPacketForTurn\(serverChart\.toolInput\);/);
|
||||
});
|
||||
|
||||
@@ -163,7 +163,10 @@ test("the title read starts after the question is saved and is awaited at the ol
|
||||
const append = route.indexOf("consultSettlement.appendQuestion()");
|
||||
assert.match(settlement, /accounting\.rpc\("append_consultation_question"/);
|
||||
const title = route.indexOf('.select("title")');
|
||||
const classify = route.indexOf("await classifyConsultationTurn({");
|
||||
// 原值: 分类以 "await classifyConsultationTurn({" 在标题读取之后开始
|
||||
// 新值: 分类在 beforeReserve 里提前开始,这里在标题读取之后等它的结果
|
||||
// 原因: issue #162 分类与扣点、写入同时进行;标题读取仍在保存成功之后,等待次序不变
|
||||
const classify = route.indexOf("await earlyClassification.turn");
|
||||
const awaited = route.indexOf("await titleRead");
|
||||
assert.ok(append > 0 && title > append && classify > title && awaited > classify);
|
||||
});
|
||||
|
||||
@@ -222,8 +222,19 @@ test("smalltalk views never reconstruct an invented settled execution timeline",
|
||||
});
|
||||
test("route gates all special entries before a single selected-model classifier and keeps tool contracts", () => {
|
||||
const route = source("../src/app/api/consult/route.ts");
|
||||
assert.match(route, /parsed.data.entrypoint === undefined\s*\? await classifyConsultationTurn/);
|
||||
assert.match(route, /classifyConsultationTurn\(\{\s*model: selectedModel,\s*question: visibleQuestion/);
|
||||
// 原值: 分类在扣点、写入之后以 `entrypoint === undefined ? await classifyConsultationTurn` 串行开始,用 selectedModel
|
||||
// 新值: 审核放行后在 beforeReserve 里开始分类,与扣点、写入同时进行;用的 sessionModel 就是扣点选中的同一个对象
|
||||
// 原因: issue #162 去掉首字前最多 3 秒的串行等待;计费顺序与寒暄退点不变
|
||||
assert.match(route, /if \(parsed\.data\.entrypoint === undefined\) \{\s*earlyClassification\.turn = startClassification\(/);
|
||||
assert.match(route, /classifyConsultationTurn\(\{\s*model: sessionModel,\s*question: visibleQuestion/);
|
||||
assert.match(route, /\(modelId\) => sessionModel\?\.id === modelId \? sessionModel : null/);
|
||||
const beforeReserve = route.slice(route.indexOf("beforeReserve: async"), route.indexOf("reserve: async () =>"));
|
||||
assert.ok(
|
||||
beforeReserve.indexOf('if (inputModeration?.verdict === "block") throw') < beforeReserve.indexOf("earlyClassification.turn = startClassification("),
|
||||
"classification starts only after moderation lets the input through",
|
||||
);
|
||||
assert.ok(route.indexOf("earlyClassification.turn = startClassification(") < route.indexOf("consultSettlement.reserveCredit"));
|
||||
assert.match(route, /await earlyClassification\.turn/);
|
||||
assert.equal((route.match(/requireTool: true/g) ?? []).length, 2);
|
||||
const smalltalk = route.slice(route.indexOf('if (turn.kind === "smalltalk")'), route.indexOf("const expectedTitle"));
|
||||
const settlement = source("../src/lib/consult/settlement.ts");
|
||||
|
||||
@@ -167,7 +167,10 @@ test("consultation plans are server-owned and bounded", () => {
|
||||
prepareCall.slice(beforeReserveAt, reserveAt),
|
||||
// 原值:参数只解构 consultationMode;新值:还解构 serverChart,用来在扣点前不等待地预热数据卡
|
||||
// 原因:issue #129 每天首问不在回答路径上现算整包;createConsultationPlan 仍在这个回调里、仍早于 reserve
|
||||
/beforeReserve:\s*async\s*\(\{\s*consultationMode,\s*serverChart\s*\}\)[\s\S]*createConsultationPlan\(\{/,
|
||||
// 原值: beforeReserve 只解构 consultationMode、serverChart
|
||||
// 新值: 同时解构 declaredWindow、subject;计划仍在 beforeReserve 里生成
|
||||
// 原因: issue #162 beforeReserve 多取 declaredWindow、subject,用来在审核后提前开始分类
|
||||
/beforeReserve:\s*async\s*\(\{\s*consultationMode,\s*serverChart,\s*declaredWindow,\s*subject\s*\}\)[\s\S]*createConsultationPlan\(\{/,
|
||||
);
|
||||
assert.match(
|
||||
prepareCall.slice(reserveAt),
|
||||
|
||||
Reference in New Issue
Block a user