@botcord/daemon 0.2.35 → 0.2.37

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.
Files changed (68) hide show
  1. package/dist/config.d.ts +30 -1
  2. package/dist/config.js +27 -0
  3. package/dist/daemon-config-map.d.ts +3 -0
  4. package/dist/daemon-config-map.js +30 -0
  5. package/dist/daemon.d.ts +15 -1
  6. package/dist/daemon.js +56 -11
  7. package/dist/gateway/channels/botcord.js +44 -0
  8. package/dist/gateway/channels/http-types.d.ts +19 -0
  9. package/dist/gateway/channels/http-types.js +1 -0
  10. package/dist/gateway/channels/index.d.ts +5 -0
  11. package/dist/gateway/channels/index.js +5 -0
  12. package/dist/gateway/channels/login-session.d.ts +83 -0
  13. package/dist/gateway/channels/login-session.js +99 -0
  14. package/dist/gateway/channels/secret-store.d.ts +21 -0
  15. package/dist/gateway/channels/secret-store.js +75 -0
  16. package/dist/gateway/channels/state-store.d.ts +60 -0
  17. package/dist/gateway/channels/state-store.js +173 -0
  18. package/dist/gateway/channels/telegram.d.ts +31 -0
  19. package/dist/gateway/channels/telegram.js +371 -0
  20. package/dist/gateway/channels/text-split.d.ts +13 -0
  21. package/dist/gateway/channels/text-split.js +33 -0
  22. package/dist/gateway/channels/url-guard.d.ts +18 -0
  23. package/dist/gateway/channels/url-guard.js +53 -0
  24. package/dist/gateway/channels/wechat-http.d.ts +18 -0
  25. package/dist/gateway/channels/wechat-http.js +28 -0
  26. package/dist/gateway/channels/wechat-login.d.ts +36 -0
  27. package/dist/gateway/channels/wechat-login.js +62 -0
  28. package/dist/gateway/channels/wechat.d.ts +40 -0
  29. package/dist/gateway/channels/wechat.js +472 -0
  30. package/dist/gateway/runtimes/openclaw-acp.js +211 -6
  31. package/dist/gateway/types.d.ts +10 -0
  32. package/dist/gateway-control.d.ts +53 -0
  33. package/dist/gateway-control.js +638 -0
  34. package/dist/openclaw-discovery.js +1 -1
  35. package/dist/provision.d.ts +7 -0
  36. package/dist/provision.js +255 -5
  37. package/package.json +1 -1
  38. package/src/__tests__/gateway-control.test.ts +499 -0
  39. package/src/__tests__/openclaw-acp.test.ts +63 -0
  40. package/src/__tests__/openclaw-discovery.test.ts +36 -0
  41. package/src/__tests__/provision.test.ts +179 -0
  42. package/src/__tests__/secret-store.test.ts +70 -0
  43. package/src/__tests__/state-store.test.ts +119 -0
  44. package/src/__tests__/third-party-gateway.test.ts +126 -0
  45. package/src/__tests__/url-guard.test.ts +85 -0
  46. package/src/__tests__/wechat-channel.test.ts +1134 -0
  47. package/src/config.ts +72 -1
  48. package/src/daemon-config-map.ts +24 -0
  49. package/src/daemon.ts +70 -11
  50. package/src/gateway/__tests__/botcord-channel.test.ts +1 -1
  51. package/src/gateway/__tests__/telegram-channel.test.ts +555 -0
  52. package/src/gateway/channels/botcord.ts +39 -0
  53. package/src/gateway/channels/http-types.ts +22 -0
  54. package/src/gateway/channels/index.ts +22 -0
  55. package/src/gateway/channels/login-session.ts +135 -0
  56. package/src/gateway/channels/secret-store.ts +100 -0
  57. package/src/gateway/channels/state-store.ts +213 -0
  58. package/src/gateway/channels/telegram.ts +469 -0
  59. package/src/gateway/channels/text-split.ts +29 -0
  60. package/src/gateway/channels/url-guard.ts +55 -0
  61. package/src/gateway/channels/wechat-http.ts +35 -0
  62. package/src/gateway/channels/wechat-login.ts +90 -0
  63. package/src/gateway/channels/wechat.ts +572 -0
  64. package/src/gateway/runtimes/openclaw-acp.ts +211 -7
  65. package/src/gateway/types.ts +10 -0
  66. package/src/gateway-control.ts +709 -0
  67. package/src/openclaw-discovery.ts +1 -1
  68. 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
- finalText = pickFinalText(promptResult) ?? assistantText;
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;
@@ -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
+ };