chattercatcher 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +212 -153
- package/dist/cli.js.map +1 -1
- package/dist/index.js +211 -152
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -995,6 +995,7 @@ function migrateDatabase(database) {
|
|
|
995
995
|
answer TEXT NOT NULL,
|
|
996
996
|
citations_json TEXT NOT NULL,
|
|
997
997
|
retrieval_debug_json TEXT NOT NULL,
|
|
998
|
+
trace_json TEXT NOT NULL DEFAULT '{}',
|
|
998
999
|
status TEXT NOT NULL CHECK(status IN ('answered','failed')),
|
|
999
1000
|
error TEXT,
|
|
1000
1001
|
created_at TEXT NOT NULL
|
|
@@ -1089,6 +1090,10 @@ function migrateDatabase(database) {
|
|
|
1089
1090
|
ensureCronJobColumn("mention_target_name", "mention_target_name TEXT");
|
|
1090
1091
|
ensureCronJobColumn("mention_open_id", "mention_open_id TEXT");
|
|
1091
1092
|
ensureCronJobColumn("mention_user_id", "mention_user_id TEXT");
|
|
1093
|
+
const qaLogColumns = database.prepare("PRAGMA table_info(qa_logs)").all();
|
|
1094
|
+
if (!qaLogColumns.some((column) => column.name === "trace_json")) {
|
|
1095
|
+
database.prepare("ALTER TABLE qa_logs ADD COLUMN trace_json TEXT NOT NULL DEFAULT '{}'").run();
|
|
1096
|
+
}
|
|
1092
1097
|
}
|
|
1093
1098
|
|
|
1094
1099
|
// src/doctor/checks.ts
|
|
@@ -3893,10 +3898,22 @@ function createFeishuChatMembersClient(client) {
|
|
|
3893
3898
|
|
|
3894
3899
|
// src/rag/qa-logs.ts
|
|
3895
3900
|
import crypto6 from "crypto";
|
|
3901
|
+
|
|
3902
|
+
// src/rag/qa-trace.ts
|
|
3903
|
+
function hasQaTrace(trace) {
|
|
3904
|
+
return Object.keys(trace).length > 0;
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
// src/rag/qa-logs.ts
|
|
3896
3908
|
function clampLimit(limit) {
|
|
3897
3909
|
return Math.max(1, Math.min(200, Math.trunc(limit)));
|
|
3898
3910
|
}
|
|
3911
|
+
function parseTrace(value) {
|
|
3912
|
+
const parsed = JSON.parse(value);
|
|
3913
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3914
|
+
}
|
|
3899
3915
|
function mapQaLogRow(row) {
|
|
3916
|
+
const trace = parseTrace(row.trace_json);
|
|
3900
3917
|
return {
|
|
3901
3918
|
id: row.id,
|
|
3902
3919
|
chatId: row.chat_id,
|
|
@@ -3905,6 +3922,8 @@ function mapQaLogRow(row) {
|
|
|
3905
3922
|
answer: row.answer,
|
|
3906
3923
|
citations: JSON.parse(row.citations_json),
|
|
3907
3924
|
retrievalDebug: JSON.parse(row.retrieval_debug_json),
|
|
3925
|
+
trace,
|
|
3926
|
+
hasTrace: hasQaTrace(trace),
|
|
3908
3927
|
status: row.status,
|
|
3909
3928
|
error: row.error,
|
|
3910
3929
|
createdAt: row.created_at
|
|
@@ -3916,6 +3935,7 @@ var QaLogRepository = class {
|
|
|
3916
3935
|
}
|
|
3917
3936
|
database;
|
|
3918
3937
|
create(input) {
|
|
3938
|
+
const trace = input.trace ?? {};
|
|
3919
3939
|
const record = {
|
|
3920
3940
|
id: `qa_${crypto6.randomUUID()}`,
|
|
3921
3941
|
chatId: input.chatId ?? null,
|
|
@@ -3924,6 +3944,8 @@ var QaLogRepository = class {
|
|
|
3924
3944
|
answer: input.answer,
|
|
3925
3945
|
citations: input.citations,
|
|
3926
3946
|
retrievalDebug: input.retrievalDebug,
|
|
3947
|
+
trace,
|
|
3948
|
+
hasTrace: hasQaTrace(trace),
|
|
3927
3949
|
status: input.status,
|
|
3928
3950
|
error: input.error ?? null,
|
|
3929
3951
|
createdAt: input.createdAt
|
|
@@ -3938,6 +3960,7 @@ var QaLogRepository = class {
|
|
|
3938
3960
|
answer,
|
|
3939
3961
|
citations_json,
|
|
3940
3962
|
retrieval_debug_json,
|
|
3963
|
+
trace_json,
|
|
3941
3964
|
status,
|
|
3942
3965
|
error,
|
|
3943
3966
|
created_at
|
|
@@ -3950,6 +3973,7 @@ var QaLogRepository = class {
|
|
|
3950
3973
|
@answer,
|
|
3951
3974
|
@citationsJson,
|
|
3952
3975
|
@retrievalDebugJson,
|
|
3976
|
+
@traceJson,
|
|
3953
3977
|
@status,
|
|
3954
3978
|
@error,
|
|
3955
3979
|
@createdAt
|
|
@@ -3963,6 +3987,7 @@ var QaLogRepository = class {
|
|
|
3963
3987
|
answer: record.answer,
|
|
3964
3988
|
citationsJson: JSON.stringify(record.citations),
|
|
3965
3989
|
retrievalDebugJson: JSON.stringify(record.retrievalDebug),
|
|
3990
|
+
traceJson: JSON.stringify(record.trace),
|
|
3966
3991
|
status: record.status,
|
|
3967
3992
|
error: record.error,
|
|
3968
3993
|
createdAt: record.createdAt
|
|
@@ -3980,6 +4005,7 @@ var QaLogRepository = class {
|
|
|
3980
4005
|
answer,
|
|
3981
4006
|
citations_json,
|
|
3982
4007
|
retrieval_debug_json,
|
|
4008
|
+
trace_json,
|
|
3983
4009
|
status,
|
|
3984
4010
|
error,
|
|
3985
4011
|
created_at
|
|
@@ -4001,6 +4027,7 @@ var QaLogRepository = class {
|
|
|
4001
4027
|
answer,
|
|
4002
4028
|
citations_json,
|
|
4003
4029
|
retrieval_debug_json,
|
|
4030
|
+
trace_json,
|
|
4004
4031
|
status,
|
|
4005
4032
|
error,
|
|
4006
4033
|
created_at
|
|
@@ -4012,6 +4039,27 @@ var QaLogRepository = class {
|
|
|
4012
4039
|
).all(chatId, clampLimit(limit));
|
|
4013
4040
|
return rows.map(mapQaLogRow);
|
|
4014
4041
|
}
|
|
4042
|
+
getById(id) {
|
|
4043
|
+
const row = this.database.prepare(
|
|
4044
|
+
`
|
|
4045
|
+
SELECT
|
|
4046
|
+
id,
|
|
4047
|
+
chat_id,
|
|
4048
|
+
question_message_id,
|
|
4049
|
+
question,
|
|
4050
|
+
answer,
|
|
4051
|
+
citations_json,
|
|
4052
|
+
retrieval_debug_json,
|
|
4053
|
+
trace_json,
|
|
4054
|
+
status,
|
|
4055
|
+
error,
|
|
4056
|
+
created_at
|
|
4057
|
+
FROM qa_logs
|
|
4058
|
+
WHERE id = ?
|
|
4059
|
+
`
|
|
4060
|
+
).get(id);
|
|
4061
|
+
return row ? mapQaLogRow(row) : null;
|
|
4062
|
+
}
|
|
4015
4063
|
getCount() {
|
|
4016
4064
|
const row = this.database.prepare("SELECT COUNT(*) AS count FROM qa_logs").get();
|
|
4017
4065
|
return row.count;
|
|
@@ -4069,6 +4117,18 @@ ${block.text}`;
|
|
|
4069
4117
|
function toToolErrorContent(message) {
|
|
4070
4118
|
return JSON.stringify({ ok: false, error: message });
|
|
4071
4119
|
}
|
|
4120
|
+
function nowIso5() {
|
|
4121
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4122
|
+
}
|
|
4123
|
+
function finalizeTrace(trace, status, finalAnswer, startedAtMs) {
|
|
4124
|
+
return {
|
|
4125
|
+
...trace,
|
|
4126
|
+
completedAt: nowIso5(),
|
|
4127
|
+
durationMs: Date.now() - startedAtMs,
|
|
4128
|
+
status,
|
|
4129
|
+
finalAnswer
|
|
4130
|
+
};
|
|
4131
|
+
}
|
|
4072
4132
|
async function executeFeishuTool(tool, input) {
|
|
4073
4133
|
const result = await tool.execute(input);
|
|
4074
4134
|
if (isEvidenceBlockArray(result)) {
|
|
@@ -4080,6 +4140,13 @@ async function runFeishuToolLoop(input) {
|
|
|
4080
4140
|
if (!input.model.completeWithTools) {
|
|
4081
4141
|
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
4082
4142
|
}
|
|
4143
|
+
const startedAtMs = Date.now();
|
|
4144
|
+
const trace = {
|
|
4145
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
4146
|
+
modelTurns: [],
|
|
4147
|
+
toolResults: [],
|
|
4148
|
+
fallbacks: []
|
|
4149
|
+
};
|
|
4083
4150
|
const maxModelTurns = input.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
|
|
4084
4151
|
const maxToolCalls = input.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
4085
4152
|
const systemPromptParts = [FEISHU_TOOL_SYSTEM_PROMPT];
|
|
@@ -4108,19 +4175,36 @@ async function runFeishuToolLoop(input) {
|
|
|
4108
4175
|
toolCalls: assistantResult.toolCalls,
|
|
4109
4176
|
reasoningContent: assistantResult.reasoningContent
|
|
4110
4177
|
});
|
|
4178
|
+
trace.modelTurns?.push({
|
|
4179
|
+
index: turn,
|
|
4180
|
+
content: assistantResult.content,
|
|
4181
|
+
reasoningContent: assistantResult.reasoningContent,
|
|
4182
|
+
toolCalls: assistantResult.toolCalls,
|
|
4183
|
+
createdAt: nowIso5()
|
|
4184
|
+
});
|
|
4111
4185
|
if (assistantResult.toolCalls.length === 0) {
|
|
4112
4186
|
if (hasRawToolCallMarkup) {
|
|
4187
|
+
trace.fallbacks?.push({ type: "raw_tool_markup", message: "\u6A21\u578B\u8F93\u51FA\u4E86\u539F\u59CB\u5DE5\u5177\u8C03\u7528\u6807\u8BB0\uFF0C\u8F6C\u5165\u6700\u7EC8\u8865\u6551\u56DE\u7B54\u3002", createdAt: nowIso5() });
|
|
4113
4188
|
break;
|
|
4114
4189
|
}
|
|
4115
|
-
|
|
4190
|
+
const answer = assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
|
|
4191
|
+
return { answer, trace: finalizeTrace(trace, "answered", answer, startedAtMs) };
|
|
4116
4192
|
}
|
|
4117
4193
|
for (const toolCall of assistantResult.toolCalls) {
|
|
4118
4194
|
if (toolCallsUsed >= maxToolCalls) {
|
|
4119
|
-
|
|
4195
|
+
trace.fallbacks?.push({ type: "tool_limit", message: FEISHU_TOOL_LOOP_LIMIT_REACHED, createdAt: nowIso5() });
|
|
4196
|
+
return { answer: FEISHU_TOOL_LOOP_LIMIT_REACHED, trace: finalizeTrace(trace, "failed", FEISHU_TOOL_LOOP_LIMIT_REACHED, startedAtMs) };
|
|
4120
4197
|
}
|
|
4121
4198
|
toolCallsUsed += 1;
|
|
4122
4199
|
const tool = toolsByName.get(toolCall.name);
|
|
4123
4200
|
if (!tool) {
|
|
4201
|
+
trace.toolResults?.push({
|
|
4202
|
+
toolCallId: toolCall.id,
|
|
4203
|
+
name: toolCall.name,
|
|
4204
|
+
input: toolCall.input,
|
|
4205
|
+
error: `\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`,
|
|
4206
|
+
createdAt: nowIso5()
|
|
4207
|
+
});
|
|
4124
4208
|
messages.push({
|
|
4125
4209
|
role: "tool",
|
|
4126
4210
|
toolCallId: toolCall.id,
|
|
@@ -4130,6 +4214,13 @@ async function runFeishuToolLoop(input) {
|
|
|
4130
4214
|
}
|
|
4131
4215
|
try {
|
|
4132
4216
|
const result = await executeFeishuTool(tool, toolCall.input);
|
|
4217
|
+
trace.toolResults?.push({
|
|
4218
|
+
toolCallId: toolCall.id,
|
|
4219
|
+
name: toolCall.name,
|
|
4220
|
+
input: toolCall.input,
|
|
4221
|
+
content: result,
|
|
4222
|
+
createdAt: nowIso5()
|
|
4223
|
+
});
|
|
4133
4224
|
messages.push({
|
|
4134
4225
|
role: "tool",
|
|
4135
4226
|
toolCallId: toolCall.id,
|
|
@@ -4137,6 +4228,13 @@ async function runFeishuToolLoop(input) {
|
|
|
4137
4228
|
});
|
|
4138
4229
|
} catch (error) {
|
|
4139
4230
|
const message = error instanceof Error ? error.message : String(error);
|
|
4231
|
+
trace.toolResults?.push({
|
|
4232
|
+
toolCallId: toolCall.id,
|
|
4233
|
+
name: toolCall.name,
|
|
4234
|
+
input: toolCall.input,
|
|
4235
|
+
error: message,
|
|
4236
|
+
createdAt: nowIso5()
|
|
4237
|
+
});
|
|
4140
4238
|
messages.push({
|
|
4141
4239
|
role: "tool",
|
|
4142
4240
|
toolCallId: toolCall.id,
|
|
@@ -4150,9 +4248,13 @@ async function runFeishuToolLoop(input) {
|
|
|
4150
4248
|
...messages,
|
|
4151
4249
|
{ role: "system", content: "\u8BF7\u57FA\u4E8E\u4EE5\u4E0A\u6240\u6709\u5DE5\u5177\u8FD4\u56DE\u7684\u4FE1\u606F\uFF0C\u76F4\u63A5\u7ED9\u51FA\u6700\u7EC8\u7B54\u6848\u3002\u4E0D\u8981\u518D\u8C03\u7528\u5DE5\u5177\u3002" }
|
|
4152
4250
|
]);
|
|
4153
|
-
|
|
4251
|
+
const answer = salvageAnswer || "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
4252
|
+
trace.fallbacks?.push({ type: "salvage_completion", message: "\u5DE5\u5177\u5FAA\u73AF\u7ED3\u675F\u540E\u4F7F\u7528\u65E0\u5DE5\u5177\u8865\u6551\u56DE\u7B54\u3002", createdAt: nowIso5() });
|
|
4253
|
+
return { answer, trace: finalizeTrace(trace, "answered", answer, startedAtMs) };
|
|
4154
4254
|
} catch {
|
|
4155
|
-
|
|
4255
|
+
const answer = "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
4256
|
+
trace.fallbacks?.push({ type: "answer_generation_failed", message: answer, createdAt: nowIso5() });
|
|
4257
|
+
return { answer, trace: finalizeTrace(trace, "failed", answer, startedAtMs) };
|
|
4156
4258
|
}
|
|
4157
4259
|
}
|
|
4158
4260
|
function formatConversationContext(records) {
|
|
@@ -4272,7 +4374,7 @@ var FeishuQuestionHandler = class {
|
|
|
4272
4374
|
const memberRepository = this.options.memberRepository ?? new FeishuMemberRepository(this.options.database);
|
|
4273
4375
|
const memberPrompt = formatFeishuMemberPrompt(memberRepository.listByChat(decision.chatId));
|
|
4274
4376
|
const conversationContext = formatConversationContext(qaLogs.listRecentByChat(decision.chatId, 6));
|
|
4275
|
-
const
|
|
4377
|
+
const result = await runFeishuToolLoop({
|
|
4276
4378
|
question: decision.question,
|
|
4277
4379
|
now,
|
|
4278
4380
|
tools: allTools,
|
|
@@ -4284,27 +4386,38 @@ var FeishuQuestionHandler = class {
|
|
|
4284
4386
|
chatId: decision.chatId,
|
|
4285
4387
|
questionMessageId,
|
|
4286
4388
|
question: decision.question,
|
|
4287
|
-
answer,
|
|
4389
|
+
answer: result.answer,
|
|
4288
4390
|
citations: [],
|
|
4289
4391
|
retrievalDebug: {},
|
|
4392
|
+
trace: result.trace,
|
|
4290
4393
|
status: "answered",
|
|
4291
4394
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4292
4395
|
});
|
|
4293
|
-
await this.sendResponse(decision.chatId, questionMessageId, answer);
|
|
4396
|
+
await this.sendResponse(decision.chatId, questionMessageId, result.answer);
|
|
4294
4397
|
} catch (error) {
|
|
4295
4398
|
const message = error instanceof Error ? error.message : String(error);
|
|
4399
|
+
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4400
|
+
const failedAnswer = `\u6682\u65F6\u65E0\u6CD5\u56DE\u7B54\uFF1A${message}`;
|
|
4296
4401
|
qaLogs.create({
|
|
4297
4402
|
chatId: decision.chatId,
|
|
4298
4403
|
questionMessageId,
|
|
4299
4404
|
question: decision.question,
|
|
4300
|
-
answer:
|
|
4405
|
+
answer: failedAnswer,
|
|
4301
4406
|
citations: [],
|
|
4302
4407
|
retrievalDebug: {},
|
|
4408
|
+
trace: {
|
|
4409
|
+
startedAt: now.toISOString(),
|
|
4410
|
+
completedAt: failedAt,
|
|
4411
|
+
durationMs: Math.max(0, Date.parse(failedAt) - now.getTime()),
|
|
4412
|
+
status: "failed",
|
|
4413
|
+
finalAnswer: failedAnswer,
|
|
4414
|
+
fallbacks: [{ type: "answer_generation_failed", message, createdAt: failedAt }]
|
|
4415
|
+
},
|
|
4303
4416
|
status: "failed",
|
|
4304
4417
|
error: message,
|
|
4305
|
-
createdAt:
|
|
4418
|
+
createdAt: failedAt
|
|
4306
4419
|
});
|
|
4307
|
-
await this.sendResponse(decision.chatId, questionMessageId,
|
|
4420
|
+
await this.sendResponse(decision.chatId, questionMessageId, failedAnswer);
|
|
4308
4421
|
}
|
|
4309
4422
|
return decision;
|
|
4310
4423
|
} finally {
|
|
@@ -4327,146 +4440,13 @@ function formatTextWithMentions(text, options) {
|
|
|
4327
4440
|
const prefix = mentions.map((mention) => `<at user_id="${escapeAtText(mention.openId)}">${escapeAtText(mention.name)}</at>`).join(" ");
|
|
4328
4441
|
return `${prefix} ${text}`.trim();
|
|
4329
4442
|
}
|
|
4330
|
-
function
|
|
4331
|
-
|
|
4332
|
-
for (let index = start; index < text.length; index += 1) {
|
|
4333
|
-
const char = text[index];
|
|
4334
|
-
if (char === "(") {
|
|
4335
|
-
depth += 1;
|
|
4336
|
-
} else if (char === ")") {
|
|
4337
|
-
if (depth === 0) return index;
|
|
4338
|
-
depth -= 1;
|
|
4339
|
-
}
|
|
4340
|
-
}
|
|
4341
|
-
return -1;
|
|
4342
|
-
}
|
|
4343
|
-
function stripInlineMarkdown(text) {
|
|
4344
|
-
let output = "";
|
|
4345
|
-
let index = 0;
|
|
4346
|
-
while (index < text.length) {
|
|
4347
|
-
const linkStart = text.indexOf("[", index);
|
|
4348
|
-
const boldStarStart = text.indexOf("**", index);
|
|
4349
|
-
const boldUnderscoreStart = text.indexOf("__", index);
|
|
4350
|
-
const candidates = [linkStart, boldStarStart, boldUnderscoreStart].filter((value) => value >= 0);
|
|
4351
|
-
const next = candidates.length ? Math.min(...candidates) : -1;
|
|
4352
|
-
if (next < 0) {
|
|
4353
|
-
output += text.slice(index);
|
|
4354
|
-
break;
|
|
4355
|
-
}
|
|
4356
|
-
output += text.slice(index, next);
|
|
4357
|
-
if (next === linkStart) {
|
|
4358
|
-
const labelEnd = text.indexOf("](", next);
|
|
4359
|
-
if (labelEnd > next) {
|
|
4360
|
-
const hrefStart = labelEnd + 2;
|
|
4361
|
-
const hrefEnd = findMarkdownLinkEnd(text, hrefStart);
|
|
4362
|
-
const href = hrefEnd >= 0 ? text.slice(hrefStart, hrefEnd) : "";
|
|
4363
|
-
if (hrefEnd >= 0 && /^https?:\/\/\S+$/.test(href)) {
|
|
4364
|
-
output += `${text.slice(next + 1, labelEnd)} ${href}`;
|
|
4365
|
-
index = hrefEnd + 1;
|
|
4366
|
-
continue;
|
|
4367
|
-
}
|
|
4368
|
-
}
|
|
4369
|
-
output += text[next];
|
|
4370
|
-
index = next + 1;
|
|
4371
|
-
continue;
|
|
4372
|
-
}
|
|
4373
|
-
const marker = next === boldStarStart ? "**" : "__";
|
|
4374
|
-
const close = text.indexOf(marker, next + marker.length);
|
|
4375
|
-
if (close > next + marker.length) {
|
|
4376
|
-
output += text.slice(next + marker.length, close);
|
|
4377
|
-
index = close + marker.length;
|
|
4378
|
-
continue;
|
|
4379
|
-
}
|
|
4380
|
-
output += marker;
|
|
4381
|
-
index = next + marker.length;
|
|
4382
|
-
}
|
|
4383
|
-
return output;
|
|
4384
|
-
}
|
|
4385
|
-
function parseInline(text) {
|
|
4386
|
-
return [{ tag: "text", text: stripInlineMarkdown(text) || " " }];
|
|
4387
|
-
}
|
|
4388
|
-
function pushParagraph(content, lines) {
|
|
4389
|
-
if (lines.length === 0) return;
|
|
4390
|
-
content.push(parseInline(lines.join("\n")));
|
|
4391
|
-
lines.length = 0;
|
|
4392
|
-
}
|
|
4393
|
-
function parseMarkdownBlocks(markdown) {
|
|
4394
|
-
if (!markdown.trim()) {
|
|
4395
|
-
return [[{ tag: "text", text: " " }]];
|
|
4396
|
-
}
|
|
4397
|
-
const content = [];
|
|
4398
|
-
const paragraph = [];
|
|
4399
|
-
const code = [];
|
|
4400
|
-
let inCodeBlock = false;
|
|
4401
|
-
for (const rawLine of markdown.replace(/\r\n/g, "\n").split("\n")) {
|
|
4402
|
-
const line = rawLine.trimEnd();
|
|
4403
|
-
if (line.startsWith("```")) {
|
|
4404
|
-
if (inCodeBlock) {
|
|
4405
|
-
content.push([{ tag: "text", text: `\`\`\`
|
|
4406
|
-
${code.join("\n")}
|
|
4407
|
-
\`\`\`` }]);
|
|
4408
|
-
code.length = 0;
|
|
4409
|
-
inCodeBlock = false;
|
|
4410
|
-
} else {
|
|
4411
|
-
pushParagraph(content, paragraph);
|
|
4412
|
-
inCodeBlock = true;
|
|
4413
|
-
}
|
|
4414
|
-
continue;
|
|
4415
|
-
}
|
|
4416
|
-
if (inCodeBlock) {
|
|
4417
|
-
code.push(rawLine);
|
|
4418
|
-
continue;
|
|
4419
|
-
}
|
|
4420
|
-
if (!line.trim()) {
|
|
4421
|
-
pushParagraph(content, paragraph);
|
|
4422
|
-
continue;
|
|
4423
|
-
}
|
|
4424
|
-
const heading = line.match(/^#{1,6}\s+(.+)$/);
|
|
4425
|
-
if (heading) {
|
|
4426
|
-
pushParagraph(content, paragraph);
|
|
4427
|
-
content.push([{ tag: "text", text: stripInlineMarkdown(heading[1]) || " " }]);
|
|
4428
|
-
continue;
|
|
4429
|
-
}
|
|
4430
|
-
const unordered = line.match(/^[-*]\s+(.+)$/);
|
|
4431
|
-
if (unordered) {
|
|
4432
|
-
pushParagraph(content, paragraph);
|
|
4433
|
-
content.push(parseInline(`\u2022 ${unordered[1]}`));
|
|
4434
|
-
continue;
|
|
4435
|
-
}
|
|
4436
|
-
const ordered = line.match(/^(\d+)\.\s+(.+)$/);
|
|
4437
|
-
if (ordered) {
|
|
4438
|
-
pushParagraph(content, paragraph);
|
|
4439
|
-
content.push(parseInline(`${ordered[1]}. ${ordered[2]}`));
|
|
4440
|
-
continue;
|
|
4441
|
-
}
|
|
4442
|
-
paragraph.push(line);
|
|
4443
|
-
}
|
|
4444
|
-
if (inCodeBlock) {
|
|
4445
|
-
content.push([{ tag: "text", text: `\`\`\`
|
|
4446
|
-
${code.join("\n")}` }]);
|
|
4447
|
-
}
|
|
4448
|
-
pushParagraph(content, paragraph);
|
|
4449
|
-
return content.length ? content : [[{ tag: "text", text: markdown }]];
|
|
4443
|
+
function buildMarkdownText(markdown, options) {
|
|
4444
|
+
return formatTextWithMentions(markdown.trim() || " ", options);
|
|
4450
4445
|
}
|
|
4451
4446
|
function buildFeishuPostContent(markdown, options) {
|
|
4452
|
-
const content = parseMarkdownBlocks(markdown);
|
|
4453
|
-
const mentions = options?.mentions ?? [];
|
|
4454
|
-
if (mentions.length) {
|
|
4455
|
-
const firstLine = content[0] ?? [];
|
|
4456
|
-
const firstText = firstLine[0];
|
|
4457
|
-
const prefix = mentions.map((mention) => `@${mention.name}`).join(" ");
|
|
4458
|
-
if (firstText?.tag === "text") {
|
|
4459
|
-
content[0] = [{ tag: "text", text: `${prefix} ${firstText.text}` }, ...firstLine.slice(1)];
|
|
4460
|
-
} else {
|
|
4461
|
-
content[0] = [{ tag: "text", text: `${prefix} ` }, ...firstLine];
|
|
4462
|
-
}
|
|
4463
|
-
}
|
|
4464
4447
|
return {
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
title: "",
|
|
4468
|
-
content
|
|
4469
|
-
}
|
|
4448
|
+
zh_cn: {
|
|
4449
|
+
content: [[{ tag: "md", text: buildMarkdownText(markdown, options) }]]
|
|
4470
4450
|
}
|
|
4471
4451
|
};
|
|
4472
4452
|
}
|
|
@@ -6050,12 +6030,15 @@ function buildHtml() {
|
|
|
6050
6030
|
let allFileJobs = [];
|
|
6051
6031
|
let allCronJobs = [];
|
|
6052
6032
|
let allQaLogs = [];
|
|
6033
|
+
let selectedQaLogId = null;
|
|
6053
6034
|
let statusData = null;
|
|
6054
6035
|
|
|
6055
6036
|
function fmt(value) { return value == null || value === "" ? "-" : String(value); }
|
|
6056
6037
|
function escapeHtml(value) {
|
|
6057
6038
|
return fmt(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
6058
6039
|
}
|
|
6040
|
+
function renderJson(value) { return '<pre style="white-space:pre-wrap;overflow:auto;max-height:320px;">' + escapeHtml(JSON.stringify(value, null, 2)) + '</pre>'; }
|
|
6041
|
+
function renderTextBlock(value) { return '<pre style="white-space:pre-wrap;overflow:auto;max-height:320px;">' + escapeHtml(value || "") + '</pre>'; }
|
|
6059
6042
|
function isOpaqueId(value) { return /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(fmt(value)); }
|
|
6060
6043
|
function formatDateTime(value) {
|
|
6061
6044
|
var date = new Date(value);
|
|
@@ -6328,17 +6311,72 @@ function buildHtml() {
|
|
|
6328
6311
|
for (var i = 0; i < allQaLogs.length; i++) {
|
|
6329
6312
|
var item = allQaLogs[i];
|
|
6330
6313
|
var citationCount = Array.isArray(item.citations) ? item.citations.length : 0;
|
|
6331
|
-
var statusClass = item.status === '
|
|
6314
|
+
var statusClass = item.status === 'answered' ? 'tag-success' : 'tag-warning';
|
|
6332
6315
|
html += '<div class="qa-card"><div class="message-meta" style="margin-bottom:var(--space-sm);">' +
|
|
6333
6316
|
'<span>' + escapeHtml(formatDateTime(item.createdAt)) + '</span>' +
|
|
6334
6317
|
'<span class="tag ' + statusClass + '">' + escapeHtml(item.status) + '</span>' +
|
|
6335
|
-
'<span>' + citationCount + ' \u6761\u5F15\u7528</span
|
|
6318
|
+
'<span>' + citationCount + ' \u6761\u5F15\u7528</span>' +
|
|
6319
|
+
'<span class="tag ' + (item.hasTrace ? 'tag-info' : 'tag-warning') + '">' + (item.hasTrace ? '\u6709 trace' : '\u65E0 trace') + '</span></div>' +
|
|
6336
6320
|
'<div class="qa-question">' + escapeHtml(item.question) + '</div>' +
|
|
6337
|
-
'<div class="qa-answer">' + escapeHtml(item.answer) + '</div
|
|
6321
|
+
'<div class="qa-answer">' + escapeHtml(item.answer) + '</div>' +
|
|
6322
|
+
'<button class="btn btn-sm" style="margin-top:var(--space-sm);" data-view-qa-log="' + escapeHtml(item.id) + '">\u67E5\u770B\u8BE6\u60C5</button>' +
|
|
6323
|
+
'<div id="qa-detail-' + escapeHtml(item.id) + '" style="margin-top:var(--space-md);"></div></div>';
|
|
6338
6324
|
}
|
|
6339
6325
|
el.innerHTML = html;
|
|
6340
6326
|
}
|
|
6341
6327
|
|
|
6328
|
+
async function showQaLogDetail(id) {
|
|
6329
|
+
selectedQaLogId = id;
|
|
6330
|
+
var container = document.getElementById("qa-detail-" + id);
|
|
6331
|
+
if (!container) return;
|
|
6332
|
+
container.innerHTML = '<div class="empty-state">\u6B63\u5728\u52A0\u8F7D\u95EE\u7B54\u8BE6\u60C5...</div>';
|
|
6333
|
+
try {
|
|
6334
|
+
var item = await fetchJson("/api/qa-logs/" + encodeURIComponent(id));
|
|
6335
|
+
renderQaLogDetail(item);
|
|
6336
|
+
} catch (error) {
|
|
6337
|
+
container.innerHTML = '<div class="empty-state">\u8BE6\u60C5\u52A0\u8F7D\u5931\u8D25\uFF1A' + escapeHtml(error instanceof Error ? error.message : String(error)) + '</div>';
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6340
|
+
|
|
6341
|
+
function renderQaLogDetail(item) {
|
|
6342
|
+
var container = document.getElementById("qa-detail-" + item.id);
|
|
6343
|
+
if (!container) return;
|
|
6344
|
+
var trace = item.trace || {};
|
|
6345
|
+
var html = '<div class="content-panel" style="margin-top:var(--space-sm);background:rgba(255,255,255,0.03);">';
|
|
6346
|
+
html += '<h3 style="font-size:15px;margin-bottom:var(--space-sm);">\u95EE\u7B54\u8BE6\u60C5</h3>';
|
|
6347
|
+
html += '<div class="message-meta" style="margin-bottom:var(--space-sm);"><span>\u72B6\u6001\uFF1A' + escapeHtml(item.status) + '</span><span>\u521B\u5EFA\uFF1A' + escapeHtml(formatDateTime(item.createdAt)) + '</span><span>\u8017\u65F6\uFF1A' + escapeHtml(trace.durationMs == null ? '-' : trace.durationMs + 'ms') + '</span></div>';
|
|
6348
|
+
if (item.error) html += '<div style="color:var(--danger);margin-bottom:var(--space-sm);">\u9519\u8BEF\uFF1A' + escapeHtml(item.error) + '</div>';
|
|
6349
|
+
html += '<div class="qa-question">' + escapeHtml(item.question) + '</div>';
|
|
6350
|
+
html += '<div class="qa-answer" style="margin-bottom:var(--space-md);">' + escapeHtml(item.answer) + '</div>';
|
|
6351
|
+
if (!item.hasTrace) {
|
|
6352
|
+
html += '<div class="empty-state">\u8FD9\u6761\u95EE\u7B54\u6CA1\u6709 trace\uFF0C\u53EF\u80FD\u6765\u81EA\u65E7\u7248\u672C\u8BB0\u5F55\u3002</div></div>';
|
|
6353
|
+
container.innerHTML = html;
|
|
6354
|
+
return;
|
|
6355
|
+
}
|
|
6356
|
+
var turns = trace.modelTurns || [];
|
|
6357
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">Reasoning</h4>';
|
|
6358
|
+
if (turns.length === 0) html += '<div class="empty-state">\u65E0 reasoningContent</div>';
|
|
6359
|
+
for (var i = 0; i < turns.length; i++) {
|
|
6360
|
+
html += '<div style="margin-bottom:var(--space-sm);"><div class="message-meta"><span>\u6A21\u578B\u8F6E\u6B21 ' + escapeHtml(turns[i].index) + '</span><span>' + escapeHtml(formatDateTime(turns[i].createdAt)) + '</span></div>' + renderTextBlock(turns[i].reasoningContent || '\u65E0 reasoningContent') + '</div>';
|
|
6361
|
+
}
|
|
6362
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">\u6A21\u578B\u8F6E\u6B21\u4E0E\u5DE5\u5177\u8C03\u7528</h4>';
|
|
6363
|
+
for (var j = 0; j < turns.length; j++) {
|
|
6364
|
+
html += '<div style="margin-bottom:var(--space-sm);"><div class="message-meta"><span>\u8F6E\u6B21 ' + escapeHtml(turns[j].index) + '</span></div>' + renderTextBlock(turns[j].content || '') + renderJson(turns[j].toolCalls || []) + '</div>';
|
|
6365
|
+
}
|
|
6366
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">\u5DE5\u5177\u7ED3\u679C</h4>';
|
|
6367
|
+
var toolResults = trace.toolResults || [];
|
|
6368
|
+
if (toolResults.length === 0) html += '<div class="empty-state">\u6CA1\u6709\u5DE5\u5177\u7ED3\u679C\u3002</div>';
|
|
6369
|
+
for (var k = 0; k < toolResults.length; k++) {
|
|
6370
|
+
html += '<div style="margin-bottom:var(--space-sm);"><div class="message-meta"><span>' + escapeHtml(toolResults[k].name) + '</span><span>' + escapeHtml(toolResults[k].toolCallId) + '</span><span>' + escapeHtml(formatDateTime(toolResults[k].createdAt)) + '</span></div>' + renderJson(toolResults[k].input) + (toolResults[k].error ? '<div style="color:var(--danger);">' + escapeHtml(toolResults[k].error) + '</div>' : renderTextBlock(toolResults[k].content || '')) + '</div>';
|
|
6371
|
+
}
|
|
6372
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">\u5F15\u7528\u4E0E\u68C0\u7D22</h4>' + renderJson({ citations: item.citations || [], retrievalDebug: item.retrievalDebug || {} });
|
|
6373
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">Fallback</h4>';
|
|
6374
|
+
var fallbacks = trace.fallbacks || [];
|
|
6375
|
+
html += fallbacks.length === 0 ? '<div class="empty-state">\u6CA1\u6709 fallback\u3002</div>' : renderJson(fallbacks);
|
|
6376
|
+
html += '</div>';
|
|
6377
|
+
container.innerHTML = html;
|
|
6378
|
+
}
|
|
6379
|
+
|
|
6342
6380
|
function renderSettings(status) {
|
|
6343
6381
|
var el = document.getElementById("settings-config");
|
|
6344
6382
|
var html = '<h3 style="font-size:16px;font-weight:600;margin-bottom:var(--space-md);">\u7CFB\u7EDF\u914D\u7F6E</h3>';
|
|
@@ -6373,7 +6411,10 @@ function buildHtml() {
|
|
|
6373
6411
|
if (currentView === "episodes") renderEpisodesView();
|
|
6374
6412
|
if (currentView === "files") renderFilesView();
|
|
6375
6413
|
if (currentView === "tasks") renderTasksView();
|
|
6376
|
-
if (currentView === "qa-logs")
|
|
6414
|
+
if (currentView === "qa-logs") {
|
|
6415
|
+
renderQaLogsView();
|
|
6416
|
+
if (selectedQaLogId) void showQaLogDetail(selectedQaLogId);
|
|
6417
|
+
}
|
|
6377
6418
|
}
|
|
6378
6419
|
|
|
6379
6420
|
async function processNow() {
|
|
@@ -6395,6 +6436,11 @@ function buildHtml() {
|
|
|
6395
6436
|
document.addEventListener("click", async function(event) {
|
|
6396
6437
|
var target = event.target;
|
|
6397
6438
|
if (!(target instanceof HTMLElement)) return;
|
|
6439
|
+
var qaLogId = target.dataset.viewQaLog;
|
|
6440
|
+
if (qaLogId) {
|
|
6441
|
+
void showQaLogDetail(qaLogId);
|
|
6442
|
+
return;
|
|
6443
|
+
}
|
|
6398
6444
|
var id = target.dataset.deleteCronJob;
|
|
6399
6445
|
if (!id) return;
|
|
6400
6446
|
target.disabled = true;
|
|
@@ -6438,6 +6484,10 @@ function parseCookies(header) {
|
|
|
6438
6484
|
function isAuthorizedWebAction(request, token) {
|
|
6439
6485
|
return parseCookies(request.headers.cookie).chattercatcher_web_token === token;
|
|
6440
6486
|
}
|
|
6487
|
+
function toQaLogListItem(log) {
|
|
6488
|
+
const { trace: _trace, ...item } = log;
|
|
6489
|
+
return item;
|
|
6490
|
+
}
|
|
6441
6491
|
function createWebApp(config, options = {}) {
|
|
6442
6492
|
const app = Fastify({ logger: false });
|
|
6443
6493
|
const database = openDatabase(config);
|
|
@@ -6516,9 +6566,18 @@ function createWebApp(config, options = {}) {
|
|
|
6516
6566
|
app.get("/api/qa-logs", async (request) => {
|
|
6517
6567
|
const limit = parseLimit(request.query.limit, 20, 100);
|
|
6518
6568
|
return {
|
|
6519
|
-
items: qaLogs.listRecent(limit)
|
|
6569
|
+
items: qaLogs.listRecent(limit).map(toQaLogListItem)
|
|
6520
6570
|
};
|
|
6521
6571
|
});
|
|
6572
|
+
app.get("/api/qa-logs/:id", async (request, reply) => {
|
|
6573
|
+
const id = request.params.id;
|
|
6574
|
+
const log = qaLogs.getById(id);
|
|
6575
|
+
if (!log) {
|
|
6576
|
+
reply.code(404);
|
|
6577
|
+
return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u95EE\u7B54\u65E5\u5FD7\u3002" };
|
|
6578
|
+
}
|
|
6579
|
+
return log;
|
|
6580
|
+
});
|
|
6522
6581
|
app.get("/api/cron-jobs", async (request) => {
|
|
6523
6582
|
const limit = parseLimit(request.query.limit, 50, 200);
|
|
6524
6583
|
return {
|