@botcord/daemon 0.2.36 → 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 (65) hide show
  1. package/dist/config.d.ts +29 -0
  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/provision.d.ts +7 -0
  35. package/dist/provision.js +255 -5
  36. package/package.json +1 -1
  37. package/src/__tests__/gateway-control.test.ts +499 -0
  38. package/src/__tests__/openclaw-acp.test.ts +63 -0
  39. package/src/__tests__/provision.test.ts +179 -0
  40. package/src/__tests__/secret-store.test.ts +70 -0
  41. package/src/__tests__/state-store.test.ts +119 -0
  42. package/src/__tests__/third-party-gateway.test.ts +126 -0
  43. package/src/__tests__/url-guard.test.ts +85 -0
  44. package/src/__tests__/wechat-channel.test.ts +1134 -0
  45. package/src/config.ts +71 -0
  46. package/src/daemon-config-map.ts +24 -0
  47. package/src/daemon.ts +70 -11
  48. package/src/gateway/__tests__/botcord-channel.test.ts +1 -1
  49. package/src/gateway/__tests__/telegram-channel.test.ts +555 -0
  50. package/src/gateway/channels/botcord.ts +39 -0
  51. package/src/gateway/channels/http-types.ts +22 -0
  52. package/src/gateway/channels/index.ts +22 -0
  53. package/src/gateway/channels/login-session.ts +135 -0
  54. package/src/gateway/channels/secret-store.ts +100 -0
  55. package/src/gateway/channels/state-store.ts +213 -0
  56. package/src/gateway/channels/telegram.ts +469 -0
  57. package/src/gateway/channels/text-split.ts +29 -0
  58. package/src/gateway/channels/url-guard.ts +55 -0
  59. package/src/gateway/channels/wechat-http.ts +35 -0
  60. package/src/gateway/channels/wechat-login.ts +90 -0
  61. package/src/gateway/channels/wechat.ts +572 -0
  62. package/src/gateway/runtimes/openclaw-acp.ts +211 -7
  63. package/src/gateway/types.ts +10 -0
  64. package/src/gateway-control.ts +709 -0
  65. package/src/provision.ts +336 -5
