@xmanrui/dsh-im 4.23.0 → 4.24.1
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/README.en.md +7 -2
- package/README.md +7 -2
- package/lib/client.js +498 -347
- package/lib/index.js +284 -283
- package/package.json +11 -2
- package/plugin-src/client/channel-card-meta.js +3 -9
- package/plugin-src/client/channels/dingtalk/index.js +9 -8
- package/plugin-src/client/channels/feishu/index.js +21 -17
- package/plugin-src/client/channels/qq/index.js +9 -8
- package/plugin-src/client/channels/shared/collapsible-account.js +49 -26
- package/plugin-src/client/channels/shared/token-api.js +3 -0
- package/plugin-src/client/channels/shared/token-channel.js +9 -8
- package/plugin-src/client/channels/telegram/index.js +3 -0
- package/plugin-src/client/channels/telegram/styles.js +12 -0
- package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
- package/plugin-src/client/channels/wecom/index.js +9 -8
- package/plugin-src/client/channels/wecom-app/index.js +9 -8
- package/plugin-src/client/channels/weixin/index.js +9 -8
- package/plugin-src/client/channels/whatsapp/index.js +9 -8
- package/plugin-src/client/i18n.js +13 -1
- package/plugin-src/client/index.js +10 -2
- package/plugin-src/client/styles.js +12 -8
- package/plugin-src/client/update-panel.js +18 -8
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
- package/plugin-src/host/modern-harness-api.mjs +7 -2
- package/plugin-src/host/update-service.mjs +19 -14
- package/scripts/verify-package.mjs +11 -5
- package/src/channels/email/email-runtime.mjs +9 -2
- package/src/channels/email/transports/agent-mail.mjs +14 -3
- package/src/channels/feishu/bridge.mjs +122 -46
- package/src/channels/feishu/feishu-channel.mjs +35 -0
- package/src/channels/feishu/live-cot.mjs +260 -0
- package/src/channels/feishu/slash-command-registry.mjs +17 -0
- package/src/channels/feishu/step-push-mode.mjs +10 -4
- package/src/channels/shared/harness-client.mjs +224 -39
- package/src/channels/shared/text-harness-bridge.mjs +60 -14
- package/src/channels/shared/workspace-session.mjs +7 -1
- package/src/channels/telegram/config-store.mjs +4 -1
- package/src/channels/telegram/telegram-controller.mjs +13 -1
- package/src/channels/telegram/telegram-runtime.mjs +198 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { extractConnectionEvidence, createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
|
|
2
2
|
import { randomInt } from 'node:crypto';
|
|
3
3
|
|
|
4
|
-
import { createEditableMessageStream
|
|
4
|
+
import { createEditableMessageStream } from '../shared/editable-message-stream.mjs';
|
|
5
5
|
import { createTextDeliveryBlock } from '../shared/semantic/delivery.mjs';
|
|
6
6
|
import { t } from '../shared/i18n.mjs';
|
|
7
7
|
import { captureContextEnhancement } from '../shared/context-enhancement.mjs';
|
|
@@ -499,6 +499,65 @@ class TelegramDeliveryStream {
|
|
|
499
499
|
}
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
+
// Thinking-trace display rules. Truncation fallback (spoiler / inline-button
|
|
503
|
+
// expansion are follow-ups gated on three-platform client verification):
|
|
504
|
+
// tool traces cap the argument summary, reasoning caps the first paragraph.
|
|
505
|
+
const TOOL_TRACE_ARG_KEYS = Object.freeze([
|
|
506
|
+
'command', 'pattern', 'query', 'url', 'file_path', 'path', 'text', 'prompt', 'message', 'description',
|
|
507
|
+
]);
|
|
508
|
+
const TOOL_TRACE_SUMMARY_LIMIT = 120;
|
|
509
|
+
const THINKING_LINE_LIMIT = 200;
|
|
510
|
+
|
|
511
|
+
function truncateCapped(value, limit) {
|
|
512
|
+
return value.length > limit ? `${value.slice(0, limit)}…` : value;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Extract a one-line argument summary from a tool call's `arguments`. */
|
|
516
|
+
export function toolTraceSummary(argumentsValue) {
|
|
517
|
+
let record = null;
|
|
518
|
+
if (argumentsValue && typeof argumentsValue === 'object' && !Array.isArray(argumentsValue)) {
|
|
519
|
+
record = argumentsValue;
|
|
520
|
+
} else if (typeof argumentsValue === 'string') {
|
|
521
|
+
const trimmed = argumentsValue.trim();
|
|
522
|
+
if (!trimmed) return null;
|
|
523
|
+
let parsed = null;
|
|
524
|
+
try {
|
|
525
|
+
parsed = JSON.parse(trimmed);
|
|
526
|
+
} catch {
|
|
527
|
+
return truncateCapped(trimmed.replace(/\s+/g, ' ').trim(), TOOL_TRACE_SUMMARY_LIMIT);
|
|
528
|
+
}
|
|
529
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
530
|
+
record = parsed;
|
|
531
|
+
} else {
|
|
532
|
+
return truncateCapped(trimmed.replace(/\s+/g, ' ').trim(), TOOL_TRACE_SUMMARY_LIMIT);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (!record) return null;
|
|
536
|
+
for (const key of TOOL_TRACE_ARG_KEYS) {
|
|
537
|
+
const value = record[key];
|
|
538
|
+
if (typeof value === 'string' && value.trim()) {
|
|
539
|
+
return truncateCapped(value.replace(/\s+/g, ' ').trim(), TOOL_TRACE_SUMMARY_LIMIT);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Format one tool-trace line: `🔧 <name> → <summary>` (summary optional). */
|
|
546
|
+
export function formatToolTrace(name, argumentsValue) {
|
|
547
|
+
const toolName = typeof name === 'string' && name.trim() ? name.trim() : t('工具');
|
|
548
|
+
const summary = toolTraceSummary(argumentsValue);
|
|
549
|
+
return summary ? `🔧 ${toolName} → ${summary}` : `🔧 ${toolName}`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Format one reasoning line: `💭 <first paragraph>` (200-char cap). */
|
|
553
|
+
export function formatThinkingLine(text) {
|
|
554
|
+
const trimmed = typeof text === 'string' ? text.trim() : '';
|
|
555
|
+
if (!trimmed) return null;
|
|
556
|
+
const firstParagraph = trimmed.split(/\n{2,}/)[0].trim();
|
|
557
|
+
if (!firstParagraph) return null;
|
|
558
|
+
return `💭 ${truncateCapped(firstParagraph, THINKING_LINE_LIMIT)}`;
|
|
559
|
+
}
|
|
560
|
+
|
|
502
561
|
export class TelegramBotClient {
|
|
503
562
|
#api;
|
|
504
563
|
#signal;
|
|
@@ -904,6 +963,142 @@ export class TelegramBotClient {
|
|
|
904
963
|
});
|
|
905
964
|
return stream.start();
|
|
906
965
|
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Thinking-trace stream: every intermediate line is a permanent, separate
|
|
969
|
+
* Telegram message (no placeholder editing), so the user watches the
|
|
970
|
+
* "think → act" chain build up. Trace lines are best-effort; the final
|
|
971
|
+
* answer must still land, falling back to plain sendText delivery.
|
|
972
|
+
*/
|
|
973
|
+
openThinkingStream(target) {
|
|
974
|
+
const client = this;
|
|
975
|
+
const providerMessageIds = [];
|
|
976
|
+
let firstMessageId = null;
|
|
977
|
+
let closed = false;
|
|
978
|
+
const logger = this.#logger;
|
|
979
|
+
const warnFailure = (label, error) => {
|
|
980
|
+
logger?.warn?.(`[dsh-im:telegram] thinking stream ${label} failed:`, error);
|
|
981
|
+
};
|
|
982
|
+
const sendLine = async (text) => {
|
|
983
|
+
const message = await this.#api.sendMessage({
|
|
984
|
+
chatId: target.chatId,
|
|
985
|
+
text,
|
|
986
|
+
replyToMessageId: firstMessageId === null ? target.replyToMessageId : undefined,
|
|
987
|
+
messageThreadId: target.messageThreadId,
|
|
988
|
+
signal: this.#signal,
|
|
989
|
+
});
|
|
990
|
+
const id = message?.message_id;
|
|
991
|
+
if (Number.isSafeInteger(id)) {
|
|
992
|
+
if (firstMessageId === null) firstMessageId = id;
|
|
993
|
+
providerMessageIds.push(String(id));
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
return {
|
|
997
|
+
get messageId() {
|
|
998
|
+
return firstMessageId;
|
|
999
|
+
},
|
|
1000
|
+
get providerMessageIds() {
|
|
1001
|
+
return [...providerMessageIds];
|
|
1002
|
+
},
|
|
1003
|
+
presentation: 'telegram-thinking',
|
|
1004
|
+
// The chain is made of permanent messages, but long tool runs still
|
|
1005
|
+
// want the typing indicator; the bridge re-sends it every 4 seconds.
|
|
1006
|
+
keepalive: true,
|
|
1007
|
+
update: async () => {},
|
|
1008
|
+
refresh: async () => {},
|
|
1009
|
+
sendLine,
|
|
1010
|
+
sendToolTrace: (name, argumentsValue) => (async () => {
|
|
1011
|
+
if (closed) return;
|
|
1012
|
+
try {
|
|
1013
|
+
await sendLine(formatToolTrace(name, argumentsValue));
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
warnFailure('tool trace', error);
|
|
1016
|
+
}
|
|
1017
|
+
})(),
|
|
1018
|
+
sendThinking: (text) => (async () => {
|
|
1019
|
+
if (closed) return;
|
|
1020
|
+
const line = formatThinkingLine(text);
|
|
1021
|
+
if (!line) return;
|
|
1022
|
+
try {
|
|
1023
|
+
await sendLine(line);
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
warnFailure('thinking line', error);
|
|
1026
|
+
}
|
|
1027
|
+
})(),
|
|
1028
|
+
async finish(answer) {
|
|
1029
|
+
if (closed) throw new Error('Message stream is already closed');
|
|
1030
|
+
closed = true;
|
|
1031
|
+
const format = answer && typeof answer === 'object' && answer.format === 'markdown'
|
|
1032
|
+
? 'markdown'
|
|
1033
|
+
: 'plain';
|
|
1034
|
+
const text = answer && typeof answer === 'object' && typeof answer.text === 'string'
|
|
1035
|
+
? answer.text
|
|
1036
|
+
: typeof answer === 'string' ? answer : '';
|
|
1037
|
+
const trimmed = text.trim();
|
|
1038
|
+
const answerText = trimmed || t('处理完成。');
|
|
1039
|
+
// The final answer keeps the channel's normal delivery path, so a
|
|
1040
|
+
// markdown answer renders rich (bold, code fences) exactly like
|
|
1041
|
+
// non-trace mode; trace lines above stay plain permanent messages.
|
|
1042
|
+
// Markdown goes to #sendRich in one shot: splitTelegramRichMarkdown
|
|
1043
|
+
// is fence-aware, so code blocks with inner blank lines stay intact
|
|
1044
|
+
// and no blank line is rewritten. A blank-line pre-split would break
|
|
1045
|
+
// both: unfinished fences fall back to plain, and rejoining with a
|
|
1046
|
+
// fixed separator collapses \n{3,} inside multi-line strings. Plain
|
|
1047
|
+
// answers use the whitespace-preserving regular-text splitter.
|
|
1048
|
+
const chunks = format === 'markdown'
|
|
1049
|
+
? [answerText]
|
|
1050
|
+
: splitTelegramRegularText(answerText);
|
|
1051
|
+
const remember = (ids) => {
|
|
1052
|
+
for (const id of ids ?? []) {
|
|
1053
|
+
if (!providerMessageIds.includes(id)) providerMessageIds.push(id);
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
1057
|
+
let result;
|
|
1058
|
+
try {
|
|
1059
|
+
result = await client.#sendRich(target, createTextDeliveryBlock(chunks[index], format));
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
const failure = telegramFailure(error);
|
|
1062
|
+
warnFailure('final answer', error);
|
|
1063
|
+
if (failure.outcome === 'unknown') {
|
|
1064
|
+
return deliveryResult('telegram-thinking', providerMessageIds, 'unknown', failure.reason);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (result?.deliveryOutcome === 'sent') {
|
|
1068
|
+
remember(result.providerMessageIds);
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
if (result?.deliveryOutcome === 'unknown') {
|
|
1072
|
+
// 结果未知(超时等):可能已送达,重发会造成重复,保留 unknown 状态。
|
|
1073
|
+
remember(result.providerMessageIds);
|
|
1074
|
+
return deliveryResult('telegram-thinking', providerMessageIds, 'unknown', result.reason);
|
|
1075
|
+
}
|
|
1076
|
+
// 明确失败:该分片确认未送达。纯文本分片再走一次 plain 重发
|
|
1077
|
+
// (markdown 分片的 plain 回退已在 #sendRich 内尝试过),只重发
|
|
1078
|
+
// 尚未发送的尾部内容,已成功的分片不重复。
|
|
1079
|
+
remember(result?.providerMessageIds);
|
|
1080
|
+
if (format === 'markdown') {
|
|
1081
|
+
return deliveryResult('telegram-thinking', providerMessageIds, 'failed', result.reason);
|
|
1082
|
+
}
|
|
1083
|
+
warnFailure('final answer chunk', new Error('chunk definitively rejected'));
|
|
1084
|
+
try {
|
|
1085
|
+
// Regular-text chunks already retain the original whitespace.
|
|
1086
|
+
const fallback = await client.sendText(target, chunks.slice(index).join(''));
|
|
1087
|
+
remember(fallback.providerMessageIds);
|
|
1088
|
+
return deliveryResult('telegram-thinking', providerMessageIds);
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
const failure = telegramFailure(error);
|
|
1091
|
+
warnFailure('final answer fallback', error);
|
|
1092
|
+
return deliveryResult('telegram-thinking', providerMessageIds, failure.outcome, failure.reason);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return deliveryResult('telegram-thinking', providerMessageIds);
|
|
1096
|
+
},
|
|
1097
|
+
cancel() {
|
|
1098
|
+
closed = true;
|
|
1099
|
+
},
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
907
1102
|
}
|
|
908
1103
|
|
|
909
1104
|
export function createTelegramRuntimeStatus() {
|
|
@@ -1073,6 +1268,8 @@ export class TelegramRuntime {
|
|
|
1073
1268
|
state: this.#state,
|
|
1074
1269
|
contextEnhancement: this.#contextEnhancement,
|
|
1075
1270
|
accessPolicy: this.#accessPolicy,
|
|
1271
|
+
// Default ON; an explicit false in the bot config opts out.
|
|
1272
|
+
thinkingTraces: this.#config?.thinkingTraces !== false,
|
|
1076
1273
|
status: this.#status,
|
|
1077
1274
|
logger: this.#logger,
|
|
1078
1275
|
replyTimeoutMs: this.#replyTimeoutMs,
|