@xmanrui/dsh-im 4.22.0 → 4.23.0

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 (137) hide show
  1. package/README.en.md +3 -3
  2. package/README.md +3 -3
  3. package/lib/client.js +2505 -1152
  4. package/lib/index.js +290 -299
  5. package/package.json +8 -1
  6. package/plugin-src/client/channel-card-meta.js +2 -0
  7. package/plugin-src/client/channel-logos.js +11 -0
  8. package/plugin-src/client/channels/dingtalk/api.js +5 -1
  9. package/plugin-src/client/channels/dingtalk/index.js +15 -13
  10. package/plugin-src/client/channels/email/api.js +52 -0
  11. package/plugin-src/client/channels/email/index.js +728 -0
  12. package/plugin-src/client/channels/email/styles.js +51 -0
  13. package/plugin-src/client/channels/feishu/api.js +4 -2
  14. package/plugin-src/client/channels/feishu/index.js +16 -3
  15. package/plugin-src/client/channels/imessage/index.js +2 -1
  16. package/plugin-src/client/channels/office/api.js +2 -0
  17. package/plugin-src/client/channels/office/index.js +6 -5
  18. package/plugin-src/client/channels/qq/api.js +7 -0
  19. package/plugin-src/client/channels/qq/index.js +16 -3
  20. package/plugin-src/client/channels/shared/token-api.js +5 -1
  21. package/plugin-src/client/channels/shared/token-channel.js +28 -3
  22. package/plugin-src/client/channels/slack/index.js +2 -1
  23. package/plugin-src/client/channels/wecom/api.js +7 -1
  24. package/plugin-src/client/channels/wecom/index.js +16 -3
  25. package/plugin-src/client/channels/wecom-app/api.js +6 -1
  26. package/plugin-src/client/channels/wecom-app/index.js +16 -3
  27. package/plugin-src/client/channels/weixin/api.js +2 -1
  28. package/plugin-src/client/channels/weixin/connection-error.js +1 -71
  29. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  30. package/plugin-src/client/channels/whatsapp/index.js +16 -3
  31. package/plugin-src/client/connection-error.js +87 -0
  32. package/plugin-src/client/global-settings.js +81 -1
  33. package/plugin-src/client/i18n.js +61 -0
  34. package/plugin-src/client/index.js +30 -2
  35. package/plugin-src/client/last-message-error.js +2 -1
  36. package/plugin-src/client/styles.js +6 -0
  37. package/plugin-src/host/build.mjs +4 -0
  38. package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
  39. package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
  40. package/plugin-src/host/channels/email/availability.mjs +60 -0
  41. package/plugin-src/host/channels/email/index.mjs +36 -0
  42. package/plugin-src/host/channels/email/production.mjs +58 -0
  43. package/plugin-src/host/channels/email/rpc.mjs +163 -0
  44. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  45. package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
  46. package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
  47. package/plugin-src/host/channels/office/rpc.mjs +3 -1
  48. package/plugin-src/host/channels/qq/production.mjs +2 -0
  49. package/plugin-src/host/channels/qq/rpc.mjs +5 -3
  50. package/plugin-src/host/channels/shared/production.mjs +52 -5
  51. package/plugin-src/host/channels/shared/rpc.mjs +5 -3
  52. package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
  53. package/plugin-src/host/channels/shared/startup.mjs +11 -6
  54. package/plugin-src/host/channels/slack/production.mjs +2 -0
  55. package/plugin-src/host/channels/slack/rpc.mjs +5 -3
  56. package/plugin-src/host/channels/wecom/production.mjs +2 -0
  57. package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
  58. package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
  59. package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
  60. package/plugin-src/host/channels/weixin/production.mjs +2 -0
  61. package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
  62. package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
  63. package/plugin-src/host/delivery-adapter.mjs +11 -0
  64. package/plugin-src/host/image-input-rpc.mjs +24 -0
  65. package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
  66. package/plugin-src/host/index.mjs +3 -0
  67. package/plugin-src/management-rpc.mjs +12 -2
  68. package/scripts/verify-package.mjs +3 -1
  69. package/src/channels/dingtalk/connection-error.mjs +5 -12
  70. package/src/channels/dingtalk/device-auth.mjs +4 -1
  71. package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
  72. package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
  73. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
  74. package/src/channels/discord/discord-api.mjs +3 -3
  75. package/src/channels/discord/discord-runtime.mjs +18 -12
  76. package/src/channels/email/config-store.mjs +194 -0
  77. package/src/channels/email/email-api.mjs +20 -0
  78. package/src/channels/email/email-bridge.mjs +17 -0
  79. package/src/channels/email/email-controller.mjs +824 -0
  80. package/src/channels/email/email-runtime.mjs +692 -0
  81. package/src/channels/email/harness-client.mjs +7 -0
  82. package/src/channels/email/mail-format.mjs +91 -0
  83. package/src/channels/email/state-store.mjs +176 -0
  84. package/src/channels/email/transport.mjs +64 -0
  85. package/src/channels/email/transports/agent-mail.mjs +593 -0
  86. package/src/channels/email/transports/agently-cli.mjs +288 -0
  87. package/src/channels/email/transports/imap-smtp.mjs +165 -0
  88. package/src/channels/feishu/bridge.mjs +8 -7
  89. package/src/channels/feishu/feishu-runtime.mjs +15 -8
  90. package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
  91. package/src/channels/feishu/message-utils.mjs +3 -2
  92. package/src/channels/feishu/multi-bot-controller.mjs +56 -47
  93. package/src/channels/feishu/registration-manager.mjs +8 -4
  94. package/src/channels/feishu/repair-manager.mjs +2 -1
  95. package/src/channels/imessage/runtime.mjs +9 -5
  96. package/src/channels/office/office-controller.mjs +23 -12
  97. package/src/channels/office/office-runtime.mjs +6 -3
  98. package/src/channels/office/office-transport.mjs +1 -0
  99. package/src/channels/qq/qq-bridge.mjs +7 -5
  100. package/src/channels/qq/qq-controller.mjs +56 -42
  101. package/src/channels/qq/qq-runtime.mjs +16 -11
  102. package/src/channels/shared/bot-workspace-store.mjs +29 -10
  103. package/src/channels/shared/connection-error.mjs +191 -0
  104. package/src/channels/shared/connection-test.mjs +3 -1
  105. package/src/channels/shared/conversation-state-store.mjs +46 -1
  106. package/src/channels/shared/diagnostic-details.mjs +95 -0
  107. package/src/channels/shared/harness-client.mjs +63 -18
  108. package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
  109. package/src/channels/shared/i18n-en/email.mjs +25 -0
  110. package/src/channels/shared/i18n-en/image-input.mjs +17 -0
  111. package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
  112. package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
  113. package/src/channels/shared/i18n-en.mjs +6 -0
  114. package/src/channels/shared/image-input-policy.mjs +35 -0
  115. package/src/channels/shared/image-input-settings-store.mjs +60 -0
  116. package/src/channels/shared/image-input.mjs +124 -0
  117. package/src/channels/shared/image-prompt.mjs +15 -7
  118. package/src/channels/shared/message-failure.mjs +12 -0
  119. package/src/channels/shared/semantic/reply-reference.mjs +3 -3
  120. package/src/channels/shared/text-harness-bridge.mjs +90 -11
  121. package/src/channels/shared/token-bot-controller.mjs +52 -47
  122. package/src/channels/slack/slack-api.mjs +3 -3
  123. package/src/channels/slack/slack-controller.mjs +59 -51
  124. package/src/channels/slack/slack-runtime.mjs +18 -12
  125. package/src/channels/telegram/telegram-api.mjs +2 -2
  126. package/src/channels/telegram/telegram-runtime.mjs +17 -13
  127. package/src/channels/wecom/wecom-bridge.mjs +15 -6
  128. package/src/channels/wecom/wecom-controller.mjs +51 -37
  129. package/src/channels/wecom/wecom-runtime.mjs +10 -6
  130. package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
  131. package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
  132. package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
  133. package/src/channels/weixin/connection-error.mjs +26 -92
  134. package/src/channels/weixin/diagnostic-details.mjs +1 -63
  135. package/src/channels/weixin/weixin-bridge.mjs +7 -5
  136. package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
  137. package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Shared mail formatting and thread helpers.