@@ -200,6 +200,7 @@ export class OpenclawAcpAdapter implements RuntimeAdapter {
200
200
  let assistantBytes = 0;
201
201
  let capped = false;
202
202
  let finalText = "";
203
+ const assistantTextFilter = createAssistantTextFilter();
203
204
 
204
205
  const emitBlock = (block: StreamBlock): void => {
205
206
  try {
@@ -212,14 +213,9 @@ export class OpenclawAcpAdapter implements RuntimeAdapter {
212
213
  };
213
214
 
214
215
  const onNotification = (note: AcpNotification): void => {
215
- seq += 1;
216
- // Forward raw notification as a stream block for downstream visibility.
217
- const kind = classifyAcpUpdate(note);
218
- emitBlock({ raw: note, kind, seq });
219
-
220
216
  const update = note.params?.update;
221
217
  if (update?.sessionUpdate === "agent_message_chunk") {
222
- const text = extractText(update.content);
218
+ const text = assistantTextFilter.push(extractText(update.content));
223
219
  if (text && !capped) {
224
220
  const bytes = Buffer.byteLength(text, "utf8");
225
221
  if (assistantBytes + bytes > ASSISTANT_TEXT_CAP) {
@@ -229,7 +225,16 @@ export class OpenclawAcpAdapter implements RuntimeAdapter {
229
225
  assistantBytes += bytes;
230
226
  }
231
227
  }
228
+ if (!text) return;
229
+ seq += 1;
230
+ emitBlock({ raw: sanitizeAssistantChunk(note, text), kind: "assistant_text", seq });
231
+ return;
232
232
  }
233
+
234
+ seq += 1;
235
+ // Forward raw non-assistant notifications as stream blocks for downstream visibility.
236
+ const kind = classifyAcpUpdate(note);
237
+ emitBlock({ raw: note, kind, seq });
233
238
  };
234
239
 
235
240
  let abortListener: (() => void) | undefined;
@@ -322,7 +327,29 @@ export class OpenclawAcpAdapter implements RuntimeAdapter {
322
327
  // OpenClaw's prompt response shape isn't strictly fixed; pull a final
323
328
  // text out of common locations and otherwise fall back to the streamed
324
329
  // chunks accumulated above.
325
- finalText = pickFinalText(promptResult) ?? assistantText;
330
+ const tailText = assistantTextFilter.flush();
331
+ if (tailText && !capped) {
332
+ const bytes = Buffer.byteLength(tailText, "utf8");
333
+ if (assistantBytes + bytes <= ASSISTANT_TEXT_CAP) {
334
+ assistantText += tailText;
335
+ assistantBytes += bytes;
336
+ seq += 1;
337
+ emitBlock({
338
+ raw: {
339
+ method: "session/update",
340
+ params: {
341
+ sessionId: acpSessionId,
342
+ update: { sessionUpdate: "agent_message_chunk", content: [{ type: "text", text: tailText }] },
343
+ },
344
+ },
345
+ kind: "assistant_text",
346
+ seq,
347
+ });
348
+ }
349
+ }
350
+ const pickedText = normalizeAssistantText(pickFinalText(promptResult));
351
+ const streamedText = normalizeAssistantText(assistantText);
352
+ finalText = pickedText && !looksLikeReasoningLeak(pickedText) ? pickedText : streamedText;
326
353
 
327
354
  if (capped) {
328
355
  log.warn("openclaw-acp.assistant-text-capped", { sessionId: acpSessionId });
@@ -639,6 +666,8 @@ function extractText(content: unknown): string {
639
666
  }
640
667
  if (typeof content === "object") {
641
668
  const c = content as Record<string, unknown>;
669
+ const type = typeof c.type === "string" ? c.type.toLowerCase() : "";
670
+ if (type === "thinking" || type === "reasoning" || type === "thought") return "";
642
671
  if (typeof c.text === "string") return c.text;
643
672
  if (typeof c.content === "string") return c.content;
644
673
  if (Array.isArray(c.content)) return extractText(c.content);
@@ -646,14 +675,189 @@ function extractText(content: unknown): string {
646
675
  return "";
647
676
  }
648
677
 
678
+ function sanitizeAssistantChunk(note: AcpNotification, text: string): AcpNotification {
679
+ return {
680
+ ...note,
681
+ params: {
682
+ ...note.params,
683
+ update: {
684
+ ...note.params?.update,
685
+ content: [{ type: "text", text }],
686
+ },
687
+ },
688
+ };
689
+ }
690
+
691
+ function normalizeAssistantText(text: string | undefined): string {
692
+ if (!text) return "";
693
+ const finalMatch = text.match(/<final>([\s\S]*?)<\/final>/i);
694
+ const selected = finalMatch ? finalMatch[1] : text;
695
+ if (!finalMatch && selected.trimStart().toLowerCase().startsWith("<think")) {
696
+ return "";
697
+ }
698
+ return selected
699
+ .replace(/<think[^>]*>[\s\S]*?<\/think>/gi, "")
700
+ .replace(/<\/?final>/gi, "")
701
+ .trim();
702
+ }
703
+
704
+ function createAssistantTextFilter(): {
705
+ push(text: string): string;
706
+ flush(): string;
707
+ } {
708
+ let pending = "";
709
+ let inThink = false;
710
+ let inFinal = false;
711
+ let seenFinal = false;
712
+ let fallback = "";
713
+
714
+ const consume = (flush: boolean): string => {
715
+ let out = "";
716
+ while (pending.length > 0) {
717
+ if (inThink) {
718
+ const close = pending.search(/<\/think>/i);
719
+ if (close === -1) {
720
+ if (flush) pending = "";
721
+ return out;
722
+ }
723
+ pending = pending.slice(close).replace(/^<\/think>/i, "");
724
+ inThink = false;
725
+ continue;
726
+ }
727
+ if (inFinal) {
728
+ const close = pending.search(/<\/final>/i);
729
+ if (close === -1) {
730
+ out += pending;
731
+ pending = "";
732
+ return out;
733
+ }
734
+ out += pending.slice(0, close);
735
+ pending = pending.slice(close).replace(/^<\/final>/i, "");
736
+ inFinal = false;
737
+ continue;
738
+ }
739
+
740
+ const lt = pending.indexOf("<");
741
+ if (lt === -1) {
742
+ if (seenFinal) {
743
+ out += pending;
744
+ } else {
745
+ fallback += pending;
746
+ }
747
+ pending = "";
748
+ return out;
749
+ }
750
+
751
+ if (lt > 0) {
752
+ if (seenFinal) {
753
+ out += pending.slice(0, lt);
754
+ } else {
755
+ fallback += pending.slice(0, lt);
756
+ }
757
+ pending = pending.slice(lt);
758
+ continue;
759
+ }
760
+
761
+ const lower = pending.toLowerCase();
762
+ if (lower.startsWith("<think")) {
763
+ const end = pending.indexOf(">");
764
+ if (end === -1) {
765
+ if (flush) pending = "";
766
+ return out;
767
+ }
768
+ pending = pending.slice(end + 1);
769
+ inThink = true;
770
+ continue;
771
+ }
772
+ if (lower.startsWith("</think")) {
773
+ const end = pending.indexOf(">");
774
+ if (end === -1) {
775
+ if (flush) pending = "";
776
+ return out;
777
+ }
778
+ pending = pending.slice(end + 1);
779
+ continue;
780
+ }
781
+ if (lower.startsWith("<final")) {
782
+ const end = pending.indexOf(">");
783
+ if (end === -1) {
784
+ if (flush) pending = "";
785
+ return out;
786
+ }
787
+ pending = pending.slice(end + 1);
788
+ seenFinal = true;
789
+ fallback = "";
790
+ inFinal = true;
791
+ continue;
792
+ }
793
+ if (lower.startsWith("</final")) {
794
+ const end = pending.indexOf(">");
795
+ if (end === -1) {
796
+ if (flush) pending = "";
797
+ return out;
798
+ }
799
+ pending = pending.slice(end + 1);
800
+ inFinal = false;
801
+ continue;
802
+ }
803
+
804
+ const knownPrefixes = ["<think", "</think", "<final", "</final"];
805
+ if (!flush && knownPrefixes.some((prefix) => prefix.startsWith(lower))) {
806
+ return out;
807
+ }
808
+
809
+ out += "<";
810
+ pending = pending.slice(1);
811
+ }
812
+ if (flush && !seenFinal && fallback) {
813
+ const text = normalizeAssistantText(fallback);
814
+ fallback = "";
815
+ if (!looksLikeReasoningLeak(text)) return text;
816
+ }
817
+ return out;
818
+ };
819
+
820
+ return {
821
+ push(text: string): string {
822
+ if (!text) return "";
823
+ pending += text;
824
+ return consume(false);
825
+ },
826
+ flush(): string {
827
+ return consume(true);
828
+ },
829
+ };
830
+ }
831
+
649
832
  function pickFinalText(result: unknown): string | undefined {
650
833
  if (!result || typeof result !== "object") return undefined;
651
834
  const r = result as Record<string, unknown>;
835
+ if (Array.isArray(r.assistantTexts)) {
836
+ const text = r.assistantTexts.filter((x): x is string => typeof x === "string").join("\n");
837
+ if (text.length > 0) return text;
838
+ }
839
+ const contentText = extractText(r.content);
840
+ if (contentText.length > 0) return contentText;
841
+ const outputText = extractText(r.output);
842
+ if (outputText.length > 0) return outputText;
843
+ const responseText = extractText(r.response);
844
+ if (responseText.length > 0) return responseText;
652
845
  if (typeof r.text === "string" && r.text.length > 0) return r.text;
653
846
  if (typeof r.message === "string" && r.message.length > 0) return r.message;
654
847
  return undefined;
655
848
  }
656
849
 
850
+ function looksLikeReasoningLeak(text: string): boolean {
851
+ const t = text.trim();
852
+ if (!t) return false;
853
+ return (
854
+ /^the user (said|asked|wants|is asking)\b/i.test(t) ||
855
+ /^i('|’)m .*\b(i('|’)ll|i will|need to|should|going to)\b/i.test(t) ||
856
+ /\bi('|’)ll respond\b/i.test(t) ||
857
+ /\bi need to\b/i.test(t)
858
+ );
859
+ }
860
+
657
861
  function stringField(bag: Record<string, unknown> | undefined, key: string): string | undefined {
658
862
  if (!bag) return undefined;
659
863
  const v = bag[key];
@@ -197,6 +197,16 @@ export interface ChannelStatusSnapshot {
197
197
  lastStartAt?: number;
198
198
  lastStopAt?: number;
199
199
  lastError?: string | null;
200
+ /** Third-party provider id when this channel is not the built-in BotCord. */
201
+ provider?: "wechat" | "telegram";
202
+ /** Last time the adapter polled the upstream provider (ms epoch). */
203
+ lastPollAt?: number;
204
+ /** Last time the adapter accepted an inbound message (ms epoch). */
205
+ lastInboundAt?: number;
206
+ /** Last time the adapter successfully sent a reply (ms epoch). */
207
+ lastSendAt?: number;
208
+ /** Whether the adapter currently holds a usable provider credential. */
209
+ authorized?: boolean;
200
210
  }
201
211
 
202
212
  /** Per-turn status snapshot describing a currently-executing runtime invocation. */