@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.
Files changed (41) hide show
  1. package/README.en.md +7 -2
  2. package/README.md +7 -2
  3. package/lib/client.js +498 -347
  4. package/lib/index.js +284 -283
  5. package/package.json +11 -2
  6. package/plugin-src/client/channel-card-meta.js +3 -9
  7. package/plugin-src/client/channels/dingtalk/index.js +9 -8
  8. package/plugin-src/client/channels/feishu/index.js +21 -17
  9. package/plugin-src/client/channels/qq/index.js +9 -8
  10. package/plugin-src/client/channels/shared/collapsible-account.js +49 -26
  11. package/plugin-src/client/channels/shared/token-api.js +3 -0
  12. package/plugin-src/client/channels/shared/token-channel.js +9 -8
  13. package/plugin-src/client/channels/telegram/index.js +3 -0
  14. package/plugin-src/client/channels/telegram/styles.js +12 -0
  15. package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
  16. package/plugin-src/client/channels/wecom/index.js +9 -8
  17. package/plugin-src/client/channels/wecom-app/index.js +9 -8
  18. package/plugin-src/client/channels/weixin/index.js +9 -8
  19. package/plugin-src/client/channels/whatsapp/index.js +9 -8
  20. package/plugin-src/client/i18n.js +13 -1
  21. package/plugin-src/client/index.js +10 -2
  22. package/plugin-src/client/styles.js +12 -8
  23. package/plugin-src/client/update-panel.js +18 -8
  24. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  25. package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
  26. package/plugin-src/host/modern-harness-api.mjs +7 -2
  27. package/plugin-src/host/update-service.mjs +19 -14
  28. package/scripts/verify-package.mjs +11 -5
  29. package/src/channels/email/email-runtime.mjs +9 -2
  30. package/src/channels/email/transports/agent-mail.mjs +14 -3
  31. package/src/channels/feishu/bridge.mjs +122 -46
  32. package/src/channels/feishu/feishu-channel.mjs +35 -0
  33. package/src/channels/feishu/live-cot.mjs +260 -0
  34. package/src/channels/feishu/slash-command-registry.mjs +17 -0
  35. package/src/channels/feishu/step-push-mode.mjs +10 -4
  36. package/src/channels/shared/harness-client.mjs +224 -39
  37. package/src/channels/shared/text-harness-bridge.mjs +60 -14
  38. package/src/channels/shared/workspace-session.mjs +7 -1
  39. package/src/channels/telegram/config-store.mjs +4 -1
  40. package/src/channels/telegram/telegram-controller.mjs +13 -1
  41. 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, splitMessageText } from '../shared/editable-message-stream.mjs';
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,