@foxden-app/foxclaw 0.5.33 → 0.5.34
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/CHANGELOG.md +12 -0
- package/dist/channels/bridge_messaging_router.d.ts +1 -0
- package/dist/channels/bridge_messaging_router.js +6 -0
- package/dist/channels/telegram/telegram_messaging_port.d.ts +1 -0
- package/dist/channels/telegram/telegram_messaging_port.js +4 -0
- package/dist/controller/controller.d.ts +4 -0
- package/dist/controller/controller.js +77 -4
- package/dist/telegram/rich_markdown.d.ts +2 -0
- package/dist/telegram/rich_markdown.js +115 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
|
|
4
4
|
|
|
5
|
+
## 0.5.34 - 2026-06-17
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- 普通 Codex 输出接入 Telegram RichMessage:流式阶段继续稳定发送纯文本,segment 完成后自动把已发消息编辑为富文本,支持标题、列表、引用、代码块、行内 code、粗体和安全链接。
|
|
9
|
+
- draft 流式路径优先使用官方 `sendRichMessageDraft`,发送失败时对当前 turn 自动熔断回原纯文本 draft,避免影响实时输出。
|
|
10
|
+
- 新增 Markdown 到 RichMessage HTML 的安全转换与回归测试,保留 Telegram/微信 fallback:RichMessage 失败时不会破坏原纯文本输出,微信继续使用原 HTML/plain 降级通道。
|
|
11
|
+
|
|
12
|
+
### English
|
|
13
|
+
- Wired normal Codex output into Telegram RichMessage: live streaming still sends stable plain text, then completed segments are edited into rich text with headings, lists, quotes, fenced code, inline code, bold text, and safe links.
|
|
14
|
+
- Draft streaming now prefers the official `sendRichMessageDraft` API and automatically disables rich drafts for the current turn if it fails, preserving the previous plain draft behavior.
|
|
15
|
+
- Added a safe Markdown-to-RichMessage HTML renderer with regression coverage while preserving Telegram/Weixin fallbacks so RichMessage failures do not break existing output.
|
|
16
|
+
|
|
5
17
|
## 0.5.33 - 2026-06-16
|
|
6
18
|
|
|
7
19
|
### 中文
|
|
@@ -23,6 +23,7 @@ export declare class BridgeMessagingRouter {
|
|
|
23
23
|
sendTypingInScope(scopeId: string): Promise<void>;
|
|
24
24
|
clearInlineKeyboard(scopeId: string, messageId: number): Promise<void>;
|
|
25
25
|
sendDraft(scopeId: string, draftId: number, text: string): Promise<void>;
|
|
26
|
+
sendRichDraft(scopeId: string, draftId: number, html: string, fallbackText: string): Promise<void>;
|
|
26
27
|
answerCallback(callbackQueryId: string, text: string): Promise<void>;
|
|
27
28
|
getFile(fileId: string): Promise<TelegramRemoteFile>;
|
|
28
29
|
downloadResolvedFile(remoteFilePath: string, destinationPath: string): Promise<number>;
|
|
@@ -85,6 +85,12 @@ export class BridgeMessagingRouter {
|
|
|
85
85
|
}
|
|
86
86
|
return this.telegram.sendDraft(scopeId, draftId, text);
|
|
87
87
|
}
|
|
88
|
+
sendRichDraft(scopeId, draftId, html, fallbackText) {
|
|
89
|
+
if (this.isWeixinScope(scopeId)) {
|
|
90
|
+
return this.requireWeixinTransport(scopeId).sendDraft(scopeId, draftId, fallbackText);
|
|
91
|
+
}
|
|
92
|
+
return this.telegram.sendRichDraft(scopeId, draftId, html);
|
|
93
|
+
}
|
|
88
94
|
answerCallback(callbackQueryId, text) {
|
|
89
95
|
return this.telegram.answerCallback(callbackQueryId, text);
|
|
90
96
|
}
|
|
@@ -21,6 +21,7 @@ export declare class TelegramMessagingPort implements ChannelPort {
|
|
|
21
21
|
sendTypingInScope(bridgeScopeId: string): Promise<void>;
|
|
22
22
|
clearInlineKeyboard(bridgeScopeId: string, messageId: number): Promise<void>;
|
|
23
23
|
sendDraft(bridgeScopeId: string, draftId: number, text: string): Promise<void>;
|
|
24
|
+
sendRichDraft(bridgeScopeId: string, draftId: number, html: string): Promise<void>;
|
|
24
25
|
answerCallback(callbackQueryId: string, text: string): Promise<void>;
|
|
25
26
|
getFile(fileId: string): Promise<TelegramRemoteFile>;
|
|
26
27
|
downloadResolvedFile(remoteFilePath: string, destinationPath: string): Promise<number>;
|
|
@@ -48,6 +48,10 @@ export class TelegramMessagingPort {
|
|
|
48
48
|
const target = parseTelegramTargetFromBridgeScope(bridgeScopeId);
|
|
49
49
|
await this.gateway.sendMessageDraft(target.chatId, draftId, text, target.topicId);
|
|
50
50
|
}
|
|
51
|
+
async sendRichDraft(bridgeScopeId, draftId, html) {
|
|
52
|
+
const target = parseTelegramTargetFromBridgeScope(bridgeScopeId);
|
|
53
|
+
await this.gateway.sendRichMessageDraft(target.chatId, draftId, telegramRichHtml(html, { skipEntityDetection: true }), target.topicId);
|
|
54
|
+
}
|
|
51
55
|
answerCallback(callbackQueryId, text) {
|
|
52
56
|
return this.gateway.answerCallback(callbackQueryId, text);
|
|
53
57
|
}
|
|
@@ -228,6 +228,7 @@ export declare class BridgeSessionCore {
|
|
|
228
228
|
private sendMessage;
|
|
229
229
|
private sendHtmlMessage;
|
|
230
230
|
private sendRichHtmlMessage;
|
|
231
|
+
private sendRichMarkdownMessage;
|
|
231
232
|
private editMessage;
|
|
232
233
|
private editHtmlMessage;
|
|
233
234
|
private editRichHtmlMessage;
|
|
@@ -439,6 +440,7 @@ export declare class BridgeSessionCore {
|
|
|
439
440
|
private forgetPreviewRecord;
|
|
440
441
|
private clearMessageButtons;
|
|
441
442
|
private sendDraft;
|
|
443
|
+
private sendRichDraft;
|
|
442
444
|
private renderActiveStatus;
|
|
443
445
|
private dismissTurnPreview;
|
|
444
446
|
private ensureStatusMessage;
|
|
@@ -459,5 +461,7 @@ export declare class BridgeSessionCore {
|
|
|
459
461
|
private findActiveTurnByThreadId;
|
|
460
462
|
private findActiveTurnsByThreadId;
|
|
461
463
|
private syncSegmentTimeline;
|
|
464
|
+
private shouldPromoteSegmentToRich;
|
|
465
|
+
private promoteSegmentMessagesToRich;
|
|
462
466
|
}
|
|
463
467
|
export { BridgeSessionCore as BridgeController };
|
|
@@ -11,6 +11,7 @@ import { TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES, buildAttachmentPrompt, isNativeI
|
|
|
11
11
|
import { TELEGRAM_MESSAGE_LIMIT, chunkTelegramMessage, chunkTelegramStreamMessage, clipTelegramDraftMessage, } from '../telegram/text.js';
|
|
12
12
|
import { escapeTelegramHtml, telegramBold, telegramDetails, telegramExpandableBlockquote, telegramPre, telegramPreCode, } from '../telegram/html.js';
|
|
13
13
|
import { TELEGRAM_RICH_MESSAGE_TEXT_LIMIT } from '../telegram/rich.js';
|
|
14
|
+
import { renderTelegramMarkdownRichHtml } from '../telegram/rich_markdown.js';
|
|
14
15
|
import { isDefaultTelegramScope, resolveTelegramAddressing } from '../telegram/addressing.js';
|
|
15
16
|
import { BRIDGE_SCOPE_WEIXIN_PREFIX, parseTelegramTargetFromBridgeScope, parseWeixinBridgeScope } from '../core/bridge_scope.js';
|
|
16
17
|
import { resolveTelegramRenderRoute } from '../telegram/rendering.js';
|
|
@@ -2930,6 +2931,7 @@ export class BridgeSessionCore {
|
|
|
2930
2931
|
previewActive: previewMessageId > 0,
|
|
2931
2932
|
draftId: null,
|
|
2932
2933
|
draftText: null,
|
|
2934
|
+
richDraftDisabled: false,
|
|
2933
2935
|
buffer: '',
|
|
2934
2936
|
finalText: null,
|
|
2935
2937
|
interruptRequested: false,
|
|
@@ -3133,7 +3135,7 @@ export class BridgeSessionCore {
|
|
|
3133
3135
|
const fallbackKey = active.interruptRequested ? 'interrupted' : 'completed';
|
|
3134
3136
|
const finalChunks = chunkTelegramMessage(active.finalText || active.buffer, undefined, t(locale, fallbackKey));
|
|
3135
3137
|
for (const chunk of finalChunks) {
|
|
3136
|
-
await this.
|
|
3138
|
+
await this.sendRichMarkdownMessage(active.scopeId, chunk);
|
|
3137
3139
|
}
|
|
3138
3140
|
}
|
|
3139
3141
|
shouldMarkPartialOutput = active.interruptRequested
|
|
@@ -3372,6 +3374,9 @@ export class BridgeSessionCore {
|
|
|
3372
3374
|
return this.sendHtmlMessage(scopeId, fallbackHtml, inlineKeyboard);
|
|
3373
3375
|
}
|
|
3374
3376
|
}
|
|
3377
|
+
async sendRichMarkdownMessage(scopeId, text, inlineKeyboard) {
|
|
3378
|
+
return this.sendRichHtmlMessage(scopeId, renderTelegramMarkdownRichHtml(text), escapeTelegramHtml(text), inlineKeyboard);
|
|
3379
|
+
}
|
|
3375
3380
|
async editMessage(scopeId, messageId, text, inlineKeyboard) {
|
|
3376
3381
|
await this.messaging.editPlain(scopeId, messageId, text, inlineKeyboard);
|
|
3377
3382
|
}
|
|
@@ -7619,6 +7624,9 @@ export class BridgeSessionCore {
|
|
|
7619
7624
|
async sendDraft(scopeId, draftId, text) {
|
|
7620
7625
|
await this.messaging.sendDraft(scopeId, draftId, text);
|
|
7621
7626
|
}
|
|
7627
|
+
async sendRichDraft(scopeId, draftId, html, fallbackText) {
|
|
7628
|
+
await this.messaging.sendRichDraft(scopeId, draftId, html, fallbackText);
|
|
7629
|
+
}
|
|
7622
7630
|
renderActiveStatus(active) {
|
|
7623
7631
|
const locale = this.localeForChat(active.scopeId);
|
|
7624
7632
|
return renderActiveTurnStatus(locale, {
|
|
@@ -7864,12 +7872,34 @@ export class BridgeSessionCore {
|
|
|
7864
7872
|
active.draftId = crypto.randomInt(1, 2_147_483_647);
|
|
7865
7873
|
}
|
|
7866
7874
|
try {
|
|
7867
|
-
|
|
7875
|
+
if (active.richDraftDisabled) {
|
|
7876
|
+
await this.sendDraft(active.scopeId, active.draftId, draftText);
|
|
7877
|
+
}
|
|
7878
|
+
else {
|
|
7879
|
+
await this.sendRichDraft(active.scopeId, active.draftId, renderTelegramMarkdownRichHtml(draftText), draftText);
|
|
7880
|
+
}
|
|
7868
7881
|
active.draftText = draftText;
|
|
7869
7882
|
}
|
|
7870
7883
|
catch (error) {
|
|
7884
|
+
let finalError = error;
|
|
7885
|
+
if (!active.richDraftDisabled) {
|
|
7886
|
+
active.richDraftDisabled = true;
|
|
7887
|
+
this.logger.warn('telegram.rich_draft_send_failed', {
|
|
7888
|
+
error: String(finalError),
|
|
7889
|
+
turnId: active.turnId,
|
|
7890
|
+
draftId: active.draftId,
|
|
7891
|
+
});
|
|
7892
|
+
try {
|
|
7893
|
+
await this.sendDraft(active.scopeId, active.draftId, draftText);
|
|
7894
|
+
active.draftText = draftText;
|
|
7895
|
+
return;
|
|
7896
|
+
}
|
|
7897
|
+
catch (fallbackError) {
|
|
7898
|
+
finalError = fallbackError;
|
|
7899
|
+
}
|
|
7900
|
+
}
|
|
7871
7901
|
this.logger.warn('telegram.draft_send_failed', {
|
|
7872
|
-
error: String(
|
|
7902
|
+
error: String(finalError),
|
|
7873
7903
|
turnId: active.turnId,
|
|
7874
7904
|
draftId: active.draftId,
|
|
7875
7905
|
});
|
|
@@ -7904,7 +7934,7 @@ export class BridgeSessionCore {
|
|
|
7904
7934
|
if (!existing) {
|
|
7905
7935
|
try {
|
|
7906
7936
|
const messageId = await this.sendMessage(active.scopeId, chunk);
|
|
7907
|
-
segment.messages.push({ messageId, text: chunk });
|
|
7937
|
+
segment.messages.push({ messageId, text: chunk, richHtml: null, richFailedForText: null });
|
|
7908
7938
|
active.statusNeedsRebase = true;
|
|
7909
7939
|
}
|
|
7910
7940
|
catch (error) {
|
|
@@ -7927,6 +7957,8 @@ export class BridgeSessionCore {
|
|
|
7927
7957
|
try {
|
|
7928
7958
|
await this.editMessage(active.scopeId, existing.messageId, chunk);
|
|
7929
7959
|
existing.text = chunk;
|
|
7960
|
+
existing.richHtml = null;
|
|
7961
|
+
existing.richFailedForText = null;
|
|
7930
7962
|
index += 1;
|
|
7931
7963
|
}
|
|
7932
7964
|
catch (error) {
|
|
@@ -7964,6 +7996,47 @@ export class BridgeSessionCore {
|
|
|
7964
7996
|
}
|
|
7965
7997
|
}
|
|
7966
7998
|
}
|
|
7999
|
+
if (this.shouldPromoteSegmentToRich(active, segment)) {
|
|
8000
|
+
await this.promoteSegmentMessagesToRich(active, segment, chunks);
|
|
8001
|
+
}
|
|
8002
|
+
}
|
|
8003
|
+
shouldPromoteSegmentToRich(active, segment) {
|
|
8004
|
+
return !active.scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)
|
|
8005
|
+
&& segment.completed
|
|
8006
|
+
&& segment.outputKind !== 'error'
|
|
8007
|
+
&& Boolean(segment.text.trim());
|
|
8008
|
+
}
|
|
8009
|
+
async promoteSegmentMessagesToRich(active, segment, chunks) {
|
|
8010
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
8011
|
+
const chunk = chunks[index];
|
|
8012
|
+
const existing = segment.messages[index];
|
|
8013
|
+
if (!existing || !chunk.trim()) {
|
|
8014
|
+
continue;
|
|
8015
|
+
}
|
|
8016
|
+
const richHtml = renderTelegramMarkdownRichHtml(chunk);
|
|
8017
|
+
if (existing.richHtml === richHtml || existing.richFailedForText === chunk) {
|
|
8018
|
+
continue;
|
|
8019
|
+
}
|
|
8020
|
+
try {
|
|
8021
|
+
await this.messaging.editRichHtml(active.scopeId, existing.messageId, richHtml, escapeTelegramHtml(chunk));
|
|
8022
|
+
existing.richHtml = richHtml;
|
|
8023
|
+
existing.richFailedForText = null;
|
|
8024
|
+
}
|
|
8025
|
+
catch (error) {
|
|
8026
|
+
if (isTelegramMessageGone(error)) {
|
|
8027
|
+
segment.messages.splice(index);
|
|
8028
|
+
return;
|
|
8029
|
+
}
|
|
8030
|
+
existing.richFailedForText = chunk;
|
|
8031
|
+
this.logger.warn('telegram.stream_rich_edit_failed', {
|
|
8032
|
+
error: String(error),
|
|
8033
|
+
turnId: active.turnId,
|
|
8034
|
+
itemId: segment.itemId,
|
|
8035
|
+
messageId: existing.messageId,
|
|
8036
|
+
chunkIndex: index,
|
|
8037
|
+
});
|
|
8038
|
+
}
|
|
8039
|
+
}
|
|
7967
8040
|
}
|
|
7968
8041
|
}
|
|
7969
8042
|
function ensureTurnSegment(active, itemId, phase, outputKind, isPlan) {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { escapeTelegramHtml, telegramPreCode } from './html.js';
|
|
2
|
+
const SAFE_LINK_SCHEMES = /^(https?:|mailto:|tg:)/i;
|
|
3
|
+
export function renderTelegramMarkdownRichHtml(markdown) {
|
|
4
|
+
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
|
|
5
|
+
const blocks = [];
|
|
6
|
+
let index = 0;
|
|
7
|
+
while (index < lines.length) {
|
|
8
|
+
const line = lines[index];
|
|
9
|
+
if (!line.trim()) {
|
|
10
|
+
index += 1;
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const fence = line.match(/^```([A-Za-z0-9_+-]*)\s*$/);
|
|
14
|
+
if (fence) {
|
|
15
|
+
const codeLines = [];
|
|
16
|
+
index += 1;
|
|
17
|
+
while (index < lines.length && !/^```\s*$/.test(lines[index])) {
|
|
18
|
+
codeLines.push(lines[index]);
|
|
19
|
+
index += 1;
|
|
20
|
+
}
|
|
21
|
+
if (index < lines.length) {
|
|
22
|
+
index += 1;
|
|
23
|
+
}
|
|
24
|
+
blocks.push(telegramPreCode(codeLines.join('\n'), sanitizeCodeLanguage(fence[1] ?? '')));
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const heading = line.match(/^(#{1,4})\s+(.+?)\s*#*\s*$/);
|
|
28
|
+
if (heading) {
|
|
29
|
+
const level = Math.min(4, Math.max(2, heading[1].length + 1));
|
|
30
|
+
blocks.push(`<h${level}>${renderInlineRichHtml(heading[2])}</h${level}>`);
|
|
31
|
+
index += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (/^\s*[-*+]\s+/.test(line)) {
|
|
35
|
+
const items = [];
|
|
36
|
+
while (index < lines.length && /^\s*[-*+]\s+/.test(lines[index])) {
|
|
37
|
+
items.push(stripListMarker(lines[index], false));
|
|
38
|
+
index += 1;
|
|
39
|
+
}
|
|
40
|
+
blocks.push(`<ul>${items.map(item => `<li>${renderInlineRichHtml(item)}</li>`).join('')}</ul>`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (/^\s*\d+[.)]\s+/.test(line)) {
|
|
44
|
+
const items = [];
|
|
45
|
+
while (index < lines.length && /^\s*\d+[.)]\s+/.test(lines[index])) {
|
|
46
|
+
items.push(stripListMarker(lines[index], true));
|
|
47
|
+
index += 1;
|
|
48
|
+
}
|
|
49
|
+
blocks.push(`<ol>${items.map(item => `<li>${renderInlineRichHtml(item)}</li>`).join('')}</ol>`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (/^\s*>\s?/.test(line)) {
|
|
53
|
+
const quoteLines = [];
|
|
54
|
+
while (index < lines.length && /^\s*>\s?/.test(lines[index])) {
|
|
55
|
+
quoteLines.push(lines[index].replace(/^\s*>\s?/, ''));
|
|
56
|
+
index += 1;
|
|
57
|
+
}
|
|
58
|
+
blocks.push(`<blockquote>${quoteLines.map(renderInlineRichHtml).join('<br>')}</blockquote>`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const paragraphLines = [line.trimEnd()];
|
|
62
|
+
index += 1;
|
|
63
|
+
while (index < lines.length
|
|
64
|
+
&& lines[index].trim()
|
|
65
|
+
&& !/^```/.test(lines[index])
|
|
66
|
+
&& !/^(#{1,4})\s+/.test(lines[index])
|
|
67
|
+
&& !/^\s*[-*+]\s+/.test(lines[index])
|
|
68
|
+
&& !/^\s*\d+[.)]\s+/.test(lines[index])
|
|
69
|
+
&& !/^\s*>\s?/.test(lines[index])) {
|
|
70
|
+
paragraphLines.push(lines[index].trimEnd());
|
|
71
|
+
index += 1;
|
|
72
|
+
}
|
|
73
|
+
blocks.push(`<p>${renderInlineRichHtml(paragraphLines.join('\n'))}</p>`);
|
|
74
|
+
}
|
|
75
|
+
return blocks.join('\n') || '<p></p>';
|
|
76
|
+
}
|
|
77
|
+
export function renderInlineRichHtml(markdown) {
|
|
78
|
+
const tokens = [];
|
|
79
|
+
const protectedText = markdown.replace(/`([^`\n]+)`/g, (_match, code) => {
|
|
80
|
+
const token = `{{FOXCLAW_RICH_CODE_${tokens.length}}}`;
|
|
81
|
+
tokens.push(`<code>${escapeTelegramHtml(code)}</code>`);
|
|
82
|
+
return token;
|
|
83
|
+
});
|
|
84
|
+
let html = escapeTelegramHtml(protectedText);
|
|
85
|
+
html = html.replace(/\[([^\]\n]+)\]\(((?:[^()\s]+|\([^)\s]*\))+)\)/g, (_match, label, url) => {
|
|
86
|
+
const normalizedUrl = unescapeHtmlAttribute(url);
|
|
87
|
+
if (!SAFE_LINK_SCHEMES.test(normalizedUrl)) {
|
|
88
|
+
return label;
|
|
89
|
+
}
|
|
90
|
+
return `<a href="${escapeTelegramHtmlAttribute(normalizedUrl)}">${label}</a>`;
|
|
91
|
+
});
|
|
92
|
+
html = html.replace(/\*\*([^*\n][\s\S]*?[^*\n])\*\*/g, '<b>$1</b>');
|
|
93
|
+
html = html.replace(/__([^_\n][\s\S]*?[^_\n])__/g, '<b>$1</b>');
|
|
94
|
+
html = html.replace(/\{\{FOXCLAW_RICH_CODE_(\d+)}}/g, (_match, tokenIndex) => tokens[Number(tokenIndex)] ?? '');
|
|
95
|
+
return html.replaceAll('\n', '<br>');
|
|
96
|
+
}
|
|
97
|
+
function stripListMarker(line, ordered) {
|
|
98
|
+
return ordered
|
|
99
|
+
? line.replace(/^\s*\d+[.)]\s+/, '')
|
|
100
|
+
: line.replace(/^\s*[-*+]\s+/, '');
|
|
101
|
+
}
|
|
102
|
+
function sanitizeCodeLanguage(language) {
|
|
103
|
+
const normalized = language.trim().replace(/[^A-Za-z0-9_+-]/g, '');
|
|
104
|
+
return normalized || undefined;
|
|
105
|
+
}
|
|
106
|
+
function escapeTelegramHtmlAttribute(value) {
|
|
107
|
+
return escapeTelegramHtml(value).replaceAll('"', '"');
|
|
108
|
+
}
|
|
109
|
+
function unescapeHtmlAttribute(value) {
|
|
110
|
+
return value
|
|
111
|
+
.replaceAll('&', '&')
|
|
112
|
+
.replaceAll('<', '<')
|
|
113
|
+
.replaceAll('>', '>')
|
|
114
|
+
.replaceAll('"', '"');
|
|
115
|
+
}
|