feat(y-log): 问答按 LLM 调用各记一条 + 全量补齐各 API 操作埋点
- 问答日志从「每轮 onFinish 一条」改为 useThreadStream onLangChainEvent 监听 on_chat_model_end,每次大模型调用结束各上报一条(含子代理) - 广覆盖各 API client 成功路径:查看(view)/查询(search)/新增(create)/ 编辑(edit)/删除(delete)/启停(enabled·disabled)/导入导出,补齐此前 整模块零埋点与漏掉的单条详情 GET;跳过高频 typeahead/轮询/bootstrap Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
320f2ef814
commit
df9c59e21c
35 files changed
+1569
-224
No files matched your search
@@ -15,6 +15,7 @@ import { getStoredAuth } from "../auth";
|
||||
import { rememberAskTime } from "../chat-timestamps";
|
||||
import { getBackendBaseURL, getLangGraphBaseURL } from "../config";
|
||||
import { useI18n } from "../i18n/hooks";
|
||||
import { OptType, writeYLog } from "../log/y-log";
|
||||
import type { FileInMessage } from "../messages/utils";
|
||||
import type { LocalSettings } from "../settings";
|
||||
import { isThreadRagModeLocked, lockThreadRagMode, useLocalSettings } from "../settings";
|
||||
@@ -231,6 +232,84 @@ function buildCurrentUserLabel(): string {
|
||||
return userId ? userId : "未知用户";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 `on_chat_model_end` 事件的 output 里尽力取出模型输出文本。
|
||||
*
|
||||
* output 形态可能是 AIMessage(`{ content: string | parts[] }`),也可能是
|
||||
* LangChain 的 `{ generations: [[{ text, message }]] }`。全部容错,取不到返回空串。
|
||||
*/
|
||||
function textFromChatModelOutput(output: unknown): string {
|
||||
if (output == null) return "";
|
||||
if (typeof output === "string") return output;
|
||||
if (typeof output !== "object") return String(output);
|
||||
const o = output as Record<string, unknown>;
|
||||
if ("content" in o) {
|
||||
const c = o.content;
|
||||
if (typeof c === "string") return c;
|
||||
if (Array.isArray(c)) {
|
||||
const parts = c
|
||||
.map((p) =>
|
||||
p && typeof p === "object" && "text" in p
|
||||
? String((p as { text?: unknown }).text ?? "")
|
||||
: "",
|
||||
)
|
||||
.filter(Boolean);
|
||||
if (parts.length) return parts.join("");
|
||||
}
|
||||
}
|
||||
if (Array.isArray(o.generations)) {
|
||||
const texts: string[] = [];
|
||||
const collect = (gen: unknown) => {
|
||||
if (Array.isArray(gen)) {
|
||||
gen.forEach(collect);
|
||||
return;
|
||||
}
|
||||
if (gen && typeof gen === "object") {
|
||||
const gg = gen as Record<string, unknown>;
|
||||
if (typeof gg.text === "string" && gg.text) texts.push(gg.text);
|
||||
else if (gg.message) {
|
||||
const t = textFromChatModelOutput(gg.message);
|
||||
if (t) texts.push(t);
|
||||
}
|
||||
}
|
||||
};
|
||||
collect(o.generations);
|
||||
if (texts.length) return texts.join("");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 `on_chat_model_*` 事件的 input 里尽力取出最后一条 user/human 文本,
|
||||
* 作为本次大模型调用的「问题」。input 可能是 `{ messages: BaseMessage[] | BaseMessage[][] }`。
|
||||
*/
|
||||
function lastHumanFromChatModelInput(input: unknown): string {
|
||||
try {
|
||||
const raw =
|
||||
input && typeof input === "object" && "messages" in input
|
||||
? (input as { messages?: unknown }).messages
|
||||
: input;
|
||||
const flat: unknown[] = [];
|
||||
const push = (m: unknown) => {
|
||||
if (Array.isArray(m)) m.forEach(push);
|
||||
else if (m != null) flat.push(m);
|
||||
};
|
||||
push(raw);
|
||||
for (let i = flat.length - 1; i >= 0; i--) {
|
||||
const m = flat[i];
|
||||
if (!m || typeof m !== "object") continue;
|
||||
const type = (m as Record<string, unknown>).type ?? (m as Record<string, unknown>).role;
|
||||
if (type === "human" || type === "user") {
|
||||
const text = textFromChatModelOutput(m);
|
||||
if (text) return text;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* 取不到忽略 */
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function useThreadStream({
|
||||
threadId,
|
||||
context,
|
||||
@@ -333,6 +412,28 @@ export function useThreadStream({
|
||||
data: event.data,
|
||||
});
|
||||
}
|
||||
// 操作日志(Y-Log):每一次大模型调用结束都各上报一条(一次 LLM 调用 = 一条),
|
||||
// 让问答日志尽量「多、全」——一个问答轮次里 lead agent / 子代理的每次模型调用
|
||||
// 都会落一条。门控与失败静默都在 writeYLog 内部处理,绝不影响问答。
|
||||
if (event.event === "on_chat_model_end") {
|
||||
try {
|
||||
const data = (event.data ?? {}) as {
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
};
|
||||
const answer = textFromChatModelOutput(data.output);
|
||||
const question = lastHumanFromChatModelInput(data.input);
|
||||
writeYLog({
|
||||
optType: OptType.other,
|
||||
optDetail: "B1-CM助手-智能问答",
|
||||
url: `/api/threads/${threadIdRef.current ?? ""}/runs/stream`,
|
||||
reqParam: { model: event.name ?? "", question },
|
||||
returnParam: { data: answer },
|
||||
});
|
||||
} catch {
|
||||
/* 上报绝不影响问答 */
|
||||
}
|
||||
}
|
||||
},
|
||||
onUpdateEvent(data) {
|
||||
const updates: Array<Partial<AgentThreadState> | null> = Object.values(
|
||||
|
||||
Reference in new issue
Block a user