@a1hvdy/cc-openclaw 0.27.1 → 0.27.4
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/src/channels/telegram-mirror/askuser.js +2 -0
- package/dist/src/channels/telegram-mirror/card-renderer.d.ts +1 -22
- package/dist/src/channels/telegram-mirror/card-renderer.js +172 -20
- package/dist/src/channels/telegram-mirror/commands.d.ts +16 -0
- package/dist/src/channels/telegram-mirror/commands.js +53 -12
- package/dist/src/channels/telegram-mirror/inbound-handler.d.ts +18 -0
- package/dist/src/channels/telegram-mirror/inbound-handler.js +21 -8
- package/dist/src/channels/telegram-mirror/turn-bridge.d.ts +11 -0
- package/dist/src/channels/telegram-mirror/turn-bridge.js +31 -0
- package/dist/src/constants.d.ts +10 -0
- package/dist/src/constants.js +10 -0
- package/dist/src/lib/error-formatter.d.ts +14 -2
- package/dist/src/lib/error-formatter.js +23 -11
- package/dist/src/lib/error-renderer.js +3 -1
- package/dist/src/lib/html-render.d.ts +8 -16
- package/dist/src/lib/html-render.js +91 -1
- package/dist/src/lib/markdown-to-mdv2.js +2 -1
- package/dist/src/lib/probes.d.ts +50 -0
- package/dist/src/lib/probes.js +96 -0
- package/dist/src/lib/telegram-bot-api.d.ts +52 -6
- package/dist/src/lib/telegram-bot-api.js +180 -13
- package/dist/src/openai-compat/message-extractor.js +4 -0
- package/dist/src/openai-compat/openai-compat.js +12 -1
- package/dist/src/openai-compat/streaming-handler.js +7 -1
- package/dist/src/session/persisted-sessions.d.ts +11 -0
- package/dist/src/session/persisted-sessions.js +17 -0
- package/dist/src/session/session-manager.js +22 -6
- package/dist/src/session-bootstrap/cwd-patch.js +1 -2
- package/dist/src/types.d.ts +7 -0
- package/package.json +1 -1
package/dist/src/constants.d.ts
CHANGED
|
@@ -36,6 +36,16 @@ export declare const TURN_TIMEOUT_MS = 900000;
|
|
|
36
36
|
export declare const STALLED_SESSION_KILL_MS = 180000;
|
|
37
37
|
/** How often the stalled-session watchdog scans the sessions Map. */
|
|
38
38
|
export declare const STALLED_WATCH_INTERVAL_MS = 30000;
|
|
39
|
+
/** v0.27.4 (M4/M6) — resume-freshness window for openai-compat sessions.
|
|
40
|
+
* After a gateway restart OR a stalled-session watchdog SIGTERM, the in-process
|
|
41
|
+
* session object is gone; without this, the next turn for the same chat spawns
|
|
42
|
+
* a FRESH Claude conversation (no --resume) and the user loses all context.
|
|
43
|
+
* When an openai-compat session opts in (config.resumeFreshnessMs), the next
|
|
44
|
+
* turn resumes the prior Claude session ONLY if it was active within this
|
|
45
|
+
* window — older sessions start fresh, preserving the anti-stale intent that
|
|
46
|
+
* motivated skipPersistence. Default 30 min; env-overridable at the use site
|
|
47
|
+
* via CC_OPENCLAW_RESUME_FRESHNESS_MS. */
|
|
48
|
+
export declare const RESUME_FRESHNESS_MS = 1800000;
|
|
39
49
|
/** Runaway-loop watchdog: max new cc-openclaw subprocess spawns within
|
|
40
50
|
* RUNAWAY_LOOP_WINDOW_MS before the next spawn is refused.
|
|
41
51
|
*
|
package/dist/src/constants.js
CHANGED
|
@@ -39,6 +39,16 @@ export const TURN_TIMEOUT_MS = 900_000;
|
|
|
39
39
|
export const STALLED_SESSION_KILL_MS = 180_000;
|
|
40
40
|
/** How often the stalled-session watchdog scans the sessions Map. */
|
|
41
41
|
export const STALLED_WATCH_INTERVAL_MS = 30_000;
|
|
42
|
+
/** v0.27.4 (M4/M6) — resume-freshness window for openai-compat sessions.
|
|
43
|
+
* After a gateway restart OR a stalled-session watchdog SIGTERM, the in-process
|
|
44
|
+
* session object is gone; without this, the next turn for the same chat spawns
|
|
45
|
+
* a FRESH Claude conversation (no --resume) and the user loses all context.
|
|
46
|
+
* When an openai-compat session opts in (config.resumeFreshnessMs), the next
|
|
47
|
+
* turn resumes the prior Claude session ONLY if it was active within this
|
|
48
|
+
* window — older sessions start fresh, preserving the anti-stale intent that
|
|
49
|
+
* motivated skipPersistence. Default 30 min; env-overridable at the use site
|
|
50
|
+
* via CC_OPENCLAW_RESUME_FRESHNESS_MS. */
|
|
51
|
+
export const RESUME_FRESHNESS_MS = 1_800_000;
|
|
42
52
|
/** Runaway-loop watchdog: max new cc-openclaw subprocess spawns within
|
|
43
53
|
* RUNAWAY_LOOP_WINDOW_MS before the next spawn is refused.
|
|
44
54
|
*
|
|
@@ -49,7 +49,12 @@ export interface ErrorContext {
|
|
|
49
49
|
export interface FormattedError {
|
|
50
50
|
/** NDJSON-ready row */
|
|
51
51
|
jsonlRow: ErrorJsonlRow;
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Telegram message text (HTML parse_mode). v0.27.3 — converted from
|
|
54
|
+
* MarkdownV2 to HTML so the entire Telegram surface (live card + error
|
|
55
|
+
* alerts) renders through ONE parse mode. error-renderer sends this with
|
|
56
|
+
* parse_mode: 'HTML'.
|
|
57
|
+
*/
|
|
53
58
|
telegramText: string;
|
|
54
59
|
}
|
|
55
60
|
export interface ErrorJsonlRow {
|
|
@@ -64,7 +69,14 @@ export interface ErrorJsonlRow {
|
|
|
64
69
|
}
|
|
65
70
|
/** Extract a usable message from unknown thrown values. */
|
|
66
71
|
export declare function extractMessage(error: unknown): string;
|
|
67
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Escape characters special to Telegram MarkdownV2.
|
|
74
|
+
*
|
|
75
|
+
* v0.27.3 — RETAINED for back-compat (and its own unit tests) but no longer
|
|
76
|
+
* used by formatError, which now emits HTML (see the telegramText block) so the
|
|
77
|
+
* whole Telegram surface renders through one parse mode. Kept exported in case a
|
|
78
|
+
* caller still needs MarkdownV2 escaping.
|
|
79
|
+
*/
|
|
68
80
|
export declare function escapeMdV2(text: string): string;
|
|
69
81
|
/**
|
|
70
82
|
* Pure formatter — no side effects on the return value, but DOES emit
|
|
@@ -80,7 +80,14 @@ function extractStack(error) {
|
|
|
80
80
|
}
|
|
81
81
|
return undefined;
|
|
82
82
|
}
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Escape characters special to Telegram MarkdownV2.
|
|
85
|
+
*
|
|
86
|
+
* v0.27.3 — RETAINED for back-compat (and its own unit tests) but no longer
|
|
87
|
+
* used by formatError, which now emits HTML (see the telegramText block) so the
|
|
88
|
+
* whole Telegram surface renders through one parse mode. Kept exported in case a
|
|
89
|
+
* caller still needs MarkdownV2 escaping.
|
|
90
|
+
*/
|
|
84
91
|
export function escapeMdV2(text) {
|
|
85
92
|
// Per Telegram docs, these must be escaped: _ * [ ] ( ) ~ ` > # + - = | { } . !
|
|
86
93
|
return text.replace(/[_*[\]()~`>#+=|{}.!\\-]/g, (c) => `\\${c}`);
|
|
@@ -97,6 +104,7 @@ const SEVERITY_EMOJI = {
|
|
|
97
104
|
// are unset, so the import itself has zero runtime cost.
|
|
98
105
|
import { emit as emitTrajectory } from './trajectory.js';
|
|
99
106
|
import { metricsRegistry } from '../health/metrics.js';
|
|
107
|
+
import { escapeHtml } from './html-render.js';
|
|
100
108
|
/**
|
|
101
109
|
* Pure formatter — no side effects on the return value, but DOES emit
|
|
102
110
|
* trajectory + metrics events for centralized observability per Pillars A+B.
|
|
@@ -128,27 +136,31 @@ export function formatError(error, context) {
|
|
|
128
136
|
...(stack !== undefined ? { stack } : {}),
|
|
129
137
|
...(context.details !== undefined ? { details: context.details } : {}),
|
|
130
138
|
};
|
|
139
|
+
// v0.27.3 — emit Telegram HTML (sent with parse_mode: 'HTML' by
|
|
140
|
+
// error-renderer) so error alerts share the live card's single render path.
|
|
141
|
+
// <b> code, <code> message, <i> timestamp; all interpolated text is
|
|
142
|
+
// HTML-escaped (& < >) so a stray < in a message can't break the parser.
|
|
131
143
|
const emoji = SEVERITY_EMOJI[severity];
|
|
132
|
-
const safeCode =
|
|
133
|
-
const safeMsg =
|
|
134
|
-
const safeTs =
|
|
144
|
+
const safeCode = escapeHtml(context.code);
|
|
145
|
+
const safeMsg = escapeHtml(message.length > 300 ? message.slice(0, 300) + '...' : message);
|
|
146
|
+
const safeTs = escapeHtml(ts.slice(0, 19).replace('T', ' '));
|
|
135
147
|
const lines = [
|
|
136
|
-
`${emoji}
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
`${emoji} <b>${safeCode}</b>`,
|
|
149
|
+
`<code>${safeMsg}</code>`,
|
|
150
|
+
`<i>${safeTs}</i>`,
|
|
139
151
|
];
|
|
140
152
|
if (context.sessionId) {
|
|
141
|
-
lines.push(`session:
|
|
153
|
+
lines.push(`session: <code>${escapeHtml(context.sessionId)}</code>`);
|
|
142
154
|
}
|
|
143
155
|
if (context.laptopId) {
|
|
144
|
-
lines.push(`laptop:
|
|
156
|
+
lines.push(`laptop: <code>${escapeHtml(context.laptopId)}</code>`);
|
|
145
157
|
}
|
|
146
158
|
if (context.details && Object.keys(context.details).length > 0) {
|
|
147
159
|
const detailParts = Object.entries(context.details)
|
|
148
160
|
.slice(0, 5)
|
|
149
|
-
.map(([k, v]) => `${
|
|
161
|
+
.map(([k, v]) => `${escapeHtml(k)}: ${escapeHtml(String(v))}`)
|
|
150
162
|
.join(', ');
|
|
151
|
-
lines.push(
|
|
163
|
+
lines.push(`<i>${detailParts}</i>`);
|
|
152
164
|
}
|
|
153
165
|
const telegramText = lines.join('\n');
|
|
154
166
|
return { jsonlRow, telegramText };
|
|
@@ -67,7 +67,9 @@ export async function renderError(formatted, opts = {}) {
|
|
|
67
67
|
const params = {
|
|
68
68
|
chat_id: chatId,
|
|
69
69
|
text: telegramText,
|
|
70
|
-
|
|
70
|
+
// v0.27.3 — HTML (formatter now emits HTML) so error alerts share the live
|
|
71
|
+
// card's single render path. The 400-fallback below sends plain text.
|
|
72
|
+
parse_mode: 'HTML',
|
|
71
73
|
};
|
|
72
74
|
if (opts.threadId)
|
|
73
75
|
params.message_thread_id = opts.threadId;
|
|
@@ -18,26 +18,18 @@
|
|
|
18
18
|
export declare function escapeHtml(text: string | null | undefined): string;
|
|
19
19
|
/** Inline monospace span: `<code>escaped</code>`. */
|
|
20
20
|
export declare function code(s: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Strip Telegram HTML markup back to readable plain text (v0.27.3). Used
|
|
23
|
+
* by editTg's plain-text fallback: when an HTML edit is rejected, re-sending the
|
|
24
|
+
* SAME string without parse_mode would dump literal `<b>`/`<pre>` tags into the
|
|
25
|
+
* chat. This removes the tags and decodes the basic entities so the fallback
|
|
26
|
+
* stays legible. Not a general sanitizer — scoped to the tags this module emits.
|
|
27
|
+
*/
|
|
28
|
+
export declare function stripHtml(input: string | null | undefined): string;
|
|
21
29
|
/**
|
|
22
30
|
* Fenced code block: `<pre><code class="language-LANG">escaped</code></pre>`.
|
|
23
31
|
* Omits the class when no language is given. Body is HTML-escaped (the only
|
|
24
32
|
* escaping Telegram requires inside pre/code).
|
|
25
33
|
*/
|
|
26
34
|
export declare function pre(body: string, lang?: string): string;
|
|
27
|
-
/**
|
|
28
|
-
* Convert a subset of CommonMark Markdown to Telegram HTML with structure
|
|
29
|
-
* preserved (`**bold**`→`<b>`, fenced blocks→`<pre><code>`, etc.). Returns ''
|
|
30
|
-
* for null/undefined.
|
|
31
|
-
*
|
|
32
|
-
* Algorithm (placeholder substitution, NUL-sentinel keyed — mirrors
|
|
33
|
-
* markdown-to-mdv2.ts so behaviour is auditable side-by-side):
|
|
34
|
-
* 1. code fences → <pre><code [class]>…</code></pre>
|
|
35
|
-
* 2. inline code → <code>…</code>
|
|
36
|
-
* 3. bold (**…**) → <b>…</b>
|
|
37
|
-
* 4. italic (*…* / _…_) → <i>…</i>
|
|
38
|
-
* 5. ATX headers (#…) at line start → <b>…</b>
|
|
39
|
-
* 6. links [label](url) → <a href="url">label</a>
|
|
40
|
-
* 7. escape remaining text (& < >)
|
|
41
|
-
* 8. restore placeholders verbatim
|
|
42
|
-
*/
|
|
43
35
|
export declare function markdownToHtml(input: string | null | undefined): string;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* pre+code[class=language-X], blockquote, tg-spoiler. Everything else in text
|
|
15
15
|
* content must have &, <, > escaped.
|
|
16
16
|
*/
|
|
17
|
+
/* eslint-disable no-control-regex -- NUL-byte sentinel delimiters intentionally guard placeholders through the bulk-escape step, then are restored */
|
|
17
18
|
/** Escape the three HTML-significant chars for Telegram HTML text content. */
|
|
18
19
|
export function escapeHtml(text) {
|
|
19
20
|
if (text == null)
|
|
@@ -27,6 +28,23 @@ export function escapeHtml(text) {
|
|
|
27
28
|
export function code(s) {
|
|
28
29
|
return `<code>${escapeHtml(s)}</code>`;
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Strip Telegram HTML markup back to readable plain text (v0.27.3). Used
|
|
33
|
+
* by editTg's plain-text fallback: when an HTML edit is rejected, re-sending the
|
|
34
|
+
* SAME string without parse_mode would dump literal `<b>`/`<pre>` tags into the
|
|
35
|
+
* chat. This removes the tags and decodes the basic entities so the fallback
|
|
36
|
+
* stays legible. Not a general sanitizer — scoped to the tags this module emits.
|
|
37
|
+
*/
|
|
38
|
+
export function stripHtml(input) {
|
|
39
|
+
if (input == null)
|
|
40
|
+
return '';
|
|
41
|
+
return String(input)
|
|
42
|
+
.replace(/<\/?(?:b|strong|i|em|u|s|a|code|pre|blockquote|tg-spoiler)\b[^>]*>/gi, '')
|
|
43
|
+
.replace(/</g, '<')
|
|
44
|
+
.replace(/>/g, '>')
|
|
45
|
+
.replace(/"/g, '"')
|
|
46
|
+
.replace(/&/g, '&');
|
|
47
|
+
}
|
|
30
48
|
/**
|
|
31
49
|
* Fenced code block: `<pre><code class="language-LANG">escaped</code></pre>`.
|
|
32
50
|
* Omits the class when no language is given. Body is HTML-escaped (the only
|
|
@@ -52,16 +70,73 @@ export function pre(body, lang) {
|
|
|
52
70
|
* 7. escape remaining text (& < >)
|
|
53
71
|
* 8. restore placeholders verbatim
|
|
54
72
|
*/
|
|
73
|
+
/** Split a GFM table row into trimmed cells, dropping the empty cells produced
|
|
74
|
+
* by leading/trailing pipes (`| a | b |` → ['a','b']). */
|
|
75
|
+
function splitTableRow(row) {
|
|
76
|
+
const cells = row.split('|').map((c) => c.trim());
|
|
77
|
+
if (cells.length && cells[0] === '')
|
|
78
|
+
cells.shift();
|
|
79
|
+
if (cells.length && cells[cells.length - 1] === '')
|
|
80
|
+
cells.pop();
|
|
81
|
+
return cells;
|
|
82
|
+
}
|
|
83
|
+
/** Render parsed table rows as a padded monospace block. Telegram HTML has no
|
|
84
|
+
* <table>, so column-aligned text inside <pre> is the faithful terminal-style
|
|
85
|
+
* rendering. Returns a ready <pre><code> string (content HTML-escaped by pre()). */
|
|
86
|
+
function renderPaddedTable(rows) {
|
|
87
|
+
const cols = Math.max(...rows.map((r) => r.length));
|
|
88
|
+
const widths = [];
|
|
89
|
+
for (let c = 0; c < cols; c++) {
|
|
90
|
+
widths[c] = Math.max(...rows.map((r) => (r[c] ?? '').length));
|
|
91
|
+
}
|
|
92
|
+
const body = rows
|
|
93
|
+
.map((r) => Array.from({ length: cols }, (_v, c) => (r[c] ?? '').padEnd(widths[c]))
|
|
94
|
+
.join(' | ')
|
|
95
|
+
.replace(/\s+$/, ''))
|
|
96
|
+
.join('\n');
|
|
97
|
+
return pre(body);
|
|
98
|
+
}
|
|
55
99
|
export function markdownToHtml(input) {
|
|
56
100
|
if (input == null)
|
|
57
101
|
return '';
|
|
58
102
|
let text = String(input);
|
|
59
103
|
const codeBlocks = [];
|
|
60
|
-
text = text.replace(/```([a-zA-Z0-9_
|
|
104
|
+
text = text.replace(/```([a-zA-Z0-9_+-]*)\n?([\s\S]*?)```/g, (_m, lang, body) => {
|
|
61
105
|
const idx = codeBlocks.length;
|
|
62
106
|
codeBlocks.push(pre(body.replace(/\n$/, ''), lang || undefined));
|
|
63
107
|
return `\x00CODEBLOCK${idx}\x00`;
|
|
64
108
|
});
|
|
109
|
+
// v0.27.4 M4 — GFM pipe tables → padded monospace <pre> (gap #5). Telegram HTML
|
|
110
|
+
// has no <table>; column-aligned text is the terminal-faithful rendering. A
|
|
111
|
+
// table is a row containing `|` immediately followed by a `---` separator row.
|
|
112
|
+
const tables = [];
|
|
113
|
+
{
|
|
114
|
+
const lines = text.split('\n');
|
|
115
|
+
const out = [];
|
|
116
|
+
let i = 0;
|
|
117
|
+
while (i < lines.length) {
|
|
118
|
+
const header = lines[i];
|
|
119
|
+
const sep = lines[i + 1];
|
|
120
|
+
const isSep = sep != null && sep.includes('-') && /^\s*\|?[\s:|-]+\|?\s*$/.test(sep);
|
|
121
|
+
if (header != null && header.includes('|') && isSep) {
|
|
122
|
+
const block = [splitTableRow(header)];
|
|
123
|
+
let j = i + 2;
|
|
124
|
+
while (j < lines.length && lines[j].includes('|') && lines[j].trim() !== '') {
|
|
125
|
+
block.push(splitTableRow(lines[j]));
|
|
126
|
+
j++;
|
|
127
|
+
}
|
|
128
|
+
const idx = tables.length;
|
|
129
|
+
tables.push(renderPaddedTable(block));
|
|
130
|
+
out.push(`\x00TABLE${idx}\x00`);
|
|
131
|
+
i = j;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
out.push(header);
|
|
135
|
+
i++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
text = out.join('\n');
|
|
139
|
+
}
|
|
65
140
|
const inlineCodes = [];
|
|
66
141
|
text = text.replace(/`([^`\n]+)`/g, (_m, body) => {
|
|
67
142
|
const idx = inlineCodes.length;
|
|
@@ -74,6 +149,14 @@ export function markdownToHtml(input) {
|
|
|
74
149
|
bolds.push(`<b>${escapeHtml(body)}</b>`);
|
|
75
150
|
return `\x00BOLD${idx}\x00`;
|
|
76
151
|
});
|
|
152
|
+
// v0.27.4 M4 — strikethrough ~~text~~ → <s> (gap #5). After bold so the `~~`
|
|
153
|
+
// pass never sees `**`-delimited spans.
|
|
154
|
+
const strikes = [];
|
|
155
|
+
text = text.replace(/~~([^~\n]+)~~/g, (_m, body) => {
|
|
156
|
+
const idx = strikes.length;
|
|
157
|
+
strikes.push(`<s>${escapeHtml(body)}</s>`);
|
|
158
|
+
return `\x00STRIKE${idx}\x00`;
|
|
159
|
+
});
|
|
77
160
|
const italics = [];
|
|
78
161
|
text = text.replace(/(?<![\w*])\*([^*\n]+)\*(?!\w)/g, (_m, body) => {
|
|
79
162
|
const idx = italics.length;
|
|
@@ -99,10 +182,17 @@ export function markdownToHtml(input) {
|
|
|
99
182
|
links.push(`<a href="${safeUrl}">${escapeHtml(label)}</a>`);
|
|
100
183
|
return `\x00LINK${idx}\x00`;
|
|
101
184
|
});
|
|
185
|
+
// v0.27.4 M4 — unordered list markers (-, *, +) at line start → "• " bullet
|
|
186
|
+
// (gap #5). Ordered lists (1. 2.) already read fine as plain text. The bullet
|
|
187
|
+
// glyph isn't HTML-significant, so it survives the escape below. Uses [ \t]
|
|
188
|
+
// (not \s) so it never consumes the line break.
|
|
189
|
+
text = text.replace(/^([ \t]*)[-*+][ \t]+/gm, (_m, indent) => `${indent}• `);
|
|
102
190
|
text = escapeHtml(text);
|
|
103
191
|
text = text.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m, idx) => codeBlocks[Number(idx)]);
|
|
192
|
+
text = text.replace(/\x00TABLE(\d+)\x00/g, (_m, idx) => tables[Number(idx)]);
|
|
104
193
|
text = text.replace(/\x00INLINECODE(\d+)\x00/g, (_m, idx) => inlineCodes[Number(idx)]);
|
|
105
194
|
text = text.replace(/\x00BOLD(\d+)\x00/g, (_m, idx) => bolds[Number(idx)]);
|
|
195
|
+
text = text.replace(/\x00STRIKE(\d+)\x00/g, (_m, idx) => strikes[Number(idx)]);
|
|
106
196
|
text = text.replace(/\x00ITALIC(\d+)\x00/g, (_m, idx) => italics[Number(idx)]);
|
|
107
197
|
text = text.replace(/\x00HEADER(\d+)\x00/g, (_m, idx) => headers[Number(idx)]);
|
|
108
198
|
text = text.replace(/\x00LINK(\d+)\x00/g, (_m, idx) => links[Number(idx)]);
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
* - "CODE"/"BOLD"/etc letters are not in the special-char set
|
|
47
47
|
* So placeholders survive the bulk-escape step intact.
|
|
48
48
|
*/
|
|
49
|
+
/* eslint-disable no-control-regex -- NUL-byte sentinel delimiters intentionally guard placeholders through the bulk-escape step, then are restored */
|
|
49
50
|
/** MarkdownV2 special characters (Telegram Bot API spec). Outside code spans
|
|
50
51
|
* / pre blocks, these MUST be backslash-escaped when intended as content. */
|
|
51
52
|
const MDV2_TEXT_SPECIAL = /[_*[\]()~`>#+\-=|{}.!\\]/g;
|
|
@@ -73,7 +74,7 @@ export function markdownToMdv2(input) {
|
|
|
73
74
|
// Step 1 — extract code fences (triple-backtick blocks). Body verbatim
|
|
74
75
|
// except backticks and backslashes are escaped per Telegram spec.
|
|
75
76
|
const codeBlocks = [];
|
|
76
|
-
text = text.replace(/```([a-zA-Z0-9_
|
|
77
|
+
text = text.replace(/```([a-zA-Z0-9_+-]*)\n?([\s\S]*?)```/g, (_m, lang, body) => {
|
|
77
78
|
const idx = codeBlocks.length;
|
|
78
79
|
const langStr = lang || '';
|
|
79
80
|
const escapedBody = escapeCodeContent(body);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/probes.ts — Phase-0 empirical probe instrumentation (planning P0-A/B/C).
|
|
3
|
+
*
|
|
4
|
+
* OBSERVE-ONLY. Gated by `CC_OPENCLAW_PROBE=1` so it is completely silent — zero
|
|
5
|
+
* behavior change, no log output — in normal operation. The operator (A1)
|
|
6
|
+
* enables it for a single probe session, exercises the relevant Telegram
|
|
7
|
+
* interaction, then greps stderr for the `[cc-openclaw/probe]` markers. The
|
|
8
|
+
* runbook (PROBES-RUNBOOK in the planning dir) has the exact steps + how to read
|
|
9
|
+
* the results.
|
|
10
|
+
*
|
|
11
|
+
* These resolve the load-bearing seams that CANNOT be read from source because
|
|
12
|
+
* they depend on OpenClaw gateway runtime behavior:
|
|
13
|
+
* P0-A: does `enqueueNextTurnInjection` trigger a run, or only stage context
|
|
14
|
+
* for the next user message? (decides feature #1 Approve + #3 /send)
|
|
15
|
+
* P0-B: does an `ExitPlanMode` tool_use ever fire on the bypassPermissions
|
|
16
|
+
* Telegram path? (decides feature #1's trigger)
|
|
17
|
+
* P0-C: does a Telegram photo reach the plugin as an image block — at
|
|
18
|
+
* before_dispatch and/or in the openai-compat request body (where
|
|
19
|
+
* message-extractor strips non-text parts) — or is it gateway-stripped?
|
|
20
|
+
* (decides whether feature #2 is plugin-side feasible at all)
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* P0-A — log each `enqueueNextTurnInjection` call site. The operator correlates
|
|
24
|
+
* this with whether a reply arrives in Telegram WITHOUT typing a follow-up
|
|
25
|
+
* message: if it does, injection triggers a run; if not, it only stages context.
|
|
26
|
+
*/
|
|
27
|
+
export declare function probeInjectionEnqueued(sessionKey: string, source: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* P0-B — log when a tool_use is `ExitPlanMode` (and any other tool name, for
|
|
30
|
+
* context). Looks across the known event field paths, mirroring
|
|
31
|
+
* inbound-handler.extractToolUse so it works whatever shape the gateway uses.
|
|
32
|
+
*/
|
|
33
|
+
export declare function probeToolUse(ev: Record<string, unknown> | undefined): void;
|
|
34
|
+
/**
|
|
35
|
+
* P0-C (inbound surface) — dump the before_dispatch event, flagging whether it
|
|
36
|
+
* carries any media field, so the operator sees whether photo/document surface
|
|
37
|
+
* to the plugin at all.
|
|
38
|
+
*/
|
|
39
|
+
export declare function probeInboundShape(event: unknown): void;
|
|
40
|
+
/**
|
|
41
|
+
* P0-C (openai-compat body) — the PRECISE probe. Does an image block survive to
|
|
42
|
+
* the request body, where `message-extractor.ts` strips non-text parts? Logs
|
|
43
|
+
* each non-text content-part type. If image parts appear here, feature #2 is
|
|
44
|
+
* feasible plugin-side (preserve them through extractUserMessage); if nothing
|
|
45
|
+
* non-text ever appears, the image is gateway-stripped upstream → hands-off-blocked.
|
|
46
|
+
*/
|
|
47
|
+
export declare function probeMultimodalContent(messages: Array<{
|
|
48
|
+
role?: string;
|
|
49
|
+
content?: unknown;
|
|
50
|
+
}> | undefined): void;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/probes.ts — Phase-0 empirical probe instrumentation (planning P0-A/B/C).
|
|
3
|
+
*
|
|
4
|
+
* OBSERVE-ONLY. Gated by `CC_OPENCLAW_PROBE=1` so it is completely silent — zero
|
|
5
|
+
* behavior change, no log output — in normal operation. The operator (A1)
|
|
6
|
+
* enables it for a single probe session, exercises the relevant Telegram
|
|
7
|
+
* interaction, then greps stderr for the `[cc-openclaw/probe]` markers. The
|
|
8
|
+
* runbook (PROBES-RUNBOOK in the planning dir) has the exact steps + how to read
|
|
9
|
+
* the results.
|
|
10
|
+
*
|
|
11
|
+
* These resolve the load-bearing seams that CANNOT be read from source because
|
|
12
|
+
* they depend on OpenClaw gateway runtime behavior:
|
|
13
|
+
* P0-A: does `enqueueNextTurnInjection` trigger a run, or only stage context
|
|
14
|
+
* for the next user message? (decides feature #1 Approve + #3 /send)
|
|
15
|
+
* P0-B: does an `ExitPlanMode` tool_use ever fire on the bypassPermissions
|
|
16
|
+
* Telegram path? (decides feature #1's trigger)
|
|
17
|
+
* P0-C: does a Telegram photo reach the plugin as an image block — at
|
|
18
|
+
* before_dispatch and/or in the openai-compat request body (where
|
|
19
|
+
* message-extractor strips non-text parts) — or is it gateway-stripped?
|
|
20
|
+
* (decides whether feature #2 is plugin-side feasible at all)
|
|
21
|
+
*/
|
|
22
|
+
const TAG = '[cc-openclaw/probe]';
|
|
23
|
+
/** Read on every call so the operator can flip it without restarting. */
|
|
24
|
+
function probeOn() {
|
|
25
|
+
return process.env.CC_OPENCLAW_PROBE === '1';
|
|
26
|
+
}
|
|
27
|
+
function emit(line) {
|
|
28
|
+
// stderr so PM2 captures it regardless of stdout-only filtering.
|
|
29
|
+
process.stderr.write(`${TAG} ${line} ts=${Date.now()}\n`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* P0-A — log each `enqueueNextTurnInjection` call site. The operator correlates
|
|
33
|
+
* this with whether a reply arrives in Telegram WITHOUT typing a follow-up
|
|
34
|
+
* message: if it does, injection triggers a run; if not, it only stages context.
|
|
35
|
+
*/
|
|
36
|
+
export function probeInjectionEnqueued(sessionKey, source) {
|
|
37
|
+
if (!probeOn())
|
|
38
|
+
return;
|
|
39
|
+
emit(`P0-A injection-enqueued source=${source} sessionKey=${sessionKey}`);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* P0-B — log when a tool_use is `ExitPlanMode` (and any other tool name, for
|
|
43
|
+
* context). Looks across the known event field paths, mirroring
|
|
44
|
+
* inbound-handler.extractToolUse so it works whatever shape the gateway uses.
|
|
45
|
+
*/
|
|
46
|
+
export function probeToolUse(ev) {
|
|
47
|
+
if (!probeOn() || !ev)
|
|
48
|
+
return;
|
|
49
|
+
const tool = ev.tool;
|
|
50
|
+
const name = ev.toolName ??
|
|
51
|
+
tool?.['name'] ??
|
|
52
|
+
ev.name;
|
|
53
|
+
if (name === 'ExitPlanMode')
|
|
54
|
+
emit('P0-B ExitPlanMode-fired');
|
|
55
|
+
else if (name)
|
|
56
|
+
emit(`P0-B tool_use name=${name}`);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* P0-C (inbound surface) — dump the before_dispatch event, flagging whether it
|
|
60
|
+
* carries any media field, so the operator sees whether photo/document surface
|
|
61
|
+
* to the plugin at all.
|
|
62
|
+
*/
|
|
63
|
+
export function probeInboundShape(event) {
|
|
64
|
+
if (!probeOn())
|
|
65
|
+
return;
|
|
66
|
+
try {
|
|
67
|
+
const ev = event;
|
|
68
|
+
const msg = ev?.raw?.message;
|
|
69
|
+
const hasMedia = !!(msg && (msg.photo || msg.document || msg.video || msg.voice || msg.audio || msg.sticker));
|
|
70
|
+
const dump = JSON.stringify(event, (_k, v) => (typeof v === 'function' ? '[fn]' : v));
|
|
71
|
+
emit(`P0-C before_dispatch hasMedia=${hasMedia} shape=${dump.slice(0, 1200)}`);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
emit(`P0-C inbound dump failed: ${err.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* P0-C (openai-compat body) — the PRECISE probe. Does an image block survive to
|
|
79
|
+
* the request body, where `message-extractor.ts` strips non-text parts? Logs
|
|
80
|
+
* each non-text content-part type. If image parts appear here, feature #2 is
|
|
81
|
+
* feasible plugin-side (preserve them through extractUserMessage); if nothing
|
|
82
|
+
* non-text ever appears, the image is gateway-stripped upstream → hands-off-blocked.
|
|
83
|
+
*/
|
|
84
|
+
export function probeMultimodalContent(messages) {
|
|
85
|
+
if (!probeOn() || !messages)
|
|
86
|
+
return;
|
|
87
|
+
for (const m of messages) {
|
|
88
|
+
if (!Array.isArray(m.content))
|
|
89
|
+
continue;
|
|
90
|
+
const parts = m.content;
|
|
91
|
+
const nonText = parts.filter((p) => p && p.type && p.type !== 'text').map((p) => p.type);
|
|
92
|
+
if (nonText.length > 0) {
|
|
93
|
+
emit(`P0-C openai-body role=${m.role ?? '?'} nonTextParts=${nonText.join(',')}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -87,14 +87,60 @@ export interface TelegramApiResponse {
|
|
|
87
87
|
*/
|
|
88
88
|
export declare function telegramApi(method: string, params: Record<string, unknown>): Promise<TelegramApiResponse>;
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
90
|
+
* v0.27.4 M6 — CLI-parity gap #8: split a message that exceeds Telegram's 4096
|
|
91
|
+
* char cap into ≤max-char chunks so long cc-openclaw-originated content sends as
|
|
92
|
+
* sequential messages instead of being rejected outright. Splits on newline
|
|
93
|
+
* boundaries (never mid-line) so HTML constructs mostly stay intact; a single
|
|
94
|
+
* line longer than max is hard-split. Returns [text] unchanged when ≤max (the
|
|
95
|
+
* common path — no behavior change for normal messages).
|
|
96
|
+
*
|
|
97
|
+
* Scope note: this covers cc-openclaw's OWN sends (slash/error responses). The
|
|
98
|
+
* live card is one edited message (truncated by design) and the model's final
|
|
99
|
+
* answer is delivered by the OpenClaw gateway — neither flows through here.
|
|
100
|
+
*/
|
|
101
|
+
export declare function splitForTelegram(text: string, max?: number): string[];
|
|
102
|
+
/**
|
|
103
|
+
* sendMessage with HTML parse_mode first + plain-text fallback. The fallback
|
|
104
|
+
* is the v0.20.1 fix: prior implementation stripped punctuation on parse
|
|
105
|
+
* errors; current behaviour retries with parse_mode omitted so all content
|
|
106
|
+
* survives. (v0.27.0 switched the live mirror MarkdownV2 → HTML; v0.27.3
|
|
107
|
+
* converted the last MarkdownV2 emitter, error-formatter, so the whole
|
|
108
|
+
* Telegram surface is now one HTML render path.) v0.27.4 M6 — auto-chunks
|
|
109
|
+
* over-cap text and the plain fallback now strips HTML (was: re-sent raw tags).
|
|
94
110
|
*/
|
|
95
111
|
export declare function sendTg(chatId: string | number, text: string, threadId?: string | number, replyMarkup?: unknown, replyToMessageId?: number | null): Promise<TelegramApiResponse>;
|
|
96
112
|
/**
|
|
97
|
-
* editMessageText with
|
|
98
|
-
* plain-text fallback.
|
|
113
|
+
* editMessageText with HTML parse_mode + 429 retry-after handling +
|
|
114
|
+
* plain-text fallback (v0.27.3 stripHtml fallback on rejection).
|
|
99
115
|
*/
|
|
100
116
|
export declare function editTg(chatId: string | number, messageId: number, text: string, replyMarkup?: unknown): Promise<TelegramApiResponse>;
|
|
117
|
+
export interface SendDocumentOptions {
|
|
118
|
+
caption?: string;
|
|
119
|
+
parseMode?: 'HTML' | 'MarkdownV2';
|
|
120
|
+
threadId?: string | number;
|
|
121
|
+
replyMarkup?: unknown;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build a multipart/form-data body for sendDocument. PURE — no I/O — so the
|
|
125
|
+
* encoding (the R-3 risk) is unit-testable without a network round-trip.
|
|
126
|
+
*
|
|
127
|
+
* The document is sent as an inline InputFile (Content-Type text/markdown). The
|
|
128
|
+
* boundary MUST NOT appear in any field value or the file content; callers use a
|
|
129
|
+
* random 16-byte boundary (sendDocumentTg) so collision is astronomically
|
|
130
|
+
* unlikely against Markdown plan bodies.
|
|
131
|
+
*/
|
|
132
|
+
export declare function buildDocumentMultipart(opts: {
|
|
133
|
+
boundary: string;
|
|
134
|
+
chatId: string | number;
|
|
135
|
+
filename: string;
|
|
136
|
+
content: string;
|
|
137
|
+
caption?: string;
|
|
138
|
+
parseMode?: 'HTML' | 'MarkdownV2';
|
|
139
|
+
threadId?: string | number;
|
|
140
|
+
replyMarkup?: unknown;
|
|
141
|
+
}): Buffer;
|
|
142
|
+
/**
|
|
143
|
+
* Upload a text document (e.g. a plan .md) to a chat via sendDocument. Returns
|
|
144
|
+
* the API response, or {ok:false} on network/encoding failure (never throws).
|
|
145
|
+
*/
|
|
146
|
+
export declare function sendDocumentTg(chatId: string | number, filename: string, content: string, opts?: SendDocumentOptions): Promise<TelegramApiResponse>;
|