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/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import fs15 from "fs/promises";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "chattercatcher",
|
|
11
|
-
version: "0.2.
|
|
11
|
+
version: "0.2.6",
|
|
12
12
|
description: "\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u5E93\u673A\u5668\u4EBA",
|
|
13
13
|
type: "module",
|
|
14
14
|
main: "dist/index.js",
|
|
@@ -495,6 +495,7 @@ function migrateDatabase(database) {
|
|
|
495
495
|
answer TEXT NOT NULL,
|
|
496
496
|
citations_json TEXT NOT NULL,
|
|
497
497
|
retrieval_debug_json TEXT NOT NULL,
|
|
498
|
+
trace_json TEXT NOT NULL DEFAULT '{}',
|
|
498
499
|
status TEXT NOT NULL CHECK(status IN ('answered','failed')),
|
|
499
500
|
error TEXT,
|
|
500
501
|
created_at TEXT NOT NULL
|
|
@@ -589,6 +590,10 @@ function migrateDatabase(database) {
|
|
|
589
590
|
ensureCronJobColumn("mention_target_name", "mention_target_name TEXT");
|
|
590
591
|
ensureCronJobColumn("mention_open_id", "mention_open_id TEXT");
|
|
591
592
|
ensureCronJobColumn("mention_user_id", "mention_user_id TEXT");
|
|
593
|
+
const qaLogColumns = database.prepare("PRAGMA table_info(qa_logs)").all();
|
|
594
|
+
if (!qaLogColumns.some((column) => column.name === "trace_json")) {
|
|
595
|
+
database.prepare("ALTER TABLE qa_logs ADD COLUMN trace_json TEXT NOT NULL DEFAULT '{}'").run();
|
|
596
|
+
}
|
|
592
597
|
}
|
|
593
598
|
|
|
594
599
|
// src/doctor/checks.ts
|
|
@@ -4038,10 +4043,22 @@ function createCronJobTools(input2) {
|
|
|
4038
4043
|
|
|
4039
4044
|
// src/rag/qa-logs.ts
|
|
4040
4045
|
import crypto6 from "crypto";
|
|
4046
|
+
|
|
4047
|
+
// src/rag/qa-trace.ts
|
|
4048
|
+
function hasQaTrace(trace) {
|
|
4049
|
+
return Object.keys(trace).length > 0;
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
// src/rag/qa-logs.ts
|
|
4041
4053
|
function clampLimit(limit) {
|
|
4042
4054
|
return Math.max(1, Math.min(200, Math.trunc(limit)));
|
|
4043
4055
|
}
|
|
4056
|
+
function parseTrace(value) {
|
|
4057
|
+
const parsed = JSON.parse(value);
|
|
4058
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
4059
|
+
}
|
|
4044
4060
|
function mapQaLogRow(row) {
|
|
4061
|
+
const trace = parseTrace(row.trace_json);
|
|
4045
4062
|
return {
|
|
4046
4063
|
id: row.id,
|
|
4047
4064
|
chatId: row.chat_id,
|
|
@@ -4050,6 +4067,8 @@ function mapQaLogRow(row) {
|
|
|
4050
4067
|
answer: row.answer,
|
|
4051
4068
|
citations: JSON.parse(row.citations_json),
|
|
4052
4069
|
retrievalDebug: JSON.parse(row.retrieval_debug_json),
|
|
4070
|
+
trace,
|
|
4071
|
+
hasTrace: hasQaTrace(trace),
|
|
4053
4072
|
status: row.status,
|
|
4054
4073
|
error: row.error,
|
|
4055
4074
|
createdAt: row.created_at
|
|
@@ -4061,6 +4080,7 @@ var QaLogRepository = class {
|
|
|
4061
4080
|
}
|
|
4062
4081
|
database;
|
|
4063
4082
|
create(input2) {
|
|
4083
|
+
const trace = input2.trace ?? {};
|
|
4064
4084
|
const record = {
|
|
4065
4085
|
id: `qa_${crypto6.randomUUID()}`,
|
|
4066
4086
|
chatId: input2.chatId ?? null,
|
|
@@ -4069,6 +4089,8 @@ var QaLogRepository = class {
|
|
|
4069
4089
|
answer: input2.answer,
|
|
4070
4090
|
citations: input2.citations,
|
|
4071
4091
|
retrievalDebug: input2.retrievalDebug,
|
|
4092
|
+
trace,
|
|
4093
|
+
hasTrace: hasQaTrace(trace),
|
|
4072
4094
|
status: input2.status,
|
|
4073
4095
|
error: input2.error ?? null,
|
|
4074
4096
|
createdAt: input2.createdAt
|
|
@@ -4083,6 +4105,7 @@ var QaLogRepository = class {
|
|
|
4083
4105
|
answer,
|
|
4084
4106
|
citations_json,
|
|
4085
4107
|
retrieval_debug_json,
|
|
4108
|
+
trace_json,
|
|
4086
4109
|
status,
|
|
4087
4110
|
error,
|
|
4088
4111
|
created_at
|
|
@@ -4095,6 +4118,7 @@ var QaLogRepository = class {
|
|
|
4095
4118
|
@answer,
|
|
4096
4119
|
@citationsJson,
|
|
4097
4120
|
@retrievalDebugJson,
|
|
4121
|
+
@traceJson,
|
|
4098
4122
|
@status,
|
|
4099
4123
|
@error,
|
|
4100
4124
|
@createdAt
|
|
@@ -4108,6 +4132,7 @@ var QaLogRepository = class {
|
|
|
4108
4132
|
answer: record.answer,
|
|
4109
4133
|
citationsJson: JSON.stringify(record.citations),
|
|
4110
4134
|
retrievalDebugJson: JSON.stringify(record.retrievalDebug),
|
|
4135
|
+
traceJson: JSON.stringify(record.trace),
|
|
4111
4136
|
status: record.status,
|
|
4112
4137
|
error: record.error,
|
|
4113
4138
|
createdAt: record.createdAt
|
|
@@ -4125,6 +4150,7 @@ var QaLogRepository = class {
|
|
|
4125
4150
|
answer,
|
|
4126
4151
|
citations_json,
|
|
4127
4152
|
retrieval_debug_json,
|
|
4153
|
+
trace_json,
|
|
4128
4154
|
status,
|
|
4129
4155
|
error,
|
|
4130
4156
|
created_at
|
|
@@ -4146,6 +4172,7 @@ var QaLogRepository = class {
|
|
|
4146
4172
|
answer,
|
|
4147
4173
|
citations_json,
|
|
4148
4174
|
retrieval_debug_json,
|
|
4175
|
+
trace_json,
|
|
4149
4176
|
status,
|
|
4150
4177
|
error,
|
|
4151
4178
|
created_at
|
|
@@ -4157,6 +4184,27 @@ var QaLogRepository = class {
|
|
|
4157
4184
|
).all(chatId, clampLimit(limit));
|
|
4158
4185
|
return rows.map(mapQaLogRow);
|
|
4159
4186
|
}
|
|
4187
|
+
getById(id) {
|
|
4188
|
+
const row = this.database.prepare(
|
|
4189
|
+
`
|
|
4190
|
+
SELECT
|
|
4191
|
+
id,
|
|
4192
|
+
chat_id,
|
|
4193
|
+
question_message_id,
|
|
4194
|
+
question,
|
|
4195
|
+
answer,
|
|
4196
|
+
citations_json,
|
|
4197
|
+
retrieval_debug_json,
|
|
4198
|
+
trace_json,
|
|
4199
|
+
status,
|
|
4200
|
+
error,
|
|
4201
|
+
created_at
|
|
4202
|
+
FROM qa_logs
|
|
4203
|
+
WHERE id = ?
|
|
4204
|
+
`
|
|
4205
|
+
).get(id);
|
|
4206
|
+
return row ? mapQaLogRow(row) : null;
|
|
4207
|
+
}
|
|
4160
4208
|
getCount() {
|
|
4161
4209
|
const row = this.database.prepare("SELECT COUNT(*) AS count FROM qa_logs").get();
|
|
4162
4210
|
return row.count;
|
|
@@ -4214,6 +4262,18 @@ ${block.text}`;
|
|
|
4214
4262
|
function toToolErrorContent(message) {
|
|
4215
4263
|
return JSON.stringify({ ok: false, error: message });
|
|
4216
4264
|
}
|
|
4265
|
+
function nowIso5() {
|
|
4266
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4267
|
+
}
|
|
4268
|
+
function finalizeTrace(trace, status, finalAnswer, startedAtMs) {
|
|
4269
|
+
return {
|
|
4270
|
+
...trace,
|
|
4271
|
+
completedAt: nowIso5(),
|
|
4272
|
+
durationMs: Date.now() - startedAtMs,
|
|
4273
|
+
status,
|
|
4274
|
+
finalAnswer
|
|
4275
|
+
};
|
|
4276
|
+
}
|
|
4217
4277
|
async function executeFeishuTool(tool, input2) {
|
|
4218
4278
|
const result = await tool.execute(input2);
|
|
4219
4279
|
if (isEvidenceBlockArray(result)) {
|
|
@@ -4225,6 +4285,13 @@ async function runFeishuToolLoop(input2) {
|
|
|
4225
4285
|
if (!input2.model.completeWithTools) {
|
|
4226
4286
|
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
4227
4287
|
}
|
|
4288
|
+
const startedAtMs = Date.now();
|
|
4289
|
+
const trace = {
|
|
4290
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
4291
|
+
modelTurns: [],
|
|
4292
|
+
toolResults: [],
|
|
4293
|
+
fallbacks: []
|
|
4294
|
+
};
|
|
4228
4295
|
const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
|
|
4229
4296
|
const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
4230
4297
|
const systemPromptParts = [FEISHU_TOOL_SYSTEM_PROMPT];
|
|
@@ -4253,19 +4320,36 @@ async function runFeishuToolLoop(input2) {
|
|
|
4253
4320
|
toolCalls: assistantResult.toolCalls,
|
|
4254
4321
|
reasoningContent: assistantResult.reasoningContent
|
|
4255
4322
|
});
|
|
4323
|
+
trace.modelTurns?.push({
|
|
4324
|
+
index: turn,
|
|
4325
|
+
content: assistantResult.content,
|
|
4326
|
+
reasoningContent: assistantResult.reasoningContent,
|
|
4327
|
+
toolCalls: assistantResult.toolCalls,
|
|
4328
|
+
createdAt: nowIso5()
|
|
4329
|
+
});
|
|
4256
4330
|
if (assistantResult.toolCalls.length === 0) {
|
|
4257
4331
|
if (hasRawToolCallMarkup) {
|
|
4332
|
+
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() });
|
|
4258
4333
|
break;
|
|
4259
4334
|
}
|
|
4260
|
-
|
|
4335
|
+
const answer = assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
|
|
4336
|
+
return { answer, trace: finalizeTrace(trace, "answered", answer, startedAtMs) };
|
|
4261
4337
|
}
|
|
4262
4338
|
for (const toolCall of assistantResult.toolCalls) {
|
|
4263
4339
|
if (toolCallsUsed >= maxToolCalls) {
|
|
4264
|
-
|
|
4340
|
+
trace.fallbacks?.push({ type: "tool_limit", message: FEISHU_TOOL_LOOP_LIMIT_REACHED, createdAt: nowIso5() });
|
|
4341
|
+
return { answer: FEISHU_TOOL_LOOP_LIMIT_REACHED, trace: finalizeTrace(trace, "failed", FEISHU_TOOL_LOOP_LIMIT_REACHED, startedAtMs) };
|
|
4265
4342
|
}
|
|
4266
4343
|
toolCallsUsed += 1;
|
|
4267
4344
|
const tool = toolsByName.get(toolCall.name);
|
|
4268
4345
|
if (!tool) {
|
|
4346
|
+
trace.toolResults?.push({
|
|
4347
|
+
toolCallId: toolCall.id,
|
|
4348
|
+
name: toolCall.name,
|
|
4349
|
+
input: toolCall.input,
|
|
4350
|
+
error: `\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`,
|
|
4351
|
+
createdAt: nowIso5()
|
|
4352
|
+
});
|
|
4269
4353
|
messages.push({
|
|
4270
4354
|
role: "tool",
|
|
4271
4355
|
toolCallId: toolCall.id,
|
|
@@ -4275,6 +4359,13 @@ async function runFeishuToolLoop(input2) {
|
|
|
4275
4359
|
}
|
|
4276
4360
|
try {
|
|
4277
4361
|
const result = await executeFeishuTool(tool, toolCall.input);
|
|
4362
|
+
trace.toolResults?.push({
|
|
4363
|
+
toolCallId: toolCall.id,
|
|
4364
|
+
name: toolCall.name,
|
|
4365
|
+
input: toolCall.input,
|
|
4366
|
+
content: result,
|
|
4367
|
+
createdAt: nowIso5()
|
|
4368
|
+
});
|
|
4278
4369
|
messages.push({
|
|
4279
4370
|
role: "tool",
|
|
4280
4371
|
toolCallId: toolCall.id,
|
|
@@ -4282,6 +4373,13 @@ async function runFeishuToolLoop(input2) {
|
|
|
4282
4373
|
});
|
|
4283
4374
|
} catch (error) {
|
|
4284
4375
|
const message = error instanceof Error ? error.message : String(error);
|
|
4376
|
+
trace.toolResults?.push({
|
|
4377
|
+
toolCallId: toolCall.id,
|
|
4378
|
+
name: toolCall.name,
|
|
4379
|
+
input: toolCall.input,
|
|
4380
|
+
error: message,
|
|
4381
|
+
createdAt: nowIso5()
|
|
4382
|
+
});
|
|
4285
4383
|
messages.push({
|
|
4286
4384
|
role: "tool",
|
|
4287
4385
|
toolCallId: toolCall.id,
|
|
@@ -4295,9 +4393,13 @@ async function runFeishuToolLoop(input2) {
|
|
|
4295
4393
|
...messages,
|
|
4296
4394
|
{ 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" }
|
|
4297
4395
|
]);
|
|
4298
|
-
|
|
4396
|
+
const answer = salvageAnswer || "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
4397
|
+
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() });
|
|
4398
|
+
return { answer, trace: finalizeTrace(trace, "answered", answer, startedAtMs) };
|
|
4299
4399
|
} catch {
|
|
4300
|
-
|
|
4400
|
+
const answer = "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
4401
|
+
trace.fallbacks?.push({ type: "answer_generation_failed", message: answer, createdAt: nowIso5() });
|
|
4402
|
+
return { answer, trace: finalizeTrace(trace, "failed", answer, startedAtMs) };
|
|
4301
4403
|
}
|
|
4302
4404
|
}
|
|
4303
4405
|
function formatConversationContext(records) {
|
|
@@ -4417,7 +4519,7 @@ var FeishuQuestionHandler = class {
|
|
|
4417
4519
|
const memberRepository = this.options.memberRepository ?? new FeishuMemberRepository(this.options.database);
|
|
4418
4520
|
const memberPrompt = formatFeishuMemberPrompt(memberRepository.listByChat(decision.chatId));
|
|
4419
4521
|
const conversationContext = formatConversationContext(qaLogs.listRecentByChat(decision.chatId, 6));
|
|
4420
|
-
const
|
|
4522
|
+
const result = await runFeishuToolLoop({
|
|
4421
4523
|
question: decision.question,
|
|
4422
4524
|
now,
|
|
4423
4525
|
tools: allTools,
|
|
@@ -4429,27 +4531,38 @@ var FeishuQuestionHandler = class {
|
|
|
4429
4531
|
chatId: decision.chatId,
|
|
4430
4532
|
questionMessageId,
|
|
4431
4533
|
question: decision.question,
|
|
4432
|
-
answer,
|
|
4534
|
+
answer: result.answer,
|
|
4433
4535
|
citations: [],
|
|
4434
4536
|
retrievalDebug: {},
|
|
4537
|
+
trace: result.trace,
|
|
4435
4538
|
status: "answered",
|
|
4436
4539
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4437
4540
|
});
|
|
4438
|
-
await this.sendResponse(decision.chatId, questionMessageId, answer);
|
|
4541
|
+
await this.sendResponse(decision.chatId, questionMessageId, result.answer);
|
|
4439
4542
|
} catch (error) {
|
|
4440
4543
|
const message = error instanceof Error ? error.message : String(error);
|
|
4544
|
+
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4545
|
+
const failedAnswer = `\u6682\u65F6\u65E0\u6CD5\u56DE\u7B54\uFF1A${message}`;
|
|
4441
4546
|
qaLogs.create({
|
|
4442
4547
|
chatId: decision.chatId,
|
|
4443
4548
|
questionMessageId,
|
|
4444
4549
|
question: decision.question,
|
|
4445
|
-
answer:
|
|
4550
|
+
answer: failedAnswer,
|
|
4446
4551
|
citations: [],
|
|
4447
4552
|
retrievalDebug: {},
|
|
4553
|
+
trace: {
|
|
4554
|
+
startedAt: now.toISOString(),
|
|
4555
|
+
completedAt: failedAt,
|
|
4556
|
+
durationMs: Math.max(0, Date.parse(failedAt) - now.getTime()),
|
|
4557
|
+
status: "failed",
|
|
4558
|
+
finalAnswer: failedAnswer,
|
|
4559
|
+
fallbacks: [{ type: "answer_generation_failed", message, createdAt: failedAt }]
|
|
4560
|
+
},
|
|
4448
4561
|
status: "failed",
|
|
4449
4562
|
error: message,
|
|
4450
|
-
createdAt:
|
|
4563
|
+
createdAt: failedAt
|
|
4451
4564
|
});
|
|
4452
|
-
await this.sendResponse(decision.chatId, questionMessageId,
|
|
4565
|
+
await this.sendResponse(decision.chatId, questionMessageId, failedAnswer);
|
|
4453
4566
|
}
|
|
4454
4567
|
return decision;
|
|
4455
4568
|
} finally {
|
|
@@ -4472,146 +4585,13 @@ function formatTextWithMentions(text, options) {
|
|
|
4472
4585
|
const prefix = mentions.map((mention) => `<at user_id="${escapeAtText(mention.openId)}">${escapeAtText(mention.name)}</at>`).join(" ");
|
|
4473
4586
|
return `${prefix} ${text}`.trim();
|
|
4474
4587
|
}
|
|
4475
|
-
function
|
|
4476
|
-
|
|
4477
|
-
for (let index2 = start; index2 < text.length; index2 += 1) {
|
|
4478
|
-
const char = text[index2];
|
|
4479
|
-
if (char === "(") {
|
|
4480
|
-
depth += 1;
|
|
4481
|
-
} else if (char === ")") {
|
|
4482
|
-
if (depth === 0) return index2;
|
|
4483
|
-
depth -= 1;
|
|
4484
|
-
}
|
|
4485
|
-
}
|
|
4486
|
-
return -1;
|
|
4487
|
-
}
|
|
4488
|
-
function stripInlineMarkdown(text) {
|
|
4489
|
-
let output = "";
|
|
4490
|
-
let index2 = 0;
|
|
4491
|
-
while (index2 < text.length) {
|
|
4492
|
-
const linkStart = text.indexOf("[", index2);
|
|
4493
|
-
const boldStarStart = text.indexOf("**", index2);
|
|
4494
|
-
const boldUnderscoreStart = text.indexOf("__", index2);
|
|
4495
|
-
const candidates = [linkStart, boldStarStart, boldUnderscoreStart].filter((value) => value >= 0);
|
|
4496
|
-
const next = candidates.length ? Math.min(...candidates) : -1;
|
|
4497
|
-
if (next < 0) {
|
|
4498
|
-
output += text.slice(index2);
|
|
4499
|
-
break;
|
|
4500
|
-
}
|
|
4501
|
-
output += text.slice(index2, next);
|
|
4502
|
-
if (next === linkStart) {
|
|
4503
|
-
const labelEnd = text.indexOf("](", next);
|
|
4504
|
-
if (labelEnd > next) {
|
|
4505
|
-
const hrefStart = labelEnd + 2;
|
|
4506
|
-
const hrefEnd = findMarkdownLinkEnd(text, hrefStart);
|
|
4507
|
-
const href = hrefEnd >= 0 ? text.slice(hrefStart, hrefEnd) : "";
|
|
4508
|
-
if (hrefEnd >= 0 && /^https?:\/\/\S+$/.test(href)) {
|
|
4509
|
-
output += `${text.slice(next + 1, labelEnd)} ${href}`;
|
|
4510
|
-
index2 = hrefEnd + 1;
|
|
4511
|
-
continue;
|
|
4512
|
-
}
|
|
4513
|
-
}
|
|
4514
|
-
output += text[next];
|
|
4515
|
-
index2 = next + 1;
|
|
4516
|
-
continue;
|
|
4517
|
-
}
|
|
4518
|
-
const marker = next === boldStarStart ? "**" : "__";
|
|
4519
|
-
const close = text.indexOf(marker, next + marker.length);
|
|
4520
|
-
if (close > next + marker.length) {
|
|
4521
|
-
output += text.slice(next + marker.length, close);
|
|
4522
|
-
index2 = close + marker.length;
|
|
4523
|
-
continue;
|
|
4524
|
-
}
|
|
4525
|
-
output += marker;
|
|
4526
|
-
index2 = next + marker.length;
|
|
4527
|
-
}
|
|
4528
|
-
return output;
|
|
4529
|
-
}
|
|
4530
|
-
function parseInline(text) {
|
|
4531
|
-
return [{ tag: "text", text: stripInlineMarkdown(text) || " " }];
|
|
4532
|
-
}
|
|
4533
|
-
function pushParagraph(content, lines) {
|
|
4534
|
-
if (lines.length === 0) return;
|
|
4535
|
-
content.push(parseInline(lines.join("\n")));
|
|
4536
|
-
lines.length = 0;
|
|
4537
|
-
}
|
|
4538
|
-
function parseMarkdownBlocks(markdown) {
|
|
4539
|
-
if (!markdown.trim()) {
|
|
4540
|
-
return [[{ tag: "text", text: " " }]];
|
|
4541
|
-
}
|
|
4542
|
-
const content = [];
|
|
4543
|
-
const paragraph = [];
|
|
4544
|
-
const code = [];
|
|
4545
|
-
let inCodeBlock = false;
|
|
4546
|
-
for (const rawLine of markdown.replace(/\r\n/g, "\n").split("\n")) {
|
|
4547
|
-
const line = rawLine.trimEnd();
|
|
4548
|
-
if (line.startsWith("```")) {
|
|
4549
|
-
if (inCodeBlock) {
|
|
4550
|
-
content.push([{ tag: "text", text: `\`\`\`
|
|
4551
|
-
${code.join("\n")}
|
|
4552
|
-
\`\`\`` }]);
|
|
4553
|
-
code.length = 0;
|
|
4554
|
-
inCodeBlock = false;
|
|
4555
|
-
} else {
|
|
4556
|
-
pushParagraph(content, paragraph);
|
|
4557
|
-
inCodeBlock = true;
|
|
4558
|
-
}
|
|
4559
|
-
continue;
|
|
4560
|
-
}
|
|
4561
|
-
if (inCodeBlock) {
|
|
4562
|
-
code.push(rawLine);
|
|
4563
|
-
continue;
|
|
4564
|
-
}
|
|
4565
|
-
if (!line.trim()) {
|
|
4566
|
-
pushParagraph(content, paragraph);
|
|
4567
|
-
continue;
|
|
4568
|
-
}
|
|
4569
|
-
const heading = line.match(/^#{1,6}\s+(.+)$/);
|
|
4570
|
-
if (heading) {
|
|
4571
|
-
pushParagraph(content, paragraph);
|
|
4572
|
-
content.push([{ tag: "text", text: stripInlineMarkdown(heading[1]) || " " }]);
|
|
4573
|
-
continue;
|
|
4574
|
-
}
|
|
4575
|
-
const unordered = line.match(/^[-*]\s+(.+)$/);
|
|
4576
|
-
if (unordered) {
|
|
4577
|
-
pushParagraph(content, paragraph);
|
|
4578
|
-
content.push(parseInline(`\u2022 ${unordered[1]}`));
|
|
4579
|
-
continue;
|
|
4580
|
-
}
|
|
4581
|
-
const ordered = line.match(/^(\d+)\.\s+(.+)$/);
|
|
4582
|
-
if (ordered) {
|
|
4583
|
-
pushParagraph(content, paragraph);
|
|
4584
|
-
content.push(parseInline(`${ordered[1]}. ${ordered[2]}`));
|
|
4585
|
-
continue;
|
|
4586
|
-
}
|
|
4587
|
-
paragraph.push(line);
|
|
4588
|
-
}
|
|
4589
|
-
if (inCodeBlock) {
|
|
4590
|
-
content.push([{ tag: "text", text: `\`\`\`
|
|
4591
|
-
${code.join("\n")}` }]);
|
|
4592
|
-
}
|
|
4593
|
-
pushParagraph(content, paragraph);
|
|
4594
|
-
return content.length ? content : [[{ tag: "text", text: markdown }]];
|
|
4588
|
+
function buildMarkdownText(markdown, options) {
|
|
4589
|
+
return formatTextWithMentions(markdown.trim() || " ", options);
|
|
4595
4590
|
}
|
|
4596
4591
|
function buildFeishuPostContent(markdown, options) {
|
|
4597
|
-
const content = parseMarkdownBlocks(markdown);
|
|
4598
|
-
const mentions = options?.mentions ?? [];
|
|
4599
|
-
if (mentions.length) {
|
|
4600
|
-
const firstLine = content[0] ?? [];
|
|
4601
|
-
const firstText = firstLine[0];
|
|
4602
|
-
const prefix = mentions.map((mention) => `@${mention.name}`).join(" ");
|
|
4603
|
-
if (firstText?.tag === "text") {
|
|
4604
|
-
content[0] = [{ tag: "text", text: `${prefix} ${firstText.text}` }, ...firstLine.slice(1)];
|
|
4605
|
-
} else {
|
|
4606
|
-
content[0] = [{ tag: "text", text: `${prefix} ` }, ...firstLine];
|
|
4607
|
-
}
|
|
4608
|
-
}
|
|
4609
4592
|
return {
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
title: "",
|
|
4613
|
-
content
|
|
4614
|
-
}
|
|
4593
|
+
zh_cn: {
|
|
4594
|
+
content: [[{ tag: "md", text: buildMarkdownText(markdown, options) }]]
|
|
4615
4595
|
}
|
|
4616
4596
|
};
|
|
4617
4597
|
}
|
|
@@ -6476,12 +6456,15 @@ function buildHtml() {
|
|
|
6476
6456
|
let allFileJobs = [];
|
|
6477
6457
|
let allCronJobs = [];
|
|
6478
6458
|
let allQaLogs = [];
|
|
6459
|
+
let selectedQaLogId = null;
|
|
6479
6460
|
let statusData = null;
|
|
6480
6461
|
|
|
6481
6462
|
function fmt(value) { return value == null || value === "" ? "-" : String(value); }
|
|
6482
6463
|
function escapeHtml(value) {
|
|
6483
6464
|
return fmt(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
6484
6465
|
}
|
|
6466
|
+
function renderJson(value) { return '<pre style="white-space:pre-wrap;overflow:auto;max-height:320px;">' + escapeHtml(JSON.stringify(value, null, 2)) + '</pre>'; }
|
|
6467
|
+
function renderTextBlock(value) { return '<pre style="white-space:pre-wrap;overflow:auto;max-height:320px;">' + escapeHtml(value || "") + '</pre>'; }
|
|
6485
6468
|
function isOpaqueId(value) { return /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(fmt(value)); }
|
|
6486
6469
|
function formatDateTime(value) {
|
|
6487
6470
|
var date = new Date(value);
|
|
@@ -6754,17 +6737,72 @@ function buildHtml() {
|
|
|
6754
6737
|
for (var i = 0; i < allQaLogs.length; i++) {
|
|
6755
6738
|
var item = allQaLogs[i];
|
|
6756
6739
|
var citationCount = Array.isArray(item.citations) ? item.citations.length : 0;
|
|
6757
|
-
var statusClass = item.status === '
|
|
6740
|
+
var statusClass = item.status === 'answered' ? 'tag-success' : 'tag-warning';
|
|
6758
6741
|
html += '<div class="qa-card"><div class="message-meta" style="margin-bottom:var(--space-sm);">' +
|
|
6759
6742
|
'<span>' + escapeHtml(formatDateTime(item.createdAt)) + '</span>' +
|
|
6760
6743
|
'<span class="tag ' + statusClass + '">' + escapeHtml(item.status) + '</span>' +
|
|
6761
|
-
'<span>' + citationCount + ' \u6761\u5F15\u7528</span
|
|
6744
|
+
'<span>' + citationCount + ' \u6761\u5F15\u7528</span>' +
|
|
6745
|
+
'<span class="tag ' + (item.hasTrace ? 'tag-info' : 'tag-warning') + '">' + (item.hasTrace ? '\u6709 trace' : '\u65E0 trace') + '</span></div>' +
|
|
6762
6746
|
'<div class="qa-question">' + escapeHtml(item.question) + '</div>' +
|
|
6763
|
-
'<div class="qa-answer">' + escapeHtml(item.answer) + '</div
|
|
6747
|
+
'<div class="qa-answer">' + escapeHtml(item.answer) + '</div>' +
|
|
6748
|
+
'<button class="btn btn-sm" style="margin-top:var(--space-sm);" data-view-qa-log="' + escapeHtml(item.id) + '">\u67E5\u770B\u8BE6\u60C5</button>' +
|
|
6749
|
+
'<div id="qa-detail-' + escapeHtml(item.id) + '" style="margin-top:var(--space-md);"></div></div>';
|
|
6764
6750
|
}
|
|
6765
6751
|
el.innerHTML = html;
|
|
6766
6752
|
}
|
|
6767
6753
|
|
|
6754
|
+
async function showQaLogDetail(id) {
|
|
6755
|
+
selectedQaLogId = id;
|
|
6756
|
+
var container = document.getElementById("qa-detail-" + id);
|
|
6757
|
+
if (!container) return;
|
|
6758
|
+
container.innerHTML = '<div class="empty-state">\u6B63\u5728\u52A0\u8F7D\u95EE\u7B54\u8BE6\u60C5...</div>';
|
|
6759
|
+
try {
|
|
6760
|
+
var item = await fetchJson("/api/qa-logs/" + encodeURIComponent(id));
|
|
6761
|
+
renderQaLogDetail(item);
|
|
6762
|
+
} catch (error) {
|
|
6763
|
+
container.innerHTML = '<div class="empty-state">\u8BE6\u60C5\u52A0\u8F7D\u5931\u8D25\uFF1A' + escapeHtml(error instanceof Error ? error.message : String(error)) + '</div>';
|
|
6764
|
+
}
|
|
6765
|
+
}
|
|
6766
|
+
|
|
6767
|
+
function renderQaLogDetail(item) {
|
|
6768
|
+
var container = document.getElementById("qa-detail-" + item.id);
|
|
6769
|
+
if (!container) return;
|
|
6770
|
+
var trace = item.trace || {};
|
|
6771
|
+
var html = '<div class="content-panel" style="margin-top:var(--space-sm);background:rgba(255,255,255,0.03);">';
|
|
6772
|
+
html += '<h3 style="font-size:15px;margin-bottom:var(--space-sm);">\u95EE\u7B54\u8BE6\u60C5</h3>';
|
|
6773
|
+
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>';
|
|
6774
|
+
if (item.error) html += '<div style="color:var(--danger);margin-bottom:var(--space-sm);">\u9519\u8BEF\uFF1A' + escapeHtml(item.error) + '</div>';
|
|
6775
|
+
html += '<div class="qa-question">' + escapeHtml(item.question) + '</div>';
|
|
6776
|
+
html += '<div class="qa-answer" style="margin-bottom:var(--space-md);">' + escapeHtml(item.answer) + '</div>';
|
|
6777
|
+
if (!item.hasTrace) {
|
|
6778
|
+
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>';
|
|
6779
|
+
container.innerHTML = html;
|
|
6780
|
+
return;
|
|
6781
|
+
}
|
|
6782
|
+
var turns = trace.modelTurns || [];
|
|
6783
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">Reasoning</h4>';
|
|
6784
|
+
if (turns.length === 0) html += '<div class="empty-state">\u65E0 reasoningContent</div>';
|
|
6785
|
+
for (var i = 0; i < turns.length; i++) {
|
|
6786
|
+
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>';
|
|
6787
|
+
}
|
|
6788
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">\u6A21\u578B\u8F6E\u6B21\u4E0E\u5DE5\u5177\u8C03\u7528</h4>';
|
|
6789
|
+
for (var j = 0; j < turns.length; j++) {
|
|
6790
|
+
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>';
|
|
6791
|
+
}
|
|
6792
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">\u5DE5\u5177\u7ED3\u679C</h4>';
|
|
6793
|
+
var toolResults = trace.toolResults || [];
|
|
6794
|
+
if (toolResults.length === 0) html += '<div class="empty-state">\u6CA1\u6709\u5DE5\u5177\u7ED3\u679C\u3002</div>';
|
|
6795
|
+
for (var k = 0; k < toolResults.length; k++) {
|
|
6796
|
+
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>';
|
|
6797
|
+
}
|
|
6798
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">\u5F15\u7528\u4E0E\u68C0\u7D22</h4>' + renderJson({ citations: item.citations || [], retrievalDebug: item.retrievalDebug || {} });
|
|
6799
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">Fallback</h4>';
|
|
6800
|
+
var fallbacks = trace.fallbacks || [];
|
|
6801
|
+
html += fallbacks.length === 0 ? '<div class="empty-state">\u6CA1\u6709 fallback\u3002</div>' : renderJson(fallbacks);
|
|
6802
|
+
html += '</div>';
|
|
6803
|
+
container.innerHTML = html;
|
|
6804
|
+
}
|
|
6805
|
+
|
|
6768
6806
|
function renderSettings(status) {
|
|
6769
6807
|
var el = document.getElementById("settings-config");
|
|
6770
6808
|
var html = '<h3 style="font-size:16px;font-weight:600;margin-bottom:var(--space-md);">\u7CFB\u7EDF\u914D\u7F6E</h3>';
|
|
@@ -6799,7 +6837,10 @@ function buildHtml() {
|
|
|
6799
6837
|
if (currentView === "episodes") renderEpisodesView();
|
|
6800
6838
|
if (currentView === "files") renderFilesView();
|
|
6801
6839
|
if (currentView === "tasks") renderTasksView();
|
|
6802
|
-
if (currentView === "qa-logs")
|
|
6840
|
+
if (currentView === "qa-logs") {
|
|
6841
|
+
renderQaLogsView();
|
|
6842
|
+
if (selectedQaLogId) void showQaLogDetail(selectedQaLogId);
|
|
6843
|
+
}
|
|
6803
6844
|
}
|
|
6804
6845
|
|
|
6805
6846
|
async function processNow() {
|
|
@@ -6821,6 +6862,11 @@ function buildHtml() {
|
|
|
6821
6862
|
document.addEventListener("click", async function(event) {
|
|
6822
6863
|
var target = event.target;
|
|
6823
6864
|
if (!(target instanceof HTMLElement)) return;
|
|
6865
|
+
var qaLogId = target.dataset.viewQaLog;
|
|
6866
|
+
if (qaLogId) {
|
|
6867
|
+
void showQaLogDetail(qaLogId);
|
|
6868
|
+
return;
|
|
6869
|
+
}
|
|
6824
6870
|
var id = target.dataset.deleteCronJob;
|
|
6825
6871
|
if (!id) return;
|
|
6826
6872
|
target.disabled = true;
|
|
@@ -6864,6 +6910,10 @@ function parseCookies(header) {
|
|
|
6864
6910
|
function isAuthorizedWebAction(request, token) {
|
|
6865
6911
|
return parseCookies(request.headers.cookie).chattercatcher_web_token === token;
|
|
6866
6912
|
}
|
|
6913
|
+
function toQaLogListItem(log) {
|
|
6914
|
+
const { trace: _trace, ...item } = log;
|
|
6915
|
+
return item;
|
|
6916
|
+
}
|
|
6867
6917
|
function createWebApp(config, options = {}) {
|
|
6868
6918
|
const app = Fastify({ logger: false });
|
|
6869
6919
|
const database = openDatabase(config);
|
|
@@ -6942,9 +6992,18 @@ function createWebApp(config, options = {}) {
|
|
|
6942
6992
|
app.get("/api/qa-logs", async (request) => {
|
|
6943
6993
|
const limit = parseLimit(request.query.limit, 20, 100);
|
|
6944
6994
|
return {
|
|
6945
|
-
items: qaLogs.listRecent(limit)
|
|
6995
|
+
items: qaLogs.listRecent(limit).map(toQaLogListItem)
|
|
6946
6996
|
};
|
|
6947
6997
|
});
|
|
6998
|
+
app.get("/api/qa-logs/:id", async (request, reply) => {
|
|
6999
|
+
const id = request.params.id;
|
|
7000
|
+
const log = qaLogs.getById(id);
|
|
7001
|
+
if (!log) {
|
|
7002
|
+
reply.code(404);
|
|
7003
|
+
return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u95EE\u7B54\u65E5\u5FD7\u3002" };
|
|
7004
|
+
}
|
|
7005
|
+
return log;
|
|
7006
|
+
});
|
|
6948
7007
|
app.get("/api/cron-jobs", async (request) => {
|
|
6949
7008
|
const limit = parseLimit(request.query.limit, 50, 200);
|
|
6950
7009
|
return {
|