3
+ *
4
+ * These are transport-independent: quote stripping, thread resolution, address
5
+ * normalization, and the TLS-mode derivation used by the IMAP/SMTP transport.
6
+ */
7
+
8
+ /** Reply text longer than this is truncated so it survives mail gateways. */
9
+ const MAX_REPLY_CHARS = 100_000;
10
+
11
+ /**
12
+ * Quoted-history markers. Mail clients append the whole prior conversation to
13
+ * every reply; leaving it in place would grow the prompt each turn and confuse
14
+ * the model, so the body is cut at the first marker.
15
+ */
16
+ const QUOTE_MARKERS = [
17
+ /^On .{10,120} wrote:$/mi,
18
+ /^-{2,}\s*Original Message\s*-{2,}$/mi,
19
+ /^在 .{4,60}(写道|寫道)[::]?\s*$/mi,
20
+ /^-{2,}\s*原始邮件\s*-{2,}$/mi,
21
+ /^\s*_{10,}\s*$/m,
22
+ /^From:\s.+$/mi,
23
+ /^发件人[::]\s*.+$/mi,
24
+ ];
25
+
26
+ /** Cut a mail body at the first quoted-history marker. */
27
+ export function stripQuotedHistory(text) {
28
+ const body = String(text ?? '').replace(/\r\n/g, '\n');
29
+ let cut = body.length;
30
+ for (const marker of QUOTE_MARKERS) {
31
+ const match = marker.exec(body);
32
+ if (match && match.index > 0 && match.index < cut) cut = match.index;
33
+ }
34
+ return body
35
+ .slice(0, cut)
36
+ .split('\n')
37
+ .filter((line) => !/^\s*>/.test(line))
38
+ .join('\n')
39
+ .replace(/\n{3,}/g, '\n\n')
40
+ .trim();
41
+ }
42
+
43
+ /**
44
+ * Derive the transport security mode from the port, which is how mail clients
45
+ * decide in practice: 465 is implicit TLS, 587 and 25 negotiate STARTTLS, and
46
+ * anything else is left to the explicit `secure` flag. Treating 587 as
47
+ * implicit TLS is the classic misconfiguration that makes SMTP hang.
48
+ */
49
+ export function smtpSecurity(port, secure) {
50
+ if (secure === true) return { secure: true };
51
+ if (secure === false) return { secure: false, requireTLS: true };
52
+ if (port === 465) return { secure: true };
53
+ if (port === 587 || port === 25 || port === 2525) return { secure: false, requireTLS: true };
54
+ return { secure: false };
55
+ }
56
+
57
+ /** IMAP uses implicit TLS on 993 and STARTTLS otherwise. */
58
+ export function imapSecurity(port, secure) {
59
+ if (secure !== undefined) return { secure: secure !== false };
60
+ return { secure: port === 993 };
61
+ }
62
+
63
+ /** Collect every message id from a header value (References / In-Reply-To). */
64
+ export function parseMessageIds(value) {
65
+ if (!value) return [];
66
+ const raw = Array.isArray(value) ? value.join(' ') : String(value);
67
+ return (raw.match(/<[^<>@\s]+@[^<>\s]+>/g) ?? []).map((id) => id.trim());
68
+ }
69
+
70
+ /**
71
+ * Resolve the thread key for a message: an existing conversation is reused when
72
+ * the reply chain points at a message we have seen before, otherwise the
73
+ * message starts a new conversation.
74
+ */
75
+ export function resolveThreadKey({ messageId, references = [], inReplyTo = [], conversationMap }) {
76
+ const chain = [...parseMessageIds(references), ...parseMessageIds(inReplyTo)];
77
+ for (let index = chain.length - 1; index >= 0; index -= 1) {
78
+ const key = conversationMap.get(chain[index]);
79
+ if (key) return key;
80
+ }
81
+ return parseMessageIds(messageId)[0] ?? `email:${Date.now()}`;
82
+ }
83
+
84
+ /** Lowercase a bare address, keeping only the addr-spec part. */
85
+ export function normalizeAddress(value) {
86
+ const text = String(value ?? '').trim().toLowerCase();
87
+ const angled = /<([^<>]+)>/.exec(text);
88
+ return (angled ? angled[1] : text).trim();
89
+ }
90
+
91
+ export { MAX_REPLY_CHARS };
@@ -0,0 +1,176 @@
1
+ import { ConversationStateStore } from '../shared/conversation-state-store.mjs';
2
+
3
+ /**
4
+ * Email adds one need on top of the shared conversation store: mail threads are
5
+ * identified by Message-ID headers, so the inbound chain (References /
6
+ * In-Reply-To) must resolve back to the conversation key we already created.
7
+ * The map is bounded — only the most recent ids are kept, since a thread that
8
+ * has been quiet for thousands of messages is not worth unbounded growth.
9
+ */
10
+ const MAX_THREAD_IDS = 2_000;
11
+
12
+ /** A conversation key that pins the chat to one existing Harness session. */
13
+ export const BOUND_KEY_PREFIX = 'bound:';
14
+
15
+ export class EmailStateStore extends ConversationStateStore {
16
+ // Loaded from persisted state on first use. Held in memory because threading
17
+ // reads it synchronously, and written back on every change so a restart does
18
+ // not turn a continuing thread into a brand-new conversation.
19
+ #threadIds = null;
20
+
21
+ /** The Message-ID → conversation map, loaded once from persisted state. */
22
+ #threads() {
23
+ if (this.#threadIds) return this.#threadIds;
24
+ const stored = this.extensionState().threadIds;
25
+ const map = new Map();
26
+ if (stored && typeof stored === 'object' && !Array.isArray(stored)) {
27
+ for (const [id, key] of Object.entries(stored)) {
28
+ if (typeof id === 'string' && id && typeof key === 'string' && key) map.set(id, key);
29
+ }
30
+ }
31
+ this.#threadIds = map;
32
+ return map;
33
+ }
34
+
35
+ /** Record that `messageId` belongs to `conversationKey`. */
36
+ rememberThreadId(messageId, conversationKey) {
37
+ if (typeof messageId !== 'string' || !messageId || typeof conversationKey !== 'string') return;
38
+ const threads = this.#threads();
39
+ // Re-insert so the freshest ids survive eviction.
40
+ threads.delete(messageId);
41
+ threads.set(messageId, conversationKey);
42
+ while (threads.size > MAX_THREAD_IDS) {
43
+ threads.delete(threads.keys().next().value);
44
+ }
45
+ // Persisted, so a reply arriving after a restart still joins its thread.
46
+ this.extensionState().threadIds = Object.fromEntries(threads);
47
+ void this.persist();
48
+ }
49
+
50
+ /** Conversation key previously associated with this Message-ID, if any. */
51
+ conversationForThreadId(messageId) {
52
+ return this.#threads().get(messageId) ?? null;
53
+ }
54
+
55
+ /** Read-only view used by the threading resolver. */
56
+ get threadMap() {
57
+ return this.#threads();
58
+ }
59
+
60
+ /**
61
+ * A QR authorization in flight. Persisted because the code stays valid for
62
+ * ten minutes, which easily outlives a plugin reload — losing it meant an
63
+ * already-completed scan could never be redeemed.
64
+ */
65
+ pendingAuth() {
66
+ const value = this.extensionState().pendingAuth;
67
+ if (!value || typeof value !== 'object') return null;
68
+ if (typeof value.pollUrl !== 'string' || !value.pollUrl) return null;
69
+ if (!Number.isFinite(value.expiresAt)) return null;
70
+ return { pollUrl: value.pollUrl, expiresAt: value.expiresAt, transport: value.transport };
71
+ }
72
+
73
+ async setPendingAuth(value) {
74
+ if (!value) {
75
+ delete this.extensionState().pendingAuth;
76
+ } else {
77
+ this.extensionState().pendingAuth = {
78
+ pollUrl: String(value.pollUrl ?? ''),
79
+ expiresAt: Number(value.expiresAt ?? 0),
80
+ ...(value.transport ? { transport: String(value.transport) } : {}),
81
+ };
82
+ }
83
+ await this.persist();
84
+ }
85
+
86
+ /**
87
+ * Fixed-session bindings, both scopes in one document:
88
+ * { account: <sessionId|null>, senders: { <address>: <sessionId> } }
89
+ * A sender entry wins over the account default; with neither set the
90
+ * conversation starts a fresh session per thread (the original behaviour).
91
+ */
92
+ emailBindings() {
93
+ const bindings = this.extensionState().emailBindings;
94
+ if (!bindings || typeof bindings !== 'object') return { account: null, senders: {} };
95
+ const senders = {};
96
+ if (bindings.senders && typeof bindings.senders === 'object' && !Array.isArray(bindings.senders)) {
97
+ for (const [address, sessionId] of Object.entries(bindings.senders)) {
98
+ if (typeof address === 'string' && address
99
+ && typeof sessionId === 'string' && sessionId) senders[address] = sessionId;
100
+ }
101
+ }
102
+ return {
103
+ account: typeof bindings.account === 'string' && bindings.account ? bindings.account : null,
104
+ senders,
105
+ };
106
+ }
107
+
108
+ async setEmailBindings(value) {
109
+ const next = value && typeof value === 'object' ? value : {};
110
+ const senders = {};
111
+ if (next.senders && typeof next.senders === 'object' && !Array.isArray(next.senders)) {
112
+ for (const [address, sessionId] of Object.entries(next.senders)) {
113
+ if (typeof address === 'string' && address
114
+ && typeof sessionId === 'string' && sessionId) senders[address] = sessionId;
115
+ }
116
+ }
117
+ this.extensionState().emailBindings = {
118
+ account: typeof next.account === 'string' && next.account ? next.account : null,
119
+ senders,
120
+ };
121
+ await this.persist();
122
+ return this.emailBindings();
123
+ }
124
+
125
+ /**
126
+ * Resolve the session a message should use, honouring the binding order:
127
+ * a sender-specific binding, then the account-wide binding, then null which
128
+ * means "start a new session for this thread".
129
+ */
130
+ boundSessionFor(senderAddress) {
131
+ const { account, senders } = this.emailBindings();
132
+ const address = typeof senderAddress === 'string' ? senderAddress.trim().toLowerCase() : '';
133
+ if (address && senders[address]) return senders[address];
134
+ return account;
135
+ }
136
+
137
+ /**
138
+ * Mail cursors are not always numbers: IMAP addresses messages by an integer
139
+ * UID, while the Agent mailbox pages by an opaque string id. The shared store
140
+ * only accepts integers, so the mailbox keeps its own cursor and returns it
141
+ * through the inherited accessor.
142
+ */
143
+ cursor() {
144
+ const value = this.extensionState().mailCursor;
145
+ return value === undefined ? null : value;
146
+ }
147
+
148
+ async setCursor(cursor) {
149
+ if (typeof cursor !== 'string' && !Number.isSafeInteger(cursor)) {
150
+ throw new TypeError('Invalid update cursor');
151
+ }
152
+ if (typeof cursor === 'string' && cursor === '') {
153
+ throw new TypeError('Invalid update cursor');
154
+ }
155
+ this.extensionState().mailCursor = cursor;
156
+ await this.persist();
157
+ }
158
+
159
+ /**
160
+ * A bound conversation key maps straight to the pinned session. Without this,
161
+ * the resolver would miss the mapping and create a brand-new session even
162
+ * though the user asked for a fixed one. The bridge prefixes the key with the
163
+ * chat kind (`direct:`), so the marker is searched rather than assumed to be
164
+ * at the start.
165
+ */
166
+ sessionFor(key) {
167
+ if (typeof key === 'string') {
168
+ const marker = key.indexOf(BOUND_KEY_PREFIX);
169
+ if (marker !== -1) {
170
+ const sessionId = key.slice(marker + BOUND_KEY_PREFIX.length);
171
+ if (sessionId) return sessionId;
172
+ }
173
+ }
174
+ return super.sessionFor(key);
175
+ }
176
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Mailbox transport contract.
3
+ *
4
+ * A transport moves mail; it owns nothing else. Conversation mapping, the
5
+ * sender allowlist, quote stripping, session binding, and loop prevention all
6
+ * live in the runtime and are shared by every transport — so adding a mail
7
+ * protocol means writing one adapter here, not another channel.
8
+ *
9
+ * Implementations:
10
+ * imap-smtp any standard mailbox, polled over IMAP and sent over SMTP
11
+ * agent-mail Tencent Agent Mail (agent.qq.com), HTTP + OAuth, long-polled
12
+ *
13
+ * Required methods:
14
+ * connect() open the underlying connection
15
+ * disconnect() close it; must be safe to call twice
16
+ * latestUid() highest cursor value currently present
17
+ * listMessages({ afterUid, limit, allowSenders })
18
+ * messages newer than the cursor, already
19
+ * filtered to allowlisted senders when a
20
+ * set is supplied
21
+ * sendReply({ to, subject, text, inReplyTo, references, attachments })
22
+ * send inside the originating conversation
23
+ * sendText({ to, subject, text, attachments })
24
+ * send a standalone message
25
+ *
26
+ * A message returned by listMessages() is a mailparser-shaped object with a
27
+ * `uid` added:
28
+ * { uid, messageId, from, to, cc, subject, text, html, attachments, headers }
29
+ * `attachments[]` entries carry { filename, contentType, size, content }.
30
+ */
31
+
32
+ /** Methods every transport must provide. */
33
+ export const TRANSPORT_METHODS = Object.freeze([
34
+ 'connect',
35
+ 'disconnect',
36
+ 'latestUid',
37
+ 'listMessages',
38
+ 'sendReply',
39
+ 'sendText',
40
+ ]);
41
+
42
+ /** Default reply subject: one "Re:" prefix so threads stay grouped. */
43
+ export function replySubject(subject) {
44
+ const text = String(subject ?? '').trim();
45
+ if (!text) return 'Re: (no subject)';
46
+ return /^re:/i.test(text) ? text : `Re: ${text}`;
47
+ }
48
+
49
+ /**
50
+ * Assert that a value satisfies the transport contract. Called when a
51
+ * transport is constructed so a malformed adapter fails at the boundary
52
+ * rather than part-way through a poll.
53
+ */
54
+ export function assertTransport(transport, label = 'transport') {
55
+ if (!transport || typeof transport !== 'object') {
56
+ throw new TypeError(`${label} must be an object`);
57
+ }
58
+ for (const method of TRANSPORT_METHODS) {
59
+ if (typeof transport[method] !== 'function') {
60
+ throw new TypeError(`${label} must implement ${method}()`);
61
+ }
62
+ }
63
+ return transport;
64
+ }