@botcord/daemon 0.2.36 → 0.2.38
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/config.d.ts +29 -0
- package/dist/config.js +27 -0
- package/dist/daemon-config-map.d.ts +3 -0
- package/dist/daemon-config-map.js +30 -0
- package/dist/daemon.d.ts +15 -1
- package/dist/daemon.js +56 -11
- package/dist/gateway/channels/botcord.js +44 -0
- package/dist/gateway/channels/http-types.d.ts +19 -0
- package/dist/gateway/channels/http-types.js +1 -0
- package/dist/gateway/channels/index.d.ts +5 -0
- package/dist/gateway/channels/index.js +5 -0
- package/dist/gateway/channels/login-session.d.ts +83 -0
- package/dist/gateway/channels/login-session.js +99 -0
- package/dist/gateway/channels/secret-store.d.ts +21 -0
- package/dist/gateway/channels/secret-store.js +75 -0
- package/dist/gateway/channels/state-store.d.ts +60 -0
- package/dist/gateway/channels/state-store.js +173 -0
- package/dist/gateway/channels/telegram.d.ts +31 -0
- package/dist/gateway/channels/telegram.js +371 -0
- package/dist/gateway/channels/text-split.d.ts +13 -0
- package/dist/gateway/channels/text-split.js +33 -0
- package/dist/gateway/channels/url-guard.d.ts +18 -0
- package/dist/gateway/channels/url-guard.js +53 -0
- package/dist/gateway/channels/wechat-http.d.ts +18 -0
- package/dist/gateway/channels/wechat-http.js +28 -0
- package/dist/gateway/channels/wechat-login.d.ts +36 -0
- package/dist/gateway/channels/wechat-login.js +62 -0
- package/dist/gateway/channels/wechat.d.ts +40 -0
- package/dist/gateway/channels/wechat.js +472 -0
- package/dist/gateway/runtimes/openclaw-acp.js +211 -6
- package/dist/gateway/types.d.ts +10 -0
- package/dist/gateway-control.d.ts +53 -0
- package/dist/gateway-control.js +638 -0
- package/dist/provision.d.ts +7 -0
- package/dist/provision.js +255 -5
- package/package.json +1 -1
- package/src/__tests__/gateway-control.test.ts +499 -0
- package/src/__tests__/openclaw-acp.test.ts +63 -0
- package/src/__tests__/provision.test.ts +179 -0
- package/src/__tests__/secret-store.test.ts +70 -0
- package/src/__tests__/state-store.test.ts +119 -0
- package/src/__tests__/third-party-gateway.test.ts +126 -0
- package/src/__tests__/url-guard.test.ts +85 -0
- package/src/__tests__/wechat-channel.test.ts +1134 -0
- package/src/config.ts +71 -0
- package/src/daemon-config-map.ts +24 -0
- package/src/daemon.ts +70 -11
- package/src/gateway/__tests__/botcord-channel.test.ts +1 -1
- package/src/gateway/__tests__/telegram-channel.test.ts +555 -0
- package/src/gateway/channels/botcord.ts +39 -0
- package/src/gateway/channels/http-types.ts +22 -0
- package/src/gateway/channels/index.ts +22 -0
- package/src/gateway/channels/login-session.ts +135 -0
- package/src/gateway/channels/secret-store.ts +100 -0
- package/src/gateway/channels/state-store.ts +213 -0
- package/src/gateway/channels/telegram.ts +469 -0
- package/src/gateway/channels/text-split.ts +29 -0
- package/src/gateway/channels/url-guard.ts +55 -0
- package/src/gateway/channels/wechat-http.ts +35 -0
- package/src/gateway/channels/wechat-login.ts +90 -0
- package/src/gateway/channels/wechat.ts +572 -0
- package/src/gateway/runtimes/openclaw-acp.ts +211 -7
- package/src/gateway/types.ts +10 -0
- package/src/gateway-control.ts +709 -0
- package/src/provision.ts +336 -5
|
@@ -127,6 +127,7 @@ export class OpenclawAcpAdapter {
|
|
|
127
127
|
let assistantBytes = 0;
|
|
128
128
|
let capped = false;
|
|
129
129
|
let finalText = "";
|
|
130
|
+
const assistantTextFilter = createAssistantTextFilter();
|
|
130
131
|
const emitBlock = (block) => {
|
|
131
132
|
try {
|
|
132
133
|
opts.onBlock?.(block);
|
|
@@ -138,13 +139,9 @@ export class OpenclawAcpAdapter {
|
|
|
138
139
|
}
|
|
139
140
|
};
|
|
140
141
|
const onNotification = (note) => {
|
|
141
|
-
seq += 1;
|
|
142
|
-
// Forward raw notification as a stream block for downstream visibility.
|
|
143
|
-
const kind = classifyAcpUpdate(note);
|
|
144
|
-
emitBlock({ raw: note, kind, seq });
|
|
145
142
|
const update = note.params?.update;
|
|
146
143
|
if (update?.sessionUpdate === "agent_message_chunk") {
|
|
147
|
-
const text = extractText(update.content);
|
|
144
|
+
const text = assistantTextFilter.push(extractText(update.content));
|
|
148
145
|
if (text && !capped) {
|
|
149
146
|
const bytes = Buffer.byteLength(text, "utf8");
|
|
150
147
|
if (assistantBytes + bytes > ASSISTANT_TEXT_CAP) {
|
|
@@ -155,7 +152,16 @@ export class OpenclawAcpAdapter {
|
|
|
155
152
|
assistantBytes += bytes;
|
|
156
153
|
}
|
|
157
154
|
}
|
|
155
|
+
if (!text)
|
|
156
|
+
return;
|
|
157
|
+
seq += 1;
|
|
158
|
+
emitBlock({ raw: sanitizeAssistantChunk(note, text), kind: "assistant_text", seq });
|
|
159
|
+
return;
|
|
158
160
|
}
|
|
161
|
+
seq += 1;
|
|
162
|
+
// Forward raw non-assistant notifications as stream blocks for downstream visibility.
|
|
163
|
+
const kind = classifyAcpUpdate(note);
|
|
164
|
+
emitBlock({ raw: note, kind, seq });
|
|
159
165
|
};
|
|
160
166
|
let abortListener;
|
|
161
167
|
try {
|
|
@@ -248,7 +254,29 @@ export class OpenclawAcpAdapter {
|
|
|
248
254
|
// OpenClaw's prompt response shape isn't strictly fixed; pull a final
|
|
249
255
|
// text out of common locations and otherwise fall back to the streamed
|
|
250
256
|
// chunks accumulated above.
|
|
251
|
-
|
|
257
|
+
const tailText = assistantTextFilter.flush();
|
|
258
|
+
if (tailText && !capped) {
|
|
259
|
+
const bytes = Buffer.byteLength(tailText, "utf8");
|
|
260
|
+
if (assistantBytes + bytes <= ASSISTANT_TEXT_CAP) {
|
|
261
|
+
assistantText += tailText;
|
|
262
|
+
assistantBytes += bytes;
|
|
263
|
+
seq += 1;
|
|
264
|
+
emitBlock({
|
|
265
|
+
raw: {
|
|
266
|
+
method: "session/update",
|
|
267
|
+
params: {
|
|
268
|
+
sessionId: acpSessionId,
|
|
269
|
+
update: { sessionUpdate: "agent_message_chunk", content: [{ type: "text", text: tailText }] },
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
kind: "assistant_text",
|
|
273
|
+
seq,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const pickedText = normalizeAssistantText(pickFinalText(promptResult));
|
|
278
|
+
const streamedText = normalizeAssistantText(assistantText);
|
|
279
|
+
finalText = pickedText && !looksLikeReasoningLeak(pickedText) ? pickedText : streamedText;
|
|
252
280
|
if (capped) {
|
|
253
281
|
log.warn("openclaw-acp.assistant-text-capped", { sessionId: acpSessionId });
|
|
254
282
|
}
|
|
@@ -529,6 +557,9 @@ function extractText(content) {
|
|
|
529
557
|
}
|
|
530
558
|
if (typeof content === "object") {
|
|
531
559
|
const c = content;
|
|
560
|
+
const type = typeof c.type === "string" ? c.type.toLowerCase() : "";
|
|
561
|
+
if (type === "thinking" || type === "reasoning" || type === "thought")
|
|
562
|
+
return "";
|
|
532
563
|
if (typeof c.text === "string")
|
|
533
564
|
return c.text;
|
|
534
565
|
if (typeof c.content === "string")
|
|
@@ -538,16 +569,190 @@ function extractText(content) {
|
|
|
538
569
|
}
|
|
539
570
|
return "";
|
|
540
571
|
}
|
|
572
|
+
function sanitizeAssistantChunk(note, text) {
|
|
573
|
+
return {
|
|
574
|
+
...note,
|
|
575
|
+
params: {
|
|
576
|
+
...note.params,
|
|
577
|
+
update: {
|
|
578
|
+
...note.params?.update,
|
|
579
|
+
content: [{ type: "text", text }],
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function normalizeAssistantText(text) {
|
|
585
|
+
if (!text)
|
|
586
|
+
return "";
|
|
587
|
+
const finalMatch = text.match(/<final>([\s\S]*?)<\/final>/i);
|
|
588
|
+
const selected = finalMatch ? finalMatch[1] : text;
|
|
589
|
+
if (!finalMatch && selected.trimStart().toLowerCase().startsWith("<think")) {
|
|
590
|
+
return "";
|
|
591
|
+
}
|
|
592
|
+
return selected
|
|
593
|
+
.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, "")
|
|
594
|
+
.replace(/<\/?final>/gi, "")
|
|
595
|
+
.trim();
|
|
596
|
+
}
|
|
597
|
+
function createAssistantTextFilter() {
|
|
598
|
+
let pending = "";
|
|
599
|
+
let inThink = false;
|
|
600
|
+
let inFinal = false;
|
|
601
|
+
let seenFinal = false;
|
|
602
|
+
let fallback = "";
|
|
603
|
+
const consume = (flush) => {
|
|
604
|
+
let out = "";
|
|
605
|
+
while (pending.length > 0) {
|
|
606
|
+
if (inThink) {
|
|
607
|
+
const close = pending.search(/<\/think>/i);
|
|
608
|
+
if (close === -1) {
|
|
609
|
+
if (flush)
|
|
610
|
+
pending = "";
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
613
|
+
pending = pending.slice(close).replace(/^<\/think>/i, "");
|
|
614
|
+
inThink = false;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (inFinal) {
|
|
618
|
+
const close = pending.search(/<\/final>/i);
|
|
619
|
+
if (close === -1) {
|
|
620
|
+
out += pending;
|
|
621
|
+
pending = "";
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
out += pending.slice(0, close);
|
|
625
|
+
pending = pending.slice(close).replace(/^<\/final>/i, "");
|
|
626
|
+
inFinal = false;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const lt = pending.indexOf("<");
|
|
630
|
+
if (lt === -1) {
|
|
631
|
+
if (seenFinal) {
|
|
632
|
+
out += pending;
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
fallback += pending;
|
|
636
|
+
}
|
|
637
|
+
pending = "";
|
|
638
|
+
return out;
|
|
639
|
+
}
|
|
640
|
+
if (lt > 0) {
|
|
641
|
+
if (seenFinal) {
|
|
642
|
+
out += pending.slice(0, lt);
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
fallback += pending.slice(0, lt);
|
|
646
|
+
}
|
|
647
|
+
pending = pending.slice(lt);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
const lower = pending.toLowerCase();
|
|
651
|
+
if (lower.startsWith("<think")) {
|
|
652
|
+
const end = pending.indexOf(">");
|
|
653
|
+
if (end === -1) {
|
|
654
|
+
if (flush)
|
|
655
|
+
pending = "";
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
pending = pending.slice(end + 1);
|
|
659
|
+
inThink = true;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (lower.startsWith("</think")) {
|
|
663
|
+
const end = pending.indexOf(">");
|
|
664
|
+
if (end === -1) {
|
|
665
|
+
if (flush)
|
|
666
|
+
pending = "";
|
|
667
|
+
return out;
|
|
668
|
+
}
|
|
669
|
+
pending = pending.slice(end + 1);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (lower.startsWith("<final")) {
|
|
673
|
+
const end = pending.indexOf(">");
|
|
674
|
+
if (end === -1) {
|
|
675
|
+
if (flush)
|
|
676
|
+
pending = "";
|
|
677
|
+
return out;
|
|
678
|
+
}
|
|
679
|
+
pending = pending.slice(end + 1);
|
|
680
|
+
seenFinal = true;
|
|
681
|
+
fallback = "";
|
|
682
|
+
inFinal = true;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (lower.startsWith("</final")) {
|
|
686
|
+
const end = pending.indexOf(">");
|
|
687
|
+
if (end === -1) {
|
|
688
|
+
if (flush)
|
|
689
|
+
pending = "";
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
692
|
+
pending = pending.slice(end + 1);
|
|
693
|
+
inFinal = false;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const knownPrefixes = ["<think", "</think", "<final", "</final"];
|
|
697
|
+
if (!flush && knownPrefixes.some((prefix) => prefix.startsWith(lower))) {
|
|
698
|
+
return out;
|
|
699
|
+
}
|
|
700
|
+
out += "<";
|
|
701
|
+
pending = pending.slice(1);
|
|
702
|
+
}
|
|
703
|
+
if (flush && !seenFinal && fallback) {
|
|
704
|
+
const text = normalizeAssistantText(fallback);
|
|
705
|
+
fallback = "";
|
|
706
|
+
if (!looksLikeReasoningLeak(text))
|
|
707
|
+
return text;
|
|
708
|
+
}
|
|
709
|
+
return out;
|
|
710
|
+
};
|
|
711
|
+
return {
|
|
712
|
+
push(text) {
|
|
713
|
+
if (!text)
|
|
714
|
+
return "";
|
|
715
|
+
pending += text;
|
|
716
|
+
return consume(false);
|
|
717
|
+
},
|
|
718
|
+
flush() {
|
|
719
|
+
return consume(true);
|
|
720
|
+
},
|
|
721
|
+
};
|
|
722
|
+
}
|
|
541
723
|
function pickFinalText(result) {
|
|
542
724
|
if (!result || typeof result !== "object")
|
|
543
725
|
return undefined;
|
|
544
726
|
const r = result;
|
|
727
|
+
if (Array.isArray(r.assistantTexts)) {
|
|
728
|
+
const text = r.assistantTexts.filter((x) => typeof x === "string").join("\n");
|
|
729
|
+
if (text.length > 0)
|
|
730
|
+
return text;
|
|
731
|
+
}
|
|
732
|
+
const contentText = extractText(r.content);
|
|
733
|
+
if (contentText.length > 0)
|
|
734
|
+
return contentText;
|
|
735
|
+
const outputText = extractText(r.output);
|
|
736
|
+
if (outputText.length > 0)
|
|
737
|
+
return outputText;
|
|
738
|
+
const responseText = extractText(r.response);
|
|
739
|
+
if (responseText.length > 0)
|
|
740
|
+
return responseText;
|
|
545
741
|
if (typeof r.text === "string" && r.text.length > 0)
|
|
546
742
|
return r.text;
|
|
547
743
|
if (typeof r.message === "string" && r.message.length > 0)
|
|
548
744
|
return r.message;
|
|
549
745
|
return undefined;
|
|
550
746
|
}
|
|
747
|
+
function looksLikeReasoningLeak(text) {
|
|
748
|
+
const t = text.trim();
|
|
749
|
+
if (!t)
|
|
750
|
+
return false;
|
|
751
|
+
return (/^the user (said|asked|wants|is asking)\b/i.test(t) ||
|
|
752
|
+
/^i('|’)m .*\b(i('|’)ll|i will|need to|should|going to)\b/i.test(t) ||
|
|
753
|
+
/\bi('|’)ll respond\b/i.test(t) ||
|
|
754
|
+
/\bi need to\b/i.test(t));
|
|
755
|
+
}
|
|
551
756
|
function stringField(bag, key) {
|
|
552
757
|
if (!bag)
|
|
553
758
|
return undefined;
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -160,6 +160,16 @@ export interface ChannelStatusSnapshot {
|
|
|
160
160
|
lastStartAt?: number;
|
|
161
161
|
lastStopAt?: number;
|
|
162
162
|
lastError?: string | null;
|
|
163
|
+
/** Third-party provider id when this channel is not the built-in BotCord. */
|
|
164
|
+
provider?: "wechat" | "telegram";
|
|
165
|
+
/** Last time the adapter polled the upstream provider (ms epoch). */
|
|
166
|
+
lastPollAt?: number;
|
|
167
|
+
/** Last time the adapter accepted an inbound message (ms epoch). */
|
|
168
|
+
lastInboundAt?: number;
|
|
169
|
+
/** Last time the adapter successfully sent a reply (ms epoch). */
|
|
170
|
+
lastSendAt?: number;
|
|
171
|
+
/** Whether the adapter currently holds a usable provider credential. */
|
|
172
|
+
authorized?: boolean;
|
|
163
173
|
}
|
|
164
174
|
/** Per-turn status snapshot describing a currently-executing runtime invocation. */
|
|
165
175
|
export interface TurnStatusSnapshot {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-side handlers for the third-party gateway control frames defined
|
|
3
|
+
* in `packages/protocol-core/src/control-frame.ts`. Kept separate from
|
|
4
|
+
* `provision.ts` so the BotCord-agent provisioning logic and the third-
|
|
5
|
+
* party adapter management stay independently testable.
|
|
6
|
+
*
|
|
7
|
+
* All handlers take a {@link GatewayControlContext} so callers can swap the
|
|
8
|
+
* gateway, login-session store, fetch impl, and config I/O — `provision.ts`
|
|
9
|
+
* wires the production defaults.
|
|
10
|
+
*/
|
|
11
|
+
import type { ControlAck, GatewayLoginStartParams, GatewayLoginStatusParams, RemoveGatewayParams, TestGatewayParams, UpsertGatewayParams } from "@botcord/protocol-core";
|
|
12
|
+
import type { Gateway } from "./gateway/index.js";
|
|
13
|
+
import { type DaemonConfig } from "./config.js";
|
|
14
|
+
import { LoginSessionStore } from "./gateway/channels/login-session.js";
|
|
15
|
+
import { getBotQrcode, getQrcodeStatus } from "./gateway/channels/wechat-login.js";
|
|
16
|
+
import type { FetchLike } from "./gateway/channels/http-types.js";
|
|
17
|
+
type AckBody = Omit<ControlAck, "id">;
|
|
18
|
+
export type { FetchLike };
|
|
19
|
+
export interface GatewayControlContext {
|
|
20
|
+
gateway: Gateway;
|
|
21
|
+
/** Override `loadConfig`/`saveConfig`. Tests pass an in-memory pair. */
|
|
22
|
+
configIO?: {
|
|
23
|
+
load: () => DaemonConfig;
|
|
24
|
+
save: (cfg: DaemonConfig) => void;
|
|
25
|
+
};
|
|
26
|
+
/** Shared login-session store. Created lazily when not supplied. */
|
|
27
|
+
loginSessions?: LoginSessionStore;
|
|
28
|
+
/**
|
|
29
|
+
* Override the iLink HTTP client. Defaults to the helpers in
|
|
30
|
+
* `wechat-login.ts` (which themselves read `globalThis.fetch`).
|
|
31
|
+
*/
|
|
32
|
+
wechatLoginClient?: {
|
|
33
|
+
getBotQrcode: typeof getBotQrcode;
|
|
34
|
+
getQrcodeStatus: typeof getQrcodeStatus;
|
|
35
|
+
};
|
|
36
|
+
/** Override the global fetch — used by `test_gateway` for Telegram getMe. */
|
|
37
|
+
fetchImpl?: FetchLike;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build a closure carrying the production / test defaults. Returned object
|
|
41
|
+
* exposes one `handle*` method per frame type so `provision.ts` can route by
|
|
42
|
+
* `frame.type` without re-resolving dependencies on every dispatch.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createGatewayControl(ctx: GatewayControlContext): {
|
|
45
|
+
handleList: () => AckBody;
|
|
46
|
+
handleUpsert: (params: UpsertGatewayParams) => Promise<AckBody>;
|
|
47
|
+
handleRemove: (params: RemoveGatewayParams) => Promise<AckBody>;
|
|
48
|
+
handleTest: (params: TestGatewayParams) => Promise<AckBody>;
|
|
49
|
+
handleLoginStart: (params: GatewayLoginStartParams) => Promise<AckBody>;
|
|
50
|
+
handleLoginStatus: (params: GatewayLoginStatusParams) => Promise<AckBody>;
|
|
51
|
+
/** Exposed for tests — direct access to the in-memory session map. */
|
|
52
|
+
_sessions: LoginSessionStore;
|
|
53
|
+
};
|