palz-connector 1.6.6 → 1.6.7
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/palz-connector.config.json +3 -2
- package/palz-connector.dev.config.json +3 -2
- package/palz-connector.prod.config.json +3 -2
- package/palz-connector.staging.config.json +3 -2
- package/src/activity.js +15 -11
- package/src/bot.js +145 -57
- package/src/channel.js +6 -3
- package/src/config.js +1 -0
- package/src/control-server.js +61 -14
- package/src/kb-ingest.js +227 -0
- package/src/kb-task-callback.js +53 -0
- package/src/media.js +6 -4
- package/src/monitor.js +11 -9
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"clawGatewayUrl": "http://claw-gateway:8080",
|
|
6
6
|
"activityReportEnabled": false,
|
|
7
7
|
"sessionTimeout": 1800000,
|
|
8
|
-
"controlPort": 8008,
|
|
9
8
|
"groupContextCache": false,
|
|
10
|
-
"showProcess": true
|
|
9
|
+
"showProcess": true,
|
|
10
|
+
"kbEngineUrl": "http://127.0.0.1:8070",
|
|
11
|
+
"controlPort": 8008
|
|
11
12
|
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"clawGatewayUrl": "http://claw-gateway:8080",
|
|
6
6
|
"activityReportEnabled": true,
|
|
7
7
|
"sessionTimeout": 1800000,
|
|
8
|
-
"controlPort": 8008,
|
|
9
8
|
"groupContextCache": false,
|
|
10
|
-
"showProcess": true
|
|
9
|
+
"showProcess": true,
|
|
10
|
+
"kbEngineUrl": "http://uniclaw-kb-engine:8070",
|
|
11
|
+
"controlPort": 8008
|
|
11
12
|
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"clawGatewayUrl": "http://claw-gateway:8080",
|
|
6
6
|
"activityReportEnabled": true,
|
|
7
7
|
"sessionTimeout": 1800000,
|
|
8
|
-
"controlPort": 8008,
|
|
9
8
|
"groupContextCache": false,
|
|
10
|
-
"showProcess": true
|
|
9
|
+
"showProcess": true,
|
|
10
|
+
"kbEngineUrl": "http://uniclaw-kb-engine:8070",
|
|
11
|
+
"controlPort": 8008
|
|
11
12
|
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"clawGatewayUrl": "http://claw-gateway:8080",
|
|
6
6
|
"activityReportEnabled": true,
|
|
7
7
|
"sessionTimeout": 1800000,
|
|
8
|
-
"controlPort": 8008,
|
|
9
8
|
"groupContextCache": false,
|
|
10
|
-
"showProcess": true
|
|
9
|
+
"showProcess": true,
|
|
10
|
+
"kbEngineUrl": "http://uniclaw-kb-engine:8070",
|
|
11
|
+
"controlPort": 8008
|
|
11
12
|
}
|
package/src/activity.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Palz IM
|
|
2
|
+
* Palz IM 收到消息后的 claw-gateway 活动上报。
|
|
3
3
|
*/
|
|
4
4
|
const ACTIVITY_REPORT_TIMEOUT_MS = 2000;
|
|
5
5
|
const ACTIVITY_REPORT_PATH_PREFIX = "/openclaw-gateway/be/deployments";
|
|
6
6
|
const warnedMissingConfig = new Set();
|
|
7
|
-
const SERVICE_NAME = process.env.OPENCLAW_WORKSPACE_POOL_SERVICE_NAME ?? "";
|
|
8
7
|
function formatDateTimeWithOffset(date) {
|
|
9
8
|
const pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
10
9
|
const offsetMinutes = -date.getTimezoneOffset();
|
|
@@ -24,7 +23,7 @@ function resolveActivityUrl(baseUrl, releaseName) {
|
|
|
24
23
|
return `${normalizedBase}${ACTIVITY_REPORT_PATH_PREFIX}/${encodeURIComponent(releaseName)}/activity`;
|
|
25
24
|
}
|
|
26
25
|
export async function reportPalzActivity(params) {
|
|
27
|
-
const { config,
|
|
26
|
+
const { config, msg, receivedAt, runtime, accountId } = params;
|
|
28
27
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
29
28
|
const error = typeof runtime?.error === "function" ? runtime.error : console.error;
|
|
30
29
|
const tag = `palz[${accountId ?? config.botId ?? "default"}]`;
|
|
@@ -42,17 +41,22 @@ export async function reportPalzActivity(params) {
|
|
|
42
41
|
}
|
|
43
42
|
const payload = {
|
|
44
43
|
source: "palz-connector",
|
|
45
|
-
eventType: "
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
eventType: "im_message_received",
|
|
45
|
+
messageId: msg.msg_id,
|
|
46
|
+
conversationId: msg.conversation_id,
|
|
47
|
+
conversationType: msg.conversation_type || "direct",
|
|
48
|
+
userId: msg.owner_id ?? "",
|
|
49
|
+
senderId: msg.sender_id,
|
|
50
|
+
receivedAt: formatDateTimeWithOffset(receivedAt),
|
|
49
51
|
};
|
|
52
|
+
if (msg.traceparent) {
|
|
53
|
+
payload.traceId = msg.traceparent;
|
|
54
|
+
}
|
|
50
55
|
const url = resolveActivityUrl(baseUrl, releaseName);
|
|
51
56
|
const body = JSON.stringify(payload);
|
|
52
57
|
const controller = new AbortController();
|
|
53
58
|
const timeout = setTimeout(() => controller.abort(), ACTIVITY_REPORT_TIMEOUT_MS);
|
|
54
59
|
const startMs = Date.now();
|
|
55
|
-
log(`${tag}: [ACTIVITY_REPORT] reporting eventType=ws_connected url=${url} body=${body}`);
|
|
56
60
|
try {
|
|
57
61
|
const response = await fetch(url, {
|
|
58
62
|
method: "POST",
|
|
@@ -63,15 +67,15 @@ export async function reportPalzActivity(params) {
|
|
|
63
67
|
const elapsedMs = Date.now() - startMs;
|
|
64
68
|
const responseText = await response.text().catch(() => "");
|
|
65
69
|
if (!response.ok) {
|
|
66
|
-
error(`${tag}: [ACTIVITY_REPORT] failed status=${response.status} elapsed=${elapsedMs}ms response=${responseText.slice(0, 300)}`);
|
|
70
|
+
error(`${tag}: [ACTIVITY_REPORT] failed status=${response.status} elapsed=${elapsedMs}ms msg_id=${msg.msg_id} response=${responseText.slice(0, 300)}`);
|
|
67
71
|
return;
|
|
68
72
|
}
|
|
69
|
-
log(`${tag}: [ACTIVITY_REPORT] ok status=${response.status} elapsed=${elapsedMs}ms
|
|
73
|
+
log(`${tag}: [ACTIVITY_REPORT] ok status=${response.status} elapsed=${elapsedMs}ms msg_id=${msg.msg_id}`);
|
|
70
74
|
}
|
|
71
75
|
catch (err) {
|
|
72
76
|
const elapsedMs = Date.now() - startMs;
|
|
73
77
|
const reason = err?.name === "AbortError" ? `timeout ${ACTIVITY_REPORT_TIMEOUT_MS}ms` : (err?.message ?? String(err));
|
|
74
|
-
error(`${tag}: [ACTIVITY_REPORT] error elapsed=${elapsedMs}ms reason=${reason}`);
|
|
78
|
+
error(`${tag}: [ACTIVITY_REPORT] error elapsed=${elapsedMs}ms msg_id=${msg.msg_id} reason=${reason}`);
|
|
75
79
|
}
|
|
76
80
|
finally {
|
|
77
81
|
clearTimeout(timeout);
|
package/src/bot.js
CHANGED
|
@@ -9,11 +9,14 @@
|
|
|
9
9
|
* - 通过 OpenClaw Runtime 的 dispatchReplyFromConfig 完成 AI 对话
|
|
10
10
|
* - Session 管理交由 OpenClaw Runtime(resolveAgentRoute)
|
|
11
11
|
*/
|
|
12
|
+
import { hostname as getHostname } from "node:os";
|
|
12
13
|
import { getPalzRuntime } from "./runtime.js";
|
|
13
14
|
import { resolvePalzAccount } from "./config.js";
|
|
14
15
|
import { tryClaimMessage } from "./dedup.js";
|
|
15
16
|
import { createPalzReplyDispatcher } from "./reply-dispatcher.js";
|
|
16
17
|
import { resolvePalzMediaList, resolveMediaLocalRoots } from "./media.js";
|
|
18
|
+
import { extractFileUrls, ingestFilesToKbEngine } from "./kb-ingest.js";
|
|
19
|
+
import { consumeKbTaskContext, extractKbTaskCallbackContent, extractKbTaskId, resolveKbTaskContext, } from "./kb-task-callback.js";
|
|
17
20
|
import { sendToPalzIM } from "./send.js";
|
|
18
21
|
import { buildPassthroughFromMsg } from "./message-utils.js";
|
|
19
22
|
import { tracer, trace, context, SpanStatusCode } from "./tracing.js";
|
|
@@ -206,10 +209,6 @@ function buildMediaPayload(mediaList) {
|
|
|
206
209
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
207
210
|
};
|
|
208
211
|
}
|
|
209
|
-
function shellSingleQuote(value) {
|
|
210
|
-
const raw = String(value ?? "");
|
|
211
|
-
return `'${raw.replace(/'/g, "'\"'\"'")}'`;
|
|
212
|
-
}
|
|
213
212
|
function buildFileSharingSystemPrompt() {
|
|
214
213
|
return [
|
|
215
214
|
"## File Sharing",
|
|
@@ -223,29 +222,40 @@ function buildFileSharingSystemPrompt() {
|
|
|
223
222
|
"Multiple files: one MEDIA: line per file.",
|
|
224
223
|
].join("\n");
|
|
225
224
|
}
|
|
226
|
-
function
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
`--group-owner-id ${shellSingleQuote(params.groupOwnerId)}`,
|
|
236
|
-
`--user-id ${shellSingleQuote(params.ownerId)}`,
|
|
237
|
-
].join(" ")
|
|
238
|
-
: "python3 skills/knowledge-mgmt/recall/kb-recall.py topics '<用户问题>' --group-id '<group_id>' --conversation-type 'group' --owner-id '<owner_id>' --group-owner-id '<group_owner_id>' --user-id '<owner_id>'";
|
|
225
|
+
function buildKbIngestNoticeMessage(results) {
|
|
226
|
+
const lines = results.map((result, index) => {
|
|
227
|
+
const details = [
|
|
228
|
+
`task_id=${result.taskId ?? "(unknown)"}`,
|
|
229
|
+
`state=${result.state ?? "(unknown)"}`,
|
|
230
|
+
result.kind ? `kind=${result.kind}` : "",
|
|
231
|
+
].filter(Boolean);
|
|
232
|
+
return `${index + 1}. ${details.join(", ")}`;
|
|
233
|
+
});
|
|
239
234
|
return [
|
|
240
|
-
"
|
|
241
|
-
"
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
|
|
245
|
-
"Replace `<用户问题>` with the current user's actual question text. Follow the runner JSON `next_step`, `repair`, and `answer_contract`; do not manually read wiki/index files or invent citation links.",
|
|
246
|
-
"Fallback to web/general knowledge is allowed only after `no_match`, `local_not_found`, `remote_unavailable`, or `remote_error`. In group chat, missing owner/group-owner context must not fallback to direct/default.",
|
|
235
|
+
"[Knowledge Ingest Status]",
|
|
236
|
+
"The current message includes file(s) that have just been submitted for knowledge-base ingestion.",
|
|
237
|
+
"This status is informational only. You may refer to it if relevant, but continue handling the user's current request normally. Do not wait for ingestion to finish, and do not change your own intended actions solely because of this status.",
|
|
238
|
+
"Created ingest task(s):",
|
|
239
|
+
...lines,
|
|
247
240
|
].join("\n");
|
|
248
241
|
}
|
|
242
|
+
function resolveWorkspacePoolServiceName() {
|
|
243
|
+
const explicit = process.env.OPENCLAW_WORKSPACE_POOL_SERVICE_NAME?.trim();
|
|
244
|
+
if (explicit)
|
|
245
|
+
return explicit;
|
|
246
|
+
const currentHostname = getHostname().trim();
|
|
247
|
+
const parts = currentHostname.split("-").filter(Boolean);
|
|
248
|
+
if (parts.length >= 3)
|
|
249
|
+
return parts.slice(0, 3).join("-");
|
|
250
|
+
return currentHostname || "127.0.0.1";
|
|
251
|
+
}
|
|
252
|
+
function resolveKbIngestCallbackUrl(config) {
|
|
253
|
+
if (typeof config.controlPort === "number" && config.controlPort > 0) {
|
|
254
|
+
const host = resolveWorkspacePoolServiceName();
|
|
255
|
+
return `http://${host}:${config.controlPort}/admin/kb-ingest/callback`;
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
249
259
|
export async function handlePalzMessage(params) {
|
|
250
260
|
const { msg } = params;
|
|
251
261
|
// 在当前 trace context(由 monitor.ts ws_recv span 建立)下创建子 span
|
|
@@ -268,7 +278,7 @@ export async function handlePalzMessage(params) {
|
|
|
268
278
|
});
|
|
269
279
|
}
|
|
270
280
|
async function _handlePalzMessageInner(params) {
|
|
271
|
-
const { cfg, msg, runtime, accountId, agentId } = params;
|
|
281
|
+
const { cfg, msg, runtime, accountId, agentId, suppressAck } = params;
|
|
272
282
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
273
283
|
const error = typeof runtime?.error === "function" ? runtime.error : console.error;
|
|
274
284
|
const tag = `palz[${accountId}]`;
|
|
@@ -352,25 +362,27 @@ async function _handlePalzMessageInner(params) {
|
|
|
352
362
|
}
|
|
353
363
|
// 立即发送确认消息(在入队之前,不受队列阻塞影响)
|
|
354
364
|
const groupId = isGroup ? msg.conversation_id : undefined;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
365
|
+
if (!suppressAck) {
|
|
366
|
+
try {
|
|
367
|
+
log(`${tag}: [ACK] 发送确认消息 conv=${msg.conversation_id} sender=${msg.sender_id}`);
|
|
368
|
+
await sendToPalzIM({
|
|
369
|
+
config: account.config,
|
|
370
|
+
conversationId: msg.conversation_id,
|
|
371
|
+
content: "收到,正在思考中…",
|
|
372
|
+
conversationType: msg.conversation_type || "direct",
|
|
373
|
+
msgId: msg.msg_id,
|
|
374
|
+
senderId: msg.sender_id,
|
|
375
|
+
msgType: msg.msg_type,
|
|
376
|
+
groupId,
|
|
377
|
+
lobsterId: msg.lobster_id,
|
|
378
|
+
palzMsgType: "thinking",
|
|
379
|
+
passthrough: buildPassthroughFromMsg(msg),
|
|
380
|
+
});
|
|
381
|
+
log(`${tag}: [ACK] 确认消息发送成功`);
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
log(`${tag}: [ACK] 确认消息发送失败(不影响主流程): ${String(err)}`);
|
|
385
|
+
}
|
|
374
386
|
}
|
|
375
387
|
// 入队(按 agentId 隔离,不同 agent 并行处理)
|
|
376
388
|
const queueKey = isGroup
|
|
@@ -455,9 +467,37 @@ async function _dispatchPalzMessageInner(params) {
|
|
|
455
467
|
span?.addEvent(step4Input);
|
|
456
468
|
const mediaList = await resolvePalzMediaList(msg.content, log);
|
|
457
469
|
let mediaPayload = buildMediaPayload(mediaList);
|
|
458
|
-
const
|
|
470
|
+
const fileMd5ByUrl = new Map(mediaList
|
|
471
|
+
.filter((m) => m.sourceUrl && m.md5)
|
|
472
|
+
.map((m) => [m.sourceUrl, m.md5]));
|
|
473
|
+
const step4Output = `[STEP 4 输出] mediaList=${JSON.stringify(mediaList.map((m) => ({ path: m.path, contentType: m.contentType, sourceUrl: m.sourceUrl, md5: m.md5 })))} mediaPayload=${JSON.stringify(mediaPayload)}`;
|
|
459
474
|
log(`${tag}: ${step4Output}`);
|
|
460
475
|
span?.addEvent(step4Output);
|
|
476
|
+
// STEP 4b: 同步提交知识库入库;单次请求内部 3s 超时,超时只打日志并继续后续流程
|
|
477
|
+
let kbIngestNoticeBody = "";
|
|
478
|
+
const kbEngineUrl = config.kbEngineUrl;
|
|
479
|
+
if (kbEngineUrl) {
|
|
480
|
+
const kbFileUrls = extractFileUrls(msg.content);
|
|
481
|
+
if (kbFileUrls.length > 0) {
|
|
482
|
+
log(`${tag}: [STEP 4b kb-ingest] 触发入库 files=${kbFileUrls.length} md5=${fileMd5ByUrl.size}/${kbFileUrls.length} engine=${kbEngineUrl}`);
|
|
483
|
+
const ingestResults = await ingestFilesToKbEngine({
|
|
484
|
+
kbEngineUrl,
|
|
485
|
+
msg,
|
|
486
|
+
fileUrls: kbFileUrls,
|
|
487
|
+
fileMd5ByUrl,
|
|
488
|
+
accountId,
|
|
489
|
+
agentId,
|
|
490
|
+
callbackUrl: resolveKbIngestCallbackUrl(config),
|
|
491
|
+
log,
|
|
492
|
+
});
|
|
493
|
+
const createdTasks = ingestResults.filter((result) => result.taskCreated === true);
|
|
494
|
+
log(`${tag}: [STEP 4b kb-ingest] 完成 results=${ingestResults.length} task_created=${createdTasks.length}`);
|
|
495
|
+
if (createdTasks.length > 0) {
|
|
496
|
+
kbIngestNoticeBody = buildKbIngestNoticeMessage(createdTasks);
|
|
497
|
+
span?.addEvent(`[STEP 4b kb-ingest] 插入消息提示 task_created=${createdTasks.length}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
461
501
|
// STEP 5: 解析路由
|
|
462
502
|
const peerKind = isGroup ? "group" : "direct";
|
|
463
503
|
const routeInput = { cfg: "(cfg)", channel: "palz-connector", accountId, peer: { kind: peerKind, id: peerId } };
|
|
@@ -500,6 +540,15 @@ async function _dispatchPalzMessageInner(params) {
|
|
|
500
540
|
const step6aOutput = `[STEP 6a 输出] envelope body (len=${body.length}): "${body.slice(0, 200)}"`;
|
|
501
541
|
log(`${tag}: ${step6aOutput}`);
|
|
502
542
|
span?.addEvent(step6aOutput);
|
|
543
|
+
const kbIngestNoticeEnvelope = kbIngestNoticeBody
|
|
544
|
+
? core.channel.reply.formatAgentEnvelope({
|
|
545
|
+
channel: "Palz",
|
|
546
|
+
from: "kb-engine",
|
|
547
|
+
timestamp: new Date(),
|
|
548
|
+
body: kbIngestNoticeBody,
|
|
549
|
+
envelope: envelopeOptions,
|
|
550
|
+
})
|
|
551
|
+
: "";
|
|
503
552
|
// STEP 6b: 构建 inbound context
|
|
504
553
|
const palzFrom = `palz:${msg.sender_id}:${msg.conversation_id}`;
|
|
505
554
|
// 新格式: {conversationType}:{senderId}:{lobsterId}:{conversationId}
|
|
@@ -520,12 +569,12 @@ async function _dispatchPalzMessageInner(params) {
|
|
|
520
569
|
// 群聊历史:将积攒的未@消息拼入 Body 上下文(仅 groupContextCache=true 时生效)
|
|
521
570
|
const groupContextCacheEnabled = account.config.groupContextCache !== false;
|
|
522
571
|
const historyKey = isGroup && groupContextCacheEnabled ? `${effectiveAgentId}:${msg.conversation_id}` : undefined;
|
|
523
|
-
let combinedBody = body;
|
|
572
|
+
let combinedBody = kbIngestNoticeEnvelope ? `${kbIngestNoticeEnvelope}\n\n${body}` : body;
|
|
524
573
|
if (isGroup && historyKey && groupContextCacheEnabled) {
|
|
525
574
|
log(`${tag}: [STEP 6b 群聊历史] 开始构建, historyKey=${historyKey} bodyLen=${body.length}`);
|
|
526
575
|
combinedBody = buildGroupHistoryContext({
|
|
527
576
|
historyKey,
|
|
528
|
-
currentMessage:
|
|
577
|
+
currentMessage: combinedBody,
|
|
529
578
|
formatEntry: (entry) => core.channel.reply.formatAgentEnvelope({
|
|
530
579
|
channel: "Palz",
|
|
531
580
|
from: entry.sender,
|
|
@@ -606,16 +655,7 @@ async function _dispatchPalzMessageInner(params) {
|
|
|
606
655
|
log(`${tag}: ${step6bCtx}`);
|
|
607
656
|
span?.addEvent(step6bCtx);
|
|
608
657
|
const showProcess = config.showProcess === true;
|
|
609
|
-
const
|
|
610
|
-
if (effectiveAgentId === "main") {
|
|
611
|
-
groupSystemPromptParts.unshift(buildKnowledgeRecallSystemPrompt({
|
|
612
|
-
isGroup,
|
|
613
|
-
groupId,
|
|
614
|
-
ownerId: typeof msg.owner_id === "string" ? msg.owner_id : undefined,
|
|
615
|
-
groupOwnerId: typeof msg.group_owner_id === "string" ? msg.group_owner_id : undefined,
|
|
616
|
-
}));
|
|
617
|
-
}
|
|
618
|
-
const groupSystemPrompt = groupSystemPromptParts.join("\n\n");
|
|
658
|
+
const groupSystemPrompt = buildFileSharingSystemPrompt();
|
|
619
659
|
const ctx = core.channel.reply.finalizeInboundContext({
|
|
620
660
|
Body: combinedBody,
|
|
621
661
|
BodyForAgent: messageBody,
|
|
@@ -872,3 +912,51 @@ export function cleanupAgentCaches(agentIdToClean) {
|
|
|
872
912
|
}
|
|
873
913
|
reasoningScannedAgents.delete(agentIdToClean);
|
|
874
914
|
}
|
|
915
|
+
export async function handleKbTaskCallback(params) {
|
|
916
|
+
const log = params.log ?? (typeof params.runtime?.log === "function" ? params.runtime.log : console.log);
|
|
917
|
+
const error = params.error ?? (typeof params.runtime?.error === "function" ? params.runtime.error : console.error);
|
|
918
|
+
const taskId = extractKbTaskId(params.body);
|
|
919
|
+
if (!taskId) {
|
|
920
|
+
return { handled: false, reason: "missing task_id" };
|
|
921
|
+
}
|
|
922
|
+
const taskContext = resolveKbTaskContext(taskId);
|
|
923
|
+
if (!taskContext) {
|
|
924
|
+
log(`palz-kb-callback: task_id=${taskId} ignored, context not found`);
|
|
925
|
+
return { handled: false, taskId, reason: "task context not found" };
|
|
926
|
+
}
|
|
927
|
+
const content = extractKbTaskCallbackContent(params.body);
|
|
928
|
+
if (!content) {
|
|
929
|
+
log(`palz-kb-callback: task_id=${taskId} ignored, callback content missing`);
|
|
930
|
+
return { handled: false, taskId, reason: "missing callback content" };
|
|
931
|
+
}
|
|
932
|
+
const original = taskContext.msg;
|
|
933
|
+
const callbackMsg = {
|
|
934
|
+
...original,
|
|
935
|
+
event: "message",
|
|
936
|
+
content,
|
|
937
|
+
msg_id: `kb_callback_${taskId}_${Date.now()}`,
|
|
938
|
+
timestamp: Date.now(),
|
|
939
|
+
stream: false,
|
|
940
|
+
mentioned_bot: true,
|
|
941
|
+
agent_id: taskContext.agentId,
|
|
942
|
+
sender_id: original.sender_id || "kb-engine",
|
|
943
|
+
sender_name: "Knowledge Base Engine",
|
|
944
|
+
};
|
|
945
|
+
log(`palz-kb-callback: dispatching task_id=${taskId} agent=${taskContext.agentId} conv=${callbackMsg.conversation_id}`);
|
|
946
|
+
try {
|
|
947
|
+
consumeKbTaskContext(taskId);
|
|
948
|
+
await handlePalzMessage({
|
|
949
|
+
cfg: params.cfg,
|
|
950
|
+
runtime: params.runtime,
|
|
951
|
+
msg: callbackMsg,
|
|
952
|
+
accountId: taskContext.accountId,
|
|
953
|
+
agentId: taskContext.agentId,
|
|
954
|
+
suppressAck: true,
|
|
955
|
+
});
|
|
956
|
+
return { handled: true, taskId };
|
|
957
|
+
}
|
|
958
|
+
catch (err) {
|
|
959
|
+
error(`palz-kb-callback: dispatch failed task_id=${taskId}: ${String(err)}`);
|
|
960
|
+
throw err;
|
|
961
|
+
}
|
|
962
|
+
}
|
package/src/channel.js
CHANGED
|
@@ -108,7 +108,8 @@ export const palzPlugin = {
|
|
|
108
108
|
log: logInfo,
|
|
109
109
|
error,
|
|
110
110
|
});
|
|
111
|
-
//
|
|
111
|
+
// Stop service: disconnect all WS connections and prevent scanner-triggered reconnects.
|
|
112
|
+
// Idempotent because it may be called from the HTTP control port or framework abort.
|
|
112
113
|
let stopped = false;
|
|
113
114
|
const doStop = () => {
|
|
114
115
|
if (stopped)
|
|
@@ -118,8 +119,8 @@ export const palzPlugin = {
|
|
|
118
119
|
scanner.stop();
|
|
119
120
|
manager.shutdown();
|
|
120
121
|
};
|
|
121
|
-
//
|
|
122
|
-
//
|
|
122
|
+
// Optional HTTP control service. /admin/stop keeps this server alive so callers can
|
|
123
|
+
// inspect /admin/health afterwards; framework abort closes it.
|
|
123
124
|
let controlServer = null;
|
|
124
125
|
const controlPort = baseConfig.controlPort;
|
|
125
126
|
if (typeof controlPort === "number" && controlPort > 0) {
|
|
@@ -130,6 +131,8 @@ export const palzPlugin = {
|
|
|
130
131
|
const activeUsers = manager.getActiveUsers();
|
|
131
132
|
return { stopped, activeConnections: activeUsers.length, activeUsers };
|
|
132
133
|
},
|
|
134
|
+
cfg: ctx.cfg,
|
|
135
|
+
runtime: ctx.runtime,
|
|
133
136
|
log: logInfo,
|
|
134
137
|
error,
|
|
135
138
|
});
|
package/src/config.js
CHANGED
|
@@ -67,6 +67,7 @@ export function resolvePalzConfig(_cfg, botId) {
|
|
|
67
67
|
sessionTimeout: file.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT,
|
|
68
68
|
groupContextCache: file.groupContextCache !== false,
|
|
69
69
|
showProcess: file.showProcess === true,
|
|
70
|
+
kbEngineUrl: file.kbEngineUrl || "",
|
|
70
71
|
controlPort: file.controlPort,
|
|
71
72
|
};
|
|
72
73
|
if (!_configLoggedOnce) {
|
package/src/control-server.js
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Palz Connector HTTP
|
|
2
|
+
* Palz Connector HTTP control service.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - GET /admin/health 查询当前状态(是否已停止 / 活跃连接数)
|
|
7
|
-
*
|
|
8
|
-
* 与 WS 连接 / 消息处理 / reconcile 完全隔离:只持有 onStop / getStatus 回调,
|
|
9
|
-
* 不碰任何业务数据;只有配置了端口才启动;启动错误只记日志不影响插件主流程。
|
|
10
|
-
*
|
|
11
|
-
* 统一响应结构:{ code, err_message, data },code=0 表示成功,非 0 为错误码。
|
|
12
|
-
* HTTP status 一律 200,业务结果由 code 表达。
|
|
4
|
+
* Exposes an optional admin port for stopping all WS connections without
|
|
5
|
+
* affecting the normal channel/message code paths.
|
|
13
6
|
*/
|
|
14
7
|
import http from "node:http";
|
|
8
|
+
import { handleKbTaskCallback } from "./bot.js";
|
|
9
|
+
import { getKbTaskRegistryStatus } from "./kb-task-callback.js";
|
|
15
10
|
function sendJson(res, body) {
|
|
16
11
|
const payload = JSON.stringify(body);
|
|
17
12
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
18
13
|
res.end(payload);
|
|
19
14
|
}
|
|
15
|
+
async function readRequestBody(req) {
|
|
16
|
+
const chunks = [];
|
|
17
|
+
for await (const chunk of req) {
|
|
18
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
19
|
+
}
|
|
20
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
21
|
+
}
|
|
22
|
+
function parseCallbackBody(req, raw) {
|
|
23
|
+
const contentType = String(req.headers["content-type"] || "").toLowerCase();
|
|
24
|
+
if (!raw.trim())
|
|
25
|
+
return {};
|
|
26
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
27
|
+
const form = new URLSearchParams(raw);
|
|
28
|
+
const payload = form.get("payload");
|
|
29
|
+
if (payload)
|
|
30
|
+
return JSON.parse(payload);
|
|
31
|
+
return Object.fromEntries(form.entries());
|
|
32
|
+
}
|
|
33
|
+
if (contentType.includes("multipart/form-data")) {
|
|
34
|
+
const match = raw.match(/name="payload"(?:\r?\n[^\r\n]*)?\r?\n\r?\n([\s\S]*?)\r?\n--/);
|
|
35
|
+
if (match?.[1])
|
|
36
|
+
return JSON.parse(match[1].trim());
|
|
37
|
+
}
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
}
|
|
20
40
|
export function startControlServer(opts) {
|
|
21
41
|
const log = opts.log ?? console.log;
|
|
22
42
|
const error = opts.error ?? console.error;
|
|
23
|
-
const server = http.createServer((req, res) => {
|
|
43
|
+
const server = http.createServer(async (req, res) => {
|
|
24
44
|
const method = (req.method || "GET").toUpperCase();
|
|
25
|
-
// 去掉查询串,只取 path
|
|
26
45
|
const path = (req.url || "/").split("?")[0].replace(/\/+$/, "") || "/";
|
|
27
46
|
try {
|
|
28
47
|
if (path === "/admin/stop") {
|
|
@@ -47,7 +66,36 @@ export function startControlServer(opts) {
|
|
|
47
66
|
return;
|
|
48
67
|
}
|
|
49
68
|
const status = opts.getStatus();
|
|
50
|
-
sendJson(res, {
|
|
69
|
+
sendJson(res, {
|
|
70
|
+
code: 0,
|
|
71
|
+
err_message: "",
|
|
72
|
+
data: { ...status, kbTasks: getKbTaskRegistryStatus() },
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (path === "/admin/kb-ingest/callback") {
|
|
77
|
+
if (method !== "POST") {
|
|
78
|
+
sendJson(res, { code: 405, err_message: "method not allowed", data: {} });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!opts.runtime) {
|
|
82
|
+
sendJson(res, { code: 503, err_message: "runtime not available", data: {} });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const rawBody = await readRequestBody(req);
|
|
86
|
+
const body = parseCallbackBody(req, rawBody);
|
|
87
|
+
const result = await handleKbTaskCallback({
|
|
88
|
+
cfg: opts.cfg,
|
|
89
|
+
runtime: opts.runtime,
|
|
90
|
+
body,
|
|
91
|
+
log,
|
|
92
|
+
error,
|
|
93
|
+
});
|
|
94
|
+
sendJson(res, {
|
|
95
|
+
code: result.handled ? 0 : 404,
|
|
96
|
+
err_message: result.handled ? "" : (result.reason ?? "not handled"),
|
|
97
|
+
data: result,
|
|
98
|
+
});
|
|
51
99
|
return;
|
|
52
100
|
}
|
|
53
101
|
sendJson(res, { code: 404, err_message: "not found", data: {} });
|
|
@@ -60,7 +108,6 @@ export function startControlServer(opts) {
|
|
|
60
108
|
catch { }
|
|
61
109
|
}
|
|
62
110
|
});
|
|
63
|
-
// 端口占用等错误只记日志,不让插件崩溃。
|
|
64
111
|
server.on("error", (err) => {
|
|
65
112
|
error(`control-server: server error on port ${opts.port}: ${err?.message ?? err}`);
|
|
66
113
|
});
|
package/src/kb-ingest.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Palz Connector → uniclaw-kb-engine 知识库入库
|
|
3
|
+
*
|
|
4
|
+
* 当 IM 消息携带文件时,调用 engine 的 ingest API
|
|
5
|
+
* (POST /uniclaw-kb-engine/api/v1/ingest-tasks),把原始 file_url 直接交给 engine 入库。
|
|
6
|
+
* engine 自行做文件类型路由 / 去重 / 跳过,本模块不过滤文件类型。
|
|
7
|
+
*
|
|
8
|
+
* 本模块的所有调用都不阻塞、不抛出,失败只打日志,绝不影响 AI 对话流程。
|
|
9
|
+
*/
|
|
10
|
+
import { registerKbTaskContext } from "./kb-task-callback.js";
|
|
11
|
+
/** 调用 engine ingest API 的超时时间(毫秒)。
|
|
12
|
+
* ingest 为异步受理(暂存+入队即返回),但走 file_url 时 engine 需从 URL
|
|
13
|
+
* 拉取文件再上传到自己的 OSS;超时只记录日志并继续后续 AI 流程。 */
|
|
14
|
+
const INGEST_TIMEOUT_MS = 3000;
|
|
15
|
+
const KB_ENGINE_API_PREFIX = "/uniclaw-kb-engine/api/v1";
|
|
16
|
+
const LEGACY_KB_ENGINE_API_PREFIX = "/api/v1";
|
|
17
|
+
const KB_ENGINE_INGEST_ENDPOINT = "ingest-tasks";
|
|
18
|
+
/**
|
|
19
|
+
* 从消息 content 中提取所有 type:"file" 的 file_url。
|
|
20
|
+
* 取值逻辑与 media.ts 的 resolvePalzMediaList 保持一致。
|
|
21
|
+
*/
|
|
22
|
+
export function extractFileUrls(content) {
|
|
23
|
+
if (typeof content === "string" || !Array.isArray(content))
|
|
24
|
+
return [];
|
|
25
|
+
const urls = [];
|
|
26
|
+
for (const part of content) {
|
|
27
|
+
if (part.type === "file") {
|
|
28
|
+
const filePart = part;
|
|
29
|
+
const url = filePart.file_url?.url;
|
|
30
|
+
if (url)
|
|
31
|
+
urls.push(url);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return urls;
|
|
35
|
+
}
|
|
36
|
+
function resolveKbEngineIngestUrl(kbEngineUrl) {
|
|
37
|
+
const normalizedBase = kbEngineUrl.trim().replace(/\/+$/, "");
|
|
38
|
+
if (normalizedBase.endsWith(KB_ENGINE_API_PREFIX)) {
|
|
39
|
+
return `${normalizedBase}/${KB_ENGINE_INGEST_ENDPOINT}`;
|
|
40
|
+
}
|
|
41
|
+
if (normalizedBase.endsWith(LEGACY_KB_ENGINE_API_PREFIX)) {
|
|
42
|
+
const hostBase = normalizedBase.slice(0, -LEGACY_KB_ENGINE_API_PREFIX.length).replace(/\/+$/, "");
|
|
43
|
+
return `${hostBase}${KB_ENGINE_API_PREFIX}/${KB_ENGINE_INGEST_ENDPOINT}`;
|
|
44
|
+
}
|
|
45
|
+
return `${normalizedBase}${KB_ENGINE_API_PREFIX}/${KB_ENGINE_INGEST_ENDPOINT}`;
|
|
46
|
+
}
|
|
47
|
+
/** 构造单个 file_url 的 ingest payload;缺少 engine 必填字段时返回 null(跳过)。 */
|
|
48
|
+
function buildPayload(msg, fileUrl, fileMd5, callbackUrl, log) {
|
|
49
|
+
const ownerId = (msg.owner_id || "").trim();
|
|
50
|
+
const conversationId = (msg.conversation_id || "").trim();
|
|
51
|
+
const senderId = (msg.sender_id || "").trim();
|
|
52
|
+
const lobsterId = (msg.lobster_id || "").trim();
|
|
53
|
+
const msgId = (msg.msg_id || "").trim();
|
|
54
|
+
const lobsterType = (msg.lobster_type || "").trim();
|
|
55
|
+
const missing = [];
|
|
56
|
+
if (!ownerId)
|
|
57
|
+
missing.push("owner_id");
|
|
58
|
+
if (!conversationId)
|
|
59
|
+
missing.push("conversation_id");
|
|
60
|
+
if (!senderId)
|
|
61
|
+
missing.push("sender_id");
|
|
62
|
+
if (!lobsterId)
|
|
63
|
+
missing.push("lobster_id");
|
|
64
|
+
if (!msgId)
|
|
65
|
+
missing.push("msg_id");
|
|
66
|
+
if (!lobsterType)
|
|
67
|
+
missing.push("lobster_type");
|
|
68
|
+
if (missing.length > 0) {
|
|
69
|
+
log?.(`palz-kb-ingest: [skip] msg_id=${msgId || "(none)"} 缺少必填字段: ${missing.join(",")}`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const isGroup = (msg.conversation_type || "").toLowerCase() === "group";
|
|
73
|
+
const payload = {
|
|
74
|
+
owner_id: ownerId,
|
|
75
|
+
conversation_id: conversationId,
|
|
76
|
+
sender_id: senderId,
|
|
77
|
+
lobster_id: lobsterId,
|
|
78
|
+
msg_id: msgId,
|
|
79
|
+
lobster_type: lobsterType,
|
|
80
|
+
conversation_type: (msg.conversation_type || "direct").trim(),
|
|
81
|
+
file_url: fileUrl,
|
|
82
|
+
};
|
|
83
|
+
if (fileMd5)
|
|
84
|
+
payload.file_md5 = fileMd5;
|
|
85
|
+
if (callbackUrl)
|
|
86
|
+
payload.callback_url = callbackUrl;
|
|
87
|
+
if (msg.sender_name)
|
|
88
|
+
payload.sender_name = msg.sender_name;
|
|
89
|
+
if (msg.sender_account_type)
|
|
90
|
+
payload.sender_account_type = msg.sender_account_type;
|
|
91
|
+
if (msg.lobster_name)
|
|
92
|
+
payload.lobster_name = msg.lobster_name;
|
|
93
|
+
if (msg.group_name)
|
|
94
|
+
payload.group_name = msg.group_name;
|
|
95
|
+
if (isGroup) {
|
|
96
|
+
const groupId = (msg.group_id || msg.conversation_id || "").trim();
|
|
97
|
+
const groupOwnerId = (msg.group_owner_id || "").trim();
|
|
98
|
+
const groupMembersList = (msg.group_members_list || "").trim();
|
|
99
|
+
const groupMissing = [];
|
|
100
|
+
if (!groupOwnerId)
|
|
101
|
+
groupMissing.push("group_owner_id");
|
|
102
|
+
if (!groupMembersList)
|
|
103
|
+
groupMissing.push("group_members_list");
|
|
104
|
+
if (groupMissing.length > 0) {
|
|
105
|
+
log?.(`palz-kb-ingest: [skip] msg_id=${msgId} 群聊缺少必填字段: ${groupMissing.join(",")}`);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
payload.group_id = groupId;
|
|
109
|
+
payload.group_owner_id = groupOwnerId;
|
|
110
|
+
payload.group_members_list = groupMembersList;
|
|
111
|
+
}
|
|
112
|
+
return payload;
|
|
113
|
+
}
|
|
114
|
+
/** 发送单个 ingest 请求,带 3s 超时;失败只打日志,绝不抛出。 */
|
|
115
|
+
async function postIngest(kbEngineUrl, payload, log) {
|
|
116
|
+
const baseResult = {
|
|
117
|
+
fileUrl: payload.file_url,
|
|
118
|
+
fileMd5: payload.file_md5,
|
|
119
|
+
submitted: false,
|
|
120
|
+
taskCreated: false,
|
|
121
|
+
};
|
|
122
|
+
const url = resolveKbEngineIngestUrl(kbEngineUrl);
|
|
123
|
+
const payloadJson = JSON.stringify(payload);
|
|
124
|
+
const form = new FormData();
|
|
125
|
+
form.append("payload", payloadJson);
|
|
126
|
+
// 完整请求体日志,便于排查字段映射 / engine 拒绝原因
|
|
127
|
+
log?.(`palz-kb-ingest: [request] POST ${url} payload=${payloadJson}`);
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const timer = setTimeout(() => controller.abort(), INGEST_TIMEOUT_MS);
|
|
130
|
+
try {
|
|
131
|
+
const resp = await fetch(url, { method: "POST", body: form, signal: controller.signal });
|
|
132
|
+
const rawBody = await resp.text();
|
|
133
|
+
// 完整响应体日志(含 HTTP 状态码与原始 body,无论 JSON 与否)
|
|
134
|
+
log?.(`palz-kb-ingest: [response] msg_id=${payload.msg_id} http=${resp.status} body=${rawBody}`);
|
|
135
|
+
let body = null;
|
|
136
|
+
try {
|
|
137
|
+
body = JSON.parse(rawBody);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* 非 JSON 响应,已在 [response] 中打出原始 body */
|
|
141
|
+
}
|
|
142
|
+
if (!resp.ok) {
|
|
143
|
+
log?.(`palz-kb-ingest: [warn] HTTP ${resp.status} msg_id=${payload.msg_id} url=${payload.file_url}`);
|
|
144
|
+
return { ...baseResult, error: `HTTP ${resp.status}` };
|
|
145
|
+
}
|
|
146
|
+
if (!body || typeof body.code !== "number") {
|
|
147
|
+
log?.(`palz-kb-ingest: [warn] engine 响应格式异常 msg_id=${payload.msg_id}`);
|
|
148
|
+
return { ...baseResult, error: "invalid engine response" };
|
|
149
|
+
}
|
|
150
|
+
const engineBody = body;
|
|
151
|
+
if (engineBody.code !== 0) {
|
|
152
|
+
const errMessage = engineBody.err_message || `engine code ${engineBody.code}`;
|
|
153
|
+
log?.(`palz-kb-ingest: [warn] engine 拒绝 msg_id=${payload.msg_id} code=${engineBody.code} err=${engineBody.err_message}`);
|
|
154
|
+
return { ...baseResult, error: errMessage };
|
|
155
|
+
}
|
|
156
|
+
const data = engineBody.data ?? {};
|
|
157
|
+
const taskId = data.task_id;
|
|
158
|
+
const state = data.state;
|
|
159
|
+
const taskCreated = data.task_created === true;
|
|
160
|
+
const dup = data.duplicate_submit === true ? " duplicate_submit=true" : "";
|
|
161
|
+
const skipped = data.skipped === true ? ` skipped=true reason=${data.reason ?? "(none)"}` : "";
|
|
162
|
+
log?.(`palz-kb-ingest: [ok] msg_id=${payload.msg_id} task_id=${taskId ?? "(none)"} state=${state ?? "(none)"} task_created=${taskCreated}${dup}${skipped}`);
|
|
163
|
+
return {
|
|
164
|
+
...baseResult,
|
|
165
|
+
submitted: true,
|
|
166
|
+
taskCreated,
|
|
167
|
+
taskId,
|
|
168
|
+
state,
|
|
169
|
+
kind: data.kind,
|
|
170
|
+
duplicateSubmit: data.duplicate_submit === true,
|
|
171
|
+
skipped: data.skipped === true,
|
|
172
|
+
skipReason: data.reason,
|
|
173
|
+
error: data.error,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
const reason = err?.name === "AbortError" ? `超时(${INGEST_TIMEOUT_MS}ms)` : err?.message;
|
|
178
|
+
log?.(`palz-kb-ingest: [error] msg_id=${payload.msg_id} url=${payload.file_url} error=${reason}`);
|
|
179
|
+
return {
|
|
180
|
+
...baseResult,
|
|
181
|
+
timedOut: err?.name === "AbortError",
|
|
182
|
+
error: reason || "unknown error",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 把消息中的文件送入 kb-engine 知识库。
|
|
191
|
+
* 内部吞掉所有错误,仅打日志并返回非创建结果,绝不抛出。
|
|
192
|
+
*/
|
|
193
|
+
export async function ingestFilesToKbEngine(params) {
|
|
194
|
+
const { kbEngineUrl, msg, fileUrls, fileMd5ByUrl, accountId, agentId, callbackUrl, log } = params;
|
|
195
|
+
if (!kbEngineUrl || fileUrls.length === 0)
|
|
196
|
+
return [];
|
|
197
|
+
log?.(`palz-kb-ingest: [start] msg_id=${msg.msg_id} files=${fileUrls.length} engine=${kbEngineUrl}`);
|
|
198
|
+
const results = [];
|
|
199
|
+
for (const fileUrl of fileUrls) {
|
|
200
|
+
const fileMd5 = fileMd5ByUrl?.get(fileUrl);
|
|
201
|
+
const payload = buildPayload(msg, fileUrl, fileMd5, callbackUrl, log);
|
|
202
|
+
if (!payload) {
|
|
203
|
+
results.push({
|
|
204
|
+
fileUrl,
|
|
205
|
+
fileMd5,
|
|
206
|
+
submitted: false,
|
|
207
|
+
taskCreated: false,
|
|
208
|
+
error: "payload skipped",
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const result = await postIngest(kbEngineUrl, payload, log);
|
|
213
|
+
results.push(result);
|
|
214
|
+
if (result.taskCreated && result.taskId && accountId && agentId) {
|
|
215
|
+
registerKbTaskContext({
|
|
216
|
+
taskId: result.taskId,
|
|
217
|
+
accountId,
|
|
218
|
+
agentId,
|
|
219
|
+
msg,
|
|
220
|
+
fileUrl,
|
|
221
|
+
fileMd5,
|
|
222
|
+
log,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return results;
|
|
227
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const DEFAULT_TASK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
2
|
+
const MAX_TASK_CONTEXTS = 5000;
|
|
3
|
+
const taskContexts = new Map();
|
|
4
|
+
function cleanupExpired(now = Date.now()) {
|
|
5
|
+
for (const [taskId, context] of taskContexts) {
|
|
6
|
+
if (context.expiresAt <= now)
|
|
7
|
+
taskContexts.delete(taskId);
|
|
8
|
+
}
|
|
9
|
+
while (taskContexts.size > MAX_TASK_CONTEXTS) {
|
|
10
|
+
const oldest = taskContexts.keys().next().value;
|
|
11
|
+
if (oldest === undefined)
|
|
12
|
+
break;
|
|
13
|
+
taskContexts.delete(oldest);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function registerKbTaskContext(params) {
|
|
17
|
+
const taskId = params.taskId.trim();
|
|
18
|
+
if (!taskId)
|
|
19
|
+
return;
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
cleanupExpired(now);
|
|
22
|
+
taskContexts.set(taskId, {
|
|
23
|
+
taskId,
|
|
24
|
+
accountId: params.accountId,
|
|
25
|
+
agentId: params.agentId,
|
|
26
|
+
msg: { ...params.msg },
|
|
27
|
+
fileUrl: params.fileUrl,
|
|
28
|
+
fileMd5: params.fileMd5,
|
|
29
|
+
createdAt: now,
|
|
30
|
+
expiresAt: now + (params.ttlMs ?? DEFAULT_TASK_TTL_MS),
|
|
31
|
+
});
|
|
32
|
+
params.log?.(`palz-kb-callback: registered task_id=${taskId} agent=${params.agentId} conv=${params.msg.conversation_id}`);
|
|
33
|
+
}
|
|
34
|
+
export function resolveKbTaskContext(taskId) {
|
|
35
|
+
cleanupExpired();
|
|
36
|
+
return taskContexts.get(taskId.trim());
|
|
37
|
+
}
|
|
38
|
+
export function consumeKbTaskContext(taskId) {
|
|
39
|
+
const context = resolveKbTaskContext(taskId);
|
|
40
|
+
if (context)
|
|
41
|
+
taskContexts.delete(context.taskId);
|
|
42
|
+
return context;
|
|
43
|
+
}
|
|
44
|
+
export function getKbTaskRegistryStatus() {
|
|
45
|
+
cleanupExpired();
|
|
46
|
+
return { trackedTasks: taskContexts.size };
|
|
47
|
+
}
|
|
48
|
+
export function extractKbTaskId(body) {
|
|
49
|
+
return typeof body.task_id === "string" ? body.task_id.trim() : "";
|
|
50
|
+
}
|
|
51
|
+
export function extractKbTaskCallbackContent(body) {
|
|
52
|
+
return typeof body.content === "string" ? body.content.trim() : "";
|
|
53
|
+
}
|
package/src/media.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import path from "path";
|
|
10
10
|
import os from "os";
|
|
11
|
+
import crypto from "crypto";
|
|
11
12
|
import { uploadFileToOss, uploadBufferToOss } from "./oss.js";
|
|
12
13
|
/** OpenClaw 允许访问的媒体目录 */
|
|
13
14
|
const MEDIA_DIR = path.join(os.homedir(), ".openclaw", "media");
|
|
@@ -62,7 +63,7 @@ function sanitizeFileName(name) {
|
|
|
62
63
|
const trimmedStem = cleaned.slice(0, MAX - ext.length);
|
|
63
64
|
return trimmedStem + ext;
|
|
64
65
|
}
|
|
65
|
-
function saveBufferToMediaDir(buffer, contentType, ext, log, originalName) {
|
|
66
|
+
function saveBufferToMediaDir(buffer, contentType, ext, log, originalName, sourceUrl) {
|
|
66
67
|
try {
|
|
67
68
|
fs.mkdirSync(MEDIA_DIR, { recursive: true });
|
|
68
69
|
let fileName;
|
|
@@ -76,8 +77,9 @@ function saveBufferToMediaDir(buffer, contentType, ext, log, originalName) {
|
|
|
76
77
|
const filePath = path.join(MEDIA_DIR, fileName);
|
|
77
78
|
fs.writeFileSync(filePath, buffer);
|
|
78
79
|
const placeholder = isImageMime(contentType) ? "<media:image>" : `<media:file:${ext}>`;
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
const md5 = crypto.createHash("md5").update(buffer).digest("hex");
|
|
81
|
+
log?.(`palz-media: [saveToMediaDir] 成功: path=${filePath} size=${buffer.length}bytes mime=${contentType} md5=${md5}${originalName ? ` originalName=${originalName}` : ""}`);
|
|
82
|
+
return { path: filePath, contentType, placeholder, sourceUrl, md5 };
|
|
81
83
|
}
|
|
82
84
|
catch (err) {
|
|
83
85
|
log?.(`palz-media: [saveToMediaDir] 失败: error=${err.message}`);
|
|
@@ -224,7 +226,7 @@ export async function resolvePalzMediaList(content, log) {
|
|
|
224
226
|
log?.(`palz-media: [resolve] 处理第 ${i + 1}/${mediaUrls.length} 个媒体, urlType=${urlType} urlLen=${url.length}`);
|
|
225
227
|
const fetched = await fetchUrlToBuffer(url, log);
|
|
226
228
|
if (fetched) {
|
|
227
|
-
const info = saveBufferToMediaDir(fetched.buffer, fetched.contentType, fetched.ext, log, fetched.originalName);
|
|
229
|
+
const info = saveBufferToMediaDir(fetched.buffer, fetched.contentType, fetched.ext, log, fetched.originalName, url);
|
|
228
230
|
if (info) {
|
|
229
231
|
results.push(info);
|
|
230
232
|
log?.(`palz-media: [resolve] 第 ${i + 1} 完成: ${JSON.stringify(info)}`);
|
package/src/monitor.js
CHANGED
|
@@ -167,14 +167,6 @@ export async function monitorPalzProvider(params) {
|
|
|
167
167
|
lastPongAt = connectedAt;
|
|
168
168
|
consecutive4002 = 0;
|
|
169
169
|
log(`palz[${accountId}]: WebSocket connected, bot_id=${config.botId}`);
|
|
170
|
-
reportPalzActivity({
|
|
171
|
-
config,
|
|
172
|
-
connectedAt: new Date(connectedAt),
|
|
173
|
-
runtime,
|
|
174
|
-
accountId,
|
|
175
|
-
}).catch((err) => {
|
|
176
|
-
error(`palz[${accountId}]: [ACTIVITY_REPORT] unhandled error: ${err.message ?? err}`);
|
|
177
|
-
});
|
|
178
170
|
pingInterval = setInterval(() => {
|
|
179
171
|
if (ws.readyState === WebSocket.OPEN) {
|
|
180
172
|
const timeSinceLastPong = Date.now() - lastPongAt;
|
|
@@ -203,6 +195,7 @@ export async function monitorPalzProvider(params) {
|
|
|
203
195
|
}
|
|
204
196
|
});
|
|
205
197
|
ws.on("message", (data) => {
|
|
198
|
+
const receivedAt = new Date();
|
|
206
199
|
const raw = data.toString();
|
|
207
200
|
try {
|
|
208
201
|
const msg = JSON.parse(raw);
|
|
@@ -215,7 +208,16 @@ export async function monitorPalzProvider(params) {
|
|
|
215
208
|
tracer.startActiveSpan("palz.ws_recv", {}, parentCtx, (span) => {
|
|
216
209
|
try {
|
|
217
210
|
span.setAttribute("raw_message", raw);
|
|
218
|
-
log(`palz[${accountId}]: [WS_RECV] #${messageCount} full_message=${raw
|
|
211
|
+
log(`palz[${accountId}]: [WS_RECV] #${messageCount} full_message=${raw} traceId=${span.spanContext().traceId}`);
|
|
212
|
+
reportPalzActivity({
|
|
213
|
+
config,
|
|
214
|
+
msg,
|
|
215
|
+
receivedAt,
|
|
216
|
+
runtime,
|
|
217
|
+
accountId,
|
|
218
|
+
}).catch((err) => {
|
|
219
|
+
error(`palz[${accountId}]: [ACTIVITY_REPORT] unhandled error: ${err.message ?? err}`);
|
|
220
|
+
});
|
|
219
221
|
// agent_id:从消息中取,缺失/空时默认为 {userId}-main
|
|
220
222
|
let messageAgentId = typeof msg.agent_id === "string" ? msg.agent_id.trim() : "";
|
|
221
223
|
if (!messageAgentId) {
|