@xmanrui/dsh-im 4.22.0 → 4.24.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.
- package/README.en.md +5 -4
- package/README.md +5 -4
- package/lib/client.js +2584 -1153
- package/lib/index.js +290 -298
- package/package.json +14 -2
- package/plugin-src/client/channel-card-meta.js +2 -0
- package/plugin-src/client/channel-logos.js +11 -0
- package/plugin-src/client/channels/dingtalk/api.js +5 -1
- package/plugin-src/client/channels/dingtalk/index.js +15 -13
- package/plugin-src/client/channels/email/api.js +52 -0
- package/plugin-src/client/channels/email/index.js +728 -0
- package/plugin-src/client/channels/email/styles.js +51 -0
- package/plugin-src/client/channels/feishu/api.js +4 -2
- package/plugin-src/client/channels/feishu/index.js +23 -7
- package/plugin-src/client/channels/imessage/index.js +2 -1
- package/plugin-src/client/channels/office/api.js +2 -0
- package/plugin-src/client/channels/office/index.js +6 -5
- package/plugin-src/client/channels/qq/api.js +7 -0
- package/plugin-src/client/channels/qq/index.js +16 -3
- package/plugin-src/client/channels/shared/token-api.js +8 -1
- package/plugin-src/client/channels/shared/token-channel.js +28 -3
- package/plugin-src/client/channels/slack/index.js +2 -1
- 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/api.js +7 -1
- package/plugin-src/client/channels/wecom/index.js +16 -3
- package/plugin-src/client/channels/wecom-app/api.js +6 -1
- package/plugin-src/client/channels/wecom-app/index.js +16 -3
- package/plugin-src/client/channels/weixin/api.js +2 -1
- package/plugin-src/client/channels/weixin/connection-error.js +1 -71
- package/plugin-src/client/channels/whatsapp/api.js +5 -0
- package/plugin-src/client/channels/whatsapp/index.js +16 -3
- package/plugin-src/client/connection-error.js +87 -0
- package/plugin-src/client/global-settings.js +81 -1
- package/plugin-src/client/i18n.js +69 -1
- package/plugin-src/client/index.js +40 -4
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/styles.js +8 -2
- package/plugin-src/host/build.mjs +4 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
- package/plugin-src/host/channels/email/availability.mjs +60 -0
- package/plugin-src/host/channels/email/index.mjs +36 -0
- package/plugin-src/host/channels/email/production.mjs +58 -0
- package/plugin-src/host/channels/email/rpc.mjs +163 -0
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
- package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
- package/plugin-src/host/channels/office/rpc.mjs +3 -1
- package/plugin-src/host/channels/qq/production.mjs +2 -0
- package/plugin-src/host/channels/qq/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/production.mjs +52 -5
- package/plugin-src/host/channels/shared/rpc.mjs +14 -3
- package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
- package/plugin-src/host/channels/shared/startup.mjs +11 -6
- package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
- package/plugin-src/host/channels/slack/production.mjs +2 -0
- package/plugin-src/host/channels/slack/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom/production.mjs +2 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
- package/plugin-src/host/channels/weixin/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
- package/plugin-src/host/delivery-adapter.mjs +11 -0
- package/plugin-src/host/image-input-rpc.mjs +24 -0
- package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
- package/plugin-src/host/index.mjs +3 -0
- package/plugin-src/host/modern-harness-api.mjs +7 -2
- package/plugin-src/management-rpc.mjs +12 -2
- package/scripts/verify-package.mjs +13 -5
- package/src/channels/dingtalk/connection-error.mjs +5 -12
- package/src/channels/dingtalk/device-auth.mjs +4 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
- package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
- package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
- package/src/channels/discord/discord-api.mjs +3 -3
- package/src/channels/discord/discord-runtime.mjs +18 -12
- package/src/channels/email/config-store.mjs +194 -0
- package/src/channels/email/email-api.mjs +20 -0
- package/src/channels/email/email-bridge.mjs +17 -0
- package/src/channels/email/email-controller.mjs +824 -0
- package/src/channels/email/email-runtime.mjs +699 -0
- package/src/channels/email/harness-client.mjs +7 -0
- package/src/channels/email/mail-format.mjs +91 -0
- package/src/channels/email/state-store.mjs +176 -0
- package/src/channels/email/transport.mjs +64 -0
- package/src/channels/email/transports/agent-mail.mjs +604 -0
- package/src/channels/email/transports/agently-cli.mjs +288 -0
- package/src/channels/email/transports/imap-smtp.mjs +165 -0
- package/src/channels/feishu/bridge.mjs +39 -11
- package/src/channels/feishu/feishu-channel.mjs +35 -0
- package/src/channels/feishu/feishu-runtime.mjs +15 -8
- package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
- package/src/channels/feishu/live-cot.mjs +260 -0
- package/src/channels/feishu/message-utils.mjs +3 -2
- package/src/channels/feishu/multi-bot-controller.mjs +56 -47
- package/src/channels/feishu/registration-manager.mjs +8 -4
- package/src/channels/feishu/repair-manager.mjs +2 -1
- package/src/channels/feishu/slash-command-registry.mjs +17 -0
- package/src/channels/feishu/step-push-mode.mjs +10 -4
- package/src/channels/imessage/runtime.mjs +9 -5
- package/src/channels/office/office-controller.mjs +23 -12
- package/src/channels/office/office-runtime.mjs +6 -3
- package/src/channels/office/office-transport.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +7 -5
- package/src/channels/qq/qq-controller.mjs +56 -42
- package/src/channels/qq/qq-runtime.mjs +16 -11
- package/src/channels/shared/bot-workspace-store.mjs +29 -10
- package/src/channels/shared/connection-error.mjs +191 -0
- package/src/channels/shared/connection-test.mjs +3 -1
- package/src/channels/shared/conversation-state-store.mjs +46 -1
- package/src/channels/shared/diagnostic-details.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +287 -57
- package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
- package/src/channels/shared/i18n-en/email.mjs +25 -0
- package/src/channels/shared/i18n-en/image-input.mjs +17 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
- package/src/channels/shared/i18n-en.mjs +6 -0
- package/src/channels/shared/image-input-policy.mjs +35 -0
- package/src/channels/shared/image-input-settings-store.mjs +60 -0
- package/src/channels/shared/image-input.mjs +124 -0
- package/src/channels/shared/image-prompt.mjs +15 -7
- package/src/channels/shared/message-failure.mjs +12 -0
- package/src/channels/shared/semantic/reply-reference.mjs +3 -3
- package/src/channels/shared/text-harness-bridge.mjs +150 -25
- package/src/channels/shared/token-bot-controller.mjs +52 -47
- package/src/channels/slack/slack-api.mjs +3 -3
- package/src/channels/slack/slack-controller.mjs +59 -51
- package/src/channels/slack/slack-runtime.mjs +18 -12
- package/src/channels/telegram/config-store.mjs +4 -1
- package/src/channels/telegram/telegram-api.mjs +2 -2
- package/src/channels/telegram/telegram-controller.mjs +13 -1
- package/src/channels/telegram/telegram-runtime.mjs +215 -14
- package/src/channels/wecom/wecom-bridge.mjs +15 -6
- package/src/channels/wecom/wecom-controller.mjs +51 -37
- package/src/channels/wecom/wecom-runtime.mjs +10 -6
- package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
- package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
- package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
- package/src/channels/weixin/connection-error.mjs +26 -92
- package/src/channels/weixin/diagnostic-details.mjs +1 -63
- package/src/channels/weixin/weixin-bridge.mjs +7 -5
- package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
- package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs the official `agently-cli` and returns its JSON.
|
|
3
|
+
*
|
|
4
|
+
* The CLI owns the Agent mailbox protocol, including OAuth and the token
|
|
5
|
+
* refresh. A hand-written client could not refresh: the server issued an
|
|
6
|
+
* access token that worked while the matching refresh token was rejected with
|
|
7
|
+
* `invalid_grant` the moment it was used, so the mailbox died an hour after
|
|
8
|
+
* every authorization. The CLI keeps credentials in the system keychain, tracks
|
|
9
|
+
* `expires_at`, refreshes ahead of expiry, and re-authorizes a watcher that has
|
|
10
|
+
* gone stale — none of which is reproducible from outside.
|
|
11
|
+
*
|
|
12
|
+
* Contract notes learned from the binary and its own schemas:
|
|
13
|
+
* - stdout carries the JSON document; the `tip:` lines go to stderr.
|
|
14
|
+
* - The exit code is 0 even for an error document, so `ok` is the only
|
|
15
|
+
* reliable success signal.
|
|
16
|
+
* - A `--print-output-schema` flag documents every command's fields.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
|
|
25
|
+
/** Platform-specific binaries, mirroring the CLI's own packaging. */
|
|
26
|
+
const PLATFORM_PACKAGES = {
|
|
27
|
+
'darwin-arm64': '@tencent-qqmail/agently-cli-darwin-arm64',
|
|
28
|
+
'darwin-x64': '@tencent-qqmail/agently-cli-darwin-x64',
|
|
29
|
+
'linux-arm64': '@tencent-qqmail/agently-cli-linux-arm64',
|
|
30
|
+
'linux-x64': '@tencent-qqmail/agently-cli-linux-x64',
|
|
31
|
+
'win32-arm64': '@tencent-qqmail/agently-cli-win32-arm64',
|
|
32
|
+
'win32-x64': '@tencent-qqmail/agently-cli-win32-x64',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** How long a single CLI invocation may run before it is abandoned. */
|
|
36
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
37
|
+
|
|
38
|
+
export class AgentMailCliError extends Error {
|
|
39
|
+
constructor(message, { code = 'cli-failed', status = null, detail = null, cause } = {}) {
|
|
40
|
+
super(message, { cause });
|
|
41
|
+
this.name = 'AgentMailCliError';
|
|
42
|
+
this.code = code;
|
|
43
|
+
if (status !== null) this.status = status;
|
|
44
|
+
if (detail !== null) this.detail = detail;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the CLI binary.
|
|
50
|
+
*
|
|
51
|
+
* The bundled platform package is preferred so a working install never depends
|
|
52
|
+
* on a global one; `agently-cli` on the PATH is the fallback for a user who
|
|
53
|
+
* installed it themselves.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveCliBinary() {
|
|
56
|
+
const platformPackage = PLATFORM_PACKAGES[`${process.platform}-${process.arch}`];
|
|
57
|
+
if (platformPackage) {
|
|
58
|
+
try {
|
|
59
|
+
const resolved = require.resolve(`${platformPackage}/package.json`);
|
|
60
|
+
const name = process.platform === 'win32' ? 'agently-cli.exe' : 'agently-cli';
|
|
61
|
+
return { command: join(dirname(resolved), 'bin', name), source: 'bundled' };
|
|
62
|
+
} catch {
|
|
63
|
+
// Fall through to the wrapper, then PATH.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const wrapper = require.resolve('@tencent-qqmail/agently-cli/package.json');
|
|
68
|
+
return { command: join(dirname(wrapper), 'scripts', 'run.js'), source: 'wrapper' };
|
|
69
|
+
} catch {
|
|
70
|
+
return { command: 'agently-cli', source: 'path' };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Whether the CLI can be launched at all. */
|
|
75
|
+
export function isCliAvailable() {
|
|
76
|
+
const { command, source } = resolveCliBinary();
|
|
77
|
+
if (source === 'path') return false;
|
|
78
|
+
return Boolean(command);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Start `auth login` and resolve as soon as it prints the authorization URL.
|
|
83
|
+
*
|
|
84
|
+
* The command then blocks until the user completes the scan, so it is left
|
|
85
|
+
* running rather than waited on; the caller polls `auth status` to observe the
|
|
86
|
+
* outcome, and the process exits on its own once the scan lands or it expires.
|
|
87
|
+
*/
|
|
88
|
+
export function startCliLogin({ signal, timeoutMs = 30_000, env = {}, workspace } = {}) {
|
|
89
|
+
const { command } = resolveCliBinary();
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
let child;
|
|
92
|
+
try {
|
|
93
|
+
child = spawn(command, ['auth', 'login'], {
|
|
94
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
95
|
+
env: { ...process.env, ...cliEnv(workspace), ...env },
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
reject(new AgentMailCliError(`unable to launch agently-cli: ${error.message}`, {
|
|
99
|
+
code: 'cli-unavailable', cause: error,
|
|
100
|
+
}));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let settled = false;
|
|
105
|
+
let buffered = '';
|
|
106
|
+
const stop = () => {
|
|
107
|
+
if (settled) return;
|
|
108
|
+
settled = true;
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
111
|
+
};
|
|
112
|
+
const timer = timeoutMs > 0
|
|
113
|
+
? setTimeout(() => {
|
|
114
|
+
if (settled) return;
|
|
115
|
+
stop();
|
|
116
|
+
reject(new AgentMailCliError('agently-cli printed no authorization URL', {
|
|
117
|
+
code: 'authorization-url-missing', detail: buffered.slice(0, 400),
|
|
118
|
+
}));
|
|
119
|
+
}, timeoutMs)
|
|
120
|
+
: null;
|
|
121
|
+
timer?.unref?.();
|
|
122
|
+
|
|
123
|
+
const onAbort = () => {
|
|
124
|
+
child.kill('SIGTERM');
|
|
125
|
+
stop();
|
|
126
|
+
reject(new AgentMailCliError('authorization aborted', { code: 'cli-aborted' }));
|
|
127
|
+
};
|
|
128
|
+
if (signal) {
|
|
129
|
+
if (signal.aborted) { onAbort(); return; }
|
|
130
|
+
signal.addEventListener?.('abort', onAbort, { once: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const scan = (chunk) => {
|
|
134
|
+
buffered += chunk;
|
|
135
|
+
const url = /https:\/\/agent\.qq\.com\/page\/oauth\S*/.exec(buffered)?.[0];
|
|
136
|
+
if (!url) return;
|
|
137
|
+
stop();
|
|
138
|
+
// The process keeps running so the scan can complete; it is detached from
|
|
139
|
+
// this promise on purpose.
|
|
140
|
+
resolve({ browserUrl: url, inputCode: /user_code=([^&\s]+)/.exec(url)?.[1] ?? '', child });
|
|
141
|
+
};
|
|
142
|
+
child.stdout.on('data', (chunk) => scan(String(chunk)));
|
|
143
|
+
child.stderr.on('data', (chunk) => scan(String(chunk)));
|
|
144
|
+
child.on('error', (error) => {
|
|
145
|
+
stop();
|
|
146
|
+
reject(new AgentMailCliError(`agently-cli failed to start: ${error.message}`, {
|
|
147
|
+
code: 'cli-unavailable', cause: error,
|
|
148
|
+
}));
|
|
149
|
+
});
|
|
150
|
+
child.on('close', () => {
|
|
151
|
+
if (settled) return;
|
|
152
|
+
stop();
|
|
153
|
+
reject(new AgentMailCliError('agently-cli exited before printing an authorization URL', {
|
|
154
|
+
code: 'authorization-url-missing', detail: buffered.slice(0, 400),
|
|
155
|
+
}));
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Run the CLI and parse its JSON document.
|
|
162
|
+
*
|
|
163
|
+
* `input` is written to stdin, which is how a body avoids both the argument
|
|
164
|
+
* list and the shell.
|
|
165
|
+
*/
|
|
166
|
+
/**
|
|
167
|
+
* The environment a CLI call runs with.
|
|
168
|
+
*
|
|
169
|
+
* The CLI isolates accounts by workspace, so a mailbox is addressed by its own
|
|
170
|
+
* workspace name. Without this every Agent mailbox shared one login and the
|
|
171
|
+
* second one always read the first one's account.
|
|
172
|
+
*/
|
|
173
|
+
export function cliEnv(workspace) {
|
|
174
|
+
return workspace ? { AGENTLY_WORKSPACE: String(workspace) } : {};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function runCli(args, {
|
|
178
|
+
input = null, timeoutMs = DEFAULT_TIMEOUT_MS, signal, env = {},
|
|
179
|
+
} = {}) {
|
|
180
|
+
const { command, source } = resolveCliBinary();
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
let child;
|
|
183
|
+
try {
|
|
184
|
+
child = spawn(command, args, {
|
|
185
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
186
|
+
env: { ...process.env, ...env },
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
reject(new AgentMailCliError(`unable to launch agently-cli (${source}): ${error.message}`, {
|
|
190
|
+
code: 'cli-unavailable', cause: error,
|
|
191
|
+
}));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let stdout = '';
|
|
196
|
+
let stderr = '';
|
|
197
|
+
let settled = false;
|
|
198
|
+
const finish = (fn, value) => {
|
|
199
|
+
if (settled) return;
|
|
200
|
+
settled = true;
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
203
|
+
fn(value);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const timer = timeoutMs > 0
|
|
207
|
+
? setTimeout(() => {
|
|
208
|
+
child.kill('SIGTERM');
|
|
209
|
+
finish(reject, new AgentMailCliError(`agently-cli timed out after ${timeoutMs}ms`, {
|
|
210
|
+
code: 'cli-timeout',
|
|
211
|
+
}));
|
|
212
|
+
}, timeoutMs)
|
|
213
|
+
: null;
|
|
214
|
+
timer?.unref?.();
|
|
215
|
+
|
|
216
|
+
const onAbort = () => {
|
|
217
|
+
child.kill('SIGTERM');
|
|
218
|
+
finish(reject, new AgentMailCliError('agently-cli aborted', { code: 'cli-aborted' }));
|
|
219
|
+
};
|
|
220
|
+
if (signal) {
|
|
221
|
+
if (signal.aborted) { onAbort(); return; }
|
|
222
|
+
signal.addEventListener?.('abort', onAbort, { once: true });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
226
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
227
|
+
child.on('error', (error) => {
|
|
228
|
+
finish(reject, new AgentMailCliError(`agently-cli failed to start: ${error.message}`, {
|
|
229
|
+
code: 'cli-unavailable', cause: error,
|
|
230
|
+
}));
|
|
231
|
+
});
|
|
232
|
+
child.on('close', (exitCode) => {
|
|
233
|
+
// The exit code is 0 even for an error document, so `ok` decides.
|
|
234
|
+
let document = null;
|
|
235
|
+
const text = stdout.trim();
|
|
236
|
+
if (text) {
|
|
237
|
+
try {
|
|
238
|
+
document = JSON.parse(text);
|
|
239
|
+
} catch {
|
|
240
|
+
finish(reject, new AgentMailCliError('agently-cli returned unparseable output', {
|
|
241
|
+
code: 'cli-invalid-output', detail: text.slice(0, 400),
|
|
242
|
+
}));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (document && document.ok === false) {
|
|
247
|
+
const failure = document.error ?? {};
|
|
248
|
+
finish(reject, new AgentMailCliError(
|
|
249
|
+
String(failure.message ?? 'agently-cli reported a failure'),
|
|
250
|
+
{
|
|
251
|
+
code: String(failure.type ?? 'cli-failed'),
|
|
252
|
+
status: typeof failure.status === 'number' ? failure.status : null,
|
|
253
|
+
detail: failure,
|
|
254
|
+
},
|
|
255
|
+
));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!document) {
|
|
259
|
+
finish(reject, new AgentMailCliError(
|
|
260
|
+
`agently-cli produced no output (exit ${exitCode})`,
|
|
261
|
+
{ code: 'cli-empty-output', detail: stderr.trim().slice(0, 400) },
|
|
262
|
+
));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
finish(resolve, { document, stdout, stderr, exitCode });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (input !== null) child.stdin.end(input);
|
|
269
|
+
else child.stdin.end();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Run the CLI through an injected implementation.
|
|
275
|
+
*
|
|
276
|
+
* Exists so tests can drive the failure and confirmation paths without a real
|
|
277
|
+
* binary; production always goes through `runCli`.
|
|
278
|
+
*/
|
|
279
|
+
export async function runCliDocumentForTests(args, { runCliImpl, input = null } = {}) {
|
|
280
|
+
const result = await runCliImpl(args, { input });
|
|
281
|
+
if (result.document && result.document.ok === false) {
|
|
282
|
+
const failure = result.document.error ?? {};
|
|
283
|
+
throw new AgentMailCliError(String(failure.message ?? 'agently-cli reported a failure'), {
|
|
284
|
+
code: String(failure.type ?? 'cli-failed'), detail: failure,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard mailbox transport: IMAP receive + SMTP send.
|
|
3
|
+
*
|
|
4
|
+
* One dedicated mailbox (the bot identity). Inbound mail is polled over IMAP;
|
|
5
|
+
* replies go back over SMTP inside the same mail thread. Everything above the
|
|
6
|
+
* transport — thread mapping, allowlist, quote stripping, loop prevention —
|
|
7
|
+
* is shared with every other transport.
|
|
8
|
+
*/
|
|
9
|
+
import { ImapFlow } from 'imapflow';
|
|
10
|
+
import { simpleParser } from 'mailparser';
|
|
11
|
+
import nodemailer from 'nodemailer';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
MAX_REPLY_CHARS, imapSecurity, normalizeAddress, smtpSecurity,
|
|
15
|
+
} from '../mail-format.mjs';
|
|
16
|
+
|
|
17
|
+
export class ImapSmtpTransport {
|
|
18
|
+
#config;
|
|
19
|
+
#signal;
|
|
20
|
+
#imap = null;
|
|
21
|
+
#transport = null;
|
|
22
|
+
|
|
23
|
+
constructor({ config, signal } = {}) {
|
|
24
|
+
if (!config?.address || !config?.password) {
|
|
25
|
+
throw new TypeError('ImapSmtpTransport requires an address and password');
|
|
26
|
+
}
|
|
27
|
+
this.#config = config;
|
|
28
|
+
this.#signal = signal;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get address() {
|
|
32
|
+
return this.#config.address;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Open the IMAP connection and select the monitored mailbox. */
|
|
36
|
+
async connect() {
|
|
37
|
+
if (this.#imap) return;
|
|
38
|
+
const client = new ImapFlow({
|
|
39
|
+
host: this.#config.imapHost,
|
|
40
|
+
port: this.#config.imapPort,
|
|
41
|
+
...imapSecurity(this.#config.imapPort, this.#config.imapSecure),
|
|
42
|
+
auth: { user: this.#config.address, pass: this.#config.password },
|
|
43
|
+
logger: false,
|
|
44
|
+
...(this.#config.rejectUnauthorized === false ? { tls: { rejectUnauthorized: false } } : {}),
|
|
45
|
+
});
|
|
46
|
+
client.on('error', () => { /* surfaced by the caller's poll/connection state */ });
|
|
47
|
+
await client.connect();
|
|
48
|
+
await client.mailboxOpen(this.#config.mailbox ?? 'INBOX');
|
|
49
|
+
this.#imap = client;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async disconnect() {
|
|
53
|
+
const client = this.#imap;
|
|
54
|
+
this.#imap = null;
|
|
55
|
+
if (client) await client.logout().catch(() => client.close?.());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Highest UID currently in the mailbox, used to skip pre-existing mail. */
|
|
59
|
+
async latestUid() {
|
|
60
|
+
await this.connect();
|
|
61
|
+
const status = await this.#imap.status(this.#config.mailbox ?? 'INBOX', { uidNext: true, messages: true });
|
|
62
|
+
const next = Number(status?.uidNext);
|
|
63
|
+
return Number.isFinite(next) && next > 1 ? next - 1 : 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Fetch messages with a UID greater than `afterUid`.
|
|
68
|
+
*
|
|
69
|
+
* The sender is read from the lightweight envelope first and checked against
|
|
70
|
+
* `allowSenders` before the body is requested. A monitored mailbox also
|
|
71
|
+
* receives ordinary personal mail, and that content must not be downloaded
|
|
72
|
+
* or parsed at all — not merely filtered after the fact.
|
|
73
|
+
*/
|
|
74
|
+
async listMessages({ afterUid = 0, limit = 25, allowSenders = null } = {}) {
|
|
75
|
+
await this.connect();
|
|
76
|
+
const mailbox = this.#config.mailbox ?? 'INBOX';
|
|
77
|
+
// A Set — even an empty one — means the caller supplied a policy: an
|
|
78
|
+
// empty allowlist admits nobody, so no body is fetched at all. Treating
|
|
79
|
+
// it as "no filter" downloaded mail the policy had already refused.
|
|
80
|
+
const allowed = allowSenders instanceof Set ? allowSenders : null;
|
|
81
|
+
|
|
82
|
+
// Step 1: read only the lightweight envelopes. ImapFlow cannot run a second
|
|
83
|
+
// fetch while one is being iterated, so the accepted UIDs are collected
|
|
84
|
+
// first and their bodies pulled afterwards.
|
|
85
|
+
const accepted = [];
|
|
86
|
+
for await (const message of this.#imap.fetch(
|
|
87
|
+
{ uid: `${afterUid + 1}:*` },
|
|
88
|
+
{ uid: true, envelope: true },
|
|
89
|
+
{ uid: true },
|
|
90
|
+
)) {
|
|
91
|
+
// A range fetch that matches nothing still yields the last message, so
|
|
92
|
+
// the UID bound is re-checked here.
|
|
93
|
+
if (!Number.isFinite(message.uid) || message.uid <= afterUid) continue;
|
|
94
|
+
if (allowed) {
|
|
95
|
+
const from = normalizeAddress(message.envelope?.from?.[0]?.address);
|
|
96
|
+
if (!from || !allowed.has(from)) continue;
|
|
97
|
+
}
|
|
98
|
+
accepted.push(message.uid);
|
|
99
|
+
if (accepted.length >= limit) break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Step 2: fetch and parse the bodies of accepted senders only. Mail from
|
|
103
|
+
// anyone else is never downloaded, so its content is not read at all.
|
|
104
|
+
const found = [];
|
|
105
|
+
for (const uid of accepted) {
|
|
106
|
+
const source = await this.#fetchSource(uid);
|
|
107
|
+
if (!source) continue;
|
|
108
|
+
const parsed = await simpleParser(source);
|
|
109
|
+
// Carry the UID alongside the parsed mail: it is the polling cursor and
|
|
110
|
+
// is not part of the RFC822 source.
|
|
111
|
+
parsed.uid = uid;
|
|
112
|
+
found.push(parsed);
|
|
113
|
+
}
|
|
114
|
+
await this.#imap.mailboxOpen(mailbox, { readOnly: false }).catch(() => {});
|
|
115
|
+
return found;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Read one message's RFC822 source by UID. */
|
|
119
|
+
async #fetchSource(uid) {
|
|
120
|
+
try {
|
|
121
|
+
const message = await this.#imap.fetchOne(String(uid), { source: true }, { uid: true });
|
|
122
|
+
return message?.source ?? null;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Send a reply inside the originating thread. */
|
|
129
|
+
async sendReply({ to, subject, text, inReplyTo, references, attachments = [] } = {}) {
|
|
130
|
+
const body = String(text ?? '').slice(0, MAX_REPLY_CHARS);
|
|
131
|
+
const transport = await this.#transportFor();
|
|
132
|
+
const info = await transport.sendMail({
|
|
133
|
+
from: this.#config.from ?? this.#config.address,
|
|
134
|
+
to,
|
|
135
|
+
subject: subject || '(no subject)',
|
|
136
|
+
text: body,
|
|
137
|
+
// RFC 3834: mark this as an automatic reply so any bot on the other side
|
|
138
|
+
// (including this mailbox replying to itself) can refuse to auto-answer
|
|
139
|
+
// it. That is the standard loop break, and it lets a mailbox accept its
|
|
140
|
+
// own address as a sender without risking an endless exchange.
|
|
141
|
+
headers: { 'Auto-Submitted': 'auto-replied' },
|
|
142
|
+
...(inReplyTo ? { inReplyTo } : {}),
|
|
143
|
+
...(references?.length ? { references: references.join(' ') } : {}),
|
|
144
|
+
...(attachments.length ? { attachments } : {}),
|
|
145
|
+
});
|
|
146
|
+
return { sent: true, messageId: info?.messageId ?? null };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Send a standalone message (proactive delivery, no thread). */
|
|
150
|
+
async sendText({ to, subject, text, attachments = [] } = {}) {
|
|
151
|
+
return this.sendReply({ to, subject, text, attachments });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async #transportFor() {
|
|
155
|
+
if (this.#transport) return this.#transport;
|
|
156
|
+
this.#transport = nodemailer.createTransport({
|
|
157
|
+
host: this.#config.smtpHost,
|
|
158
|
+
port: this.#config.smtpPort,
|
|
159
|
+
...smtpSecurity(this.#config.smtpPort, this.#config.smtpSecure),
|
|
160
|
+
auth: { user: this.#config.address, pass: this.#config.password },
|
|
161
|
+
...(this.#config.rejectUnauthorized === false ? { tls: { rejectUnauthorized: false } } : {}),
|
|
162
|
+
});
|
|
163
|
+
return this.#transport;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -115,6 +115,7 @@ import {
|
|
|
115
115
|
FEISHU_STEP_PUSH_MODES,
|
|
116
116
|
normalizeFeishuStepPushMode,
|
|
117
117
|
} from './step-push-mode.mjs';
|
|
118
|
+
import { FeishuLiveCot } from './live-cot.mjs';
|
|
118
119
|
|
|
119
120
|
// Lazily evaluated: t() must run after setImHostLanguage, not at import time.
|
|
120
121
|
const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
|
|
@@ -607,7 +608,7 @@ export class FeishuHarnessBridge {
|
|
|
607
608
|
#groupTopicReply = false;
|
|
608
609
|
/** When true, streaming turns push tool calls and interim notes as discrete messages. */
|
|
609
610
|
#stepPush = false;
|
|
610
|
-
/** Step push presentation:
|
|
611
|
+
/** Step push presentation: discrete posts, a CardKit card, or native live CoT. */
|
|
611
612
|
#stepPushMode = FEISHU_STEP_PUSH_MODES.POST;
|
|
612
613
|
/** Per-conversation step push state: key → { lastSentAt, count, breakerLogged }. */
|
|
613
614
|
#stepPushSendState = new Map();
|
|
@@ -4062,7 +4063,7 @@ export class FeishuHarnessBridge {
|
|
|
4062
4063
|
await Promise.allSettled(sessionIds.map((sessionId) => this.#scheduleCompensation(sessionId)));
|
|
4063
4064
|
}
|
|
4064
4065
|
|
|
4065
|
-
#interactionAskOptions(event, key, files) {
|
|
4066
|
+
#interactionAskOptions(event, key, files, images) {
|
|
4066
4067
|
return {
|
|
4067
4068
|
timeoutMs: this.#replyTimeoutMs,
|
|
4068
4069
|
signal: this.#signal,
|
|
@@ -4076,6 +4077,7 @@ export class FeishuHarnessBridge {
|
|
|
4076
4077
|
}),
|
|
4077
4078
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
4078
4079
|
files,
|
|
4080
|
+
images,
|
|
4079
4081
|
};
|
|
4080
4082
|
}
|
|
4081
4083
|
|
|
@@ -4894,7 +4896,7 @@ export class FeishuHarnessBridge {
|
|
|
4894
4896
|
// 的上下文增强按原样重放。直推分流发生在 `#answerWithStream` 构造之前,
|
|
4895
4897
|
// 这里就是本回合唯一一次构造(无重复的 prompt 往返)。
|
|
4896
4898
|
let content = hasInboundImages(message) || hasReplyReference(message)
|
|
4897
|
-
? await promptContentForInboundMessage(message, { signal: this.#signal })
|
|
4899
|
+
? await promptContentForInboundMessage(message, { signal: this.#signal, deferImages: true })
|
|
4898
4900
|
: undefined;
|
|
4899
4901
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
4900
4902
|
let contextEnhanced = false;
|
|
@@ -4930,6 +4932,19 @@ export class FeishuHarnessBridge {
|
|
|
4930
4932
|
// 流式卡片模式:每轮一张过程卡(原地 patch),过程与最终答案都进卡;
|
|
4931
4933
|
// post 模式维持逐条直推。先预建卡片状态,纯问答回合也能在收尾时开卡。
|
|
4932
4934
|
const streamingCard = this.#stepPushMode === FEISHU_STEP_PUSH_MODES.STREAMING_CARD;
|
|
4935
|
+
const liveCot = this.#stepPushMode === FEISHU_STEP_PUSH_MODES.LIVE_COT
|
|
4936
|
+
&& typeof this.#channel?.createCot === 'function'
|
|
4937
|
+
&& typeof this.#channel?.writeCotEvents === 'function';
|
|
4938
|
+
const cot = liveCot
|
|
4939
|
+
? new FeishuLiveCot(this.#channel, chatId, {
|
|
4940
|
+
replyTo: messageId,
|
|
4941
|
+
hidden: false,
|
|
4942
|
+
onFailure: (error) => this.#logger.warn?.(
|
|
4943
|
+
'[dsh-feishu] native live process failed; final answer will continue:',
|
|
4944
|
+
error?.message ?? String(error),
|
|
4945
|
+
),
|
|
4946
|
+
})
|
|
4947
|
+
: null;
|
|
4933
4948
|
if (streamingCard) {
|
|
4934
4949
|
if (this.#stepCards.has(key)) {
|
|
4935
4950
|
// 上一轮异常退出留下的「运行中」卡片先收尾,避免与本轮混淆。
|
|
@@ -4940,7 +4955,7 @@ export class FeishuHarnessBridge {
|
|
|
4940
4955
|
// /stop 打标:回合进行中收到停止请求时,封存为「已停止」而非「已完成」。
|
|
4941
4956
|
const stepStopFlag = streamingCard ? { requested: false } : null;
|
|
4942
4957
|
if (streamingCard) this.#stepStopFlags.set(key, stepStopFlag);
|
|
4943
|
-
const baseAskOptions = this.#interactionAskOptions(event, key, message.files);
|
|
4958
|
+
const baseAskOptions = this.#interactionAskOptions(event, key, message.files, message.images);
|
|
4944
4959
|
|
|
4945
4960
|
// 上下文注入动作在详细级别下也体现为一条步骤(注入细节可忽略)。
|
|
4946
4961
|
// 不计入工具/助手消息的熔断计数(规格口径:熔断只管「工具 + 助手」)。
|
|
@@ -4982,7 +4997,9 @@ export class FeishuHarnessBridge {
|
|
|
4982
4997
|
messageId,
|
|
4983
4998
|
);
|
|
4984
4999
|
};
|
|
4985
|
-
const watchdog = streamingCard
|
|
5000
|
+
const watchdog = streamingCard || liveCot
|
|
5001
|
+
? null
|
|
5002
|
+
: this.#startThinkingStatusWatchdog(key, chatId, messageId);
|
|
4986
5003
|
let completed;
|
|
4987
5004
|
try {
|
|
4988
5005
|
completed = await askInWorkspaceSession({
|
|
@@ -4999,7 +5016,7 @@ export class FeishuHarnessBridge {
|
|
|
4999
5016
|
existsOptions: { signal: this.#signal },
|
|
5000
5017
|
askOptions: {
|
|
5001
5018
|
...baseAskOptions,
|
|
5002
|
-
progressMode: 'all',
|
|
5019
|
+
progressMode: liveCot ? 'live' : 'all',
|
|
5003
5020
|
// 提问/审批卡弹出前先定格当前过程卡,答案随之流到交互消息之后
|
|
5004
5021
|
// 的新卡上(与主流式路径的 rotate 语义一致)。
|
|
5005
5022
|
...(streamingCard ? {
|
|
@@ -5011,6 +5028,15 @@ export class FeishuHarnessBridge {
|
|
|
5011
5028
|
},
|
|
5012
5029
|
} : {}),
|
|
5013
5030
|
onUpdate: async (update) => {
|
|
5031
|
+
if (liveCot) {
|
|
5032
|
+
await cot.handle(update);
|
|
5033
|
+
if (update.type === 'assistant-message') {
|
|
5034
|
+
pendingStep = update;
|
|
5035
|
+
} else if (update.type === 'tool') {
|
|
5036
|
+
pendingStep = null;
|
|
5037
|
+
}
|
|
5038
|
+
return;
|
|
5039
|
+
}
|
|
5014
5040
|
if (update.type === 'assistant-message') {
|
|
5015
5041
|
if (pendingStep && Number(update.step) > Number(pendingStep.step)) {
|
|
5016
5042
|
await flushPendingStep();
|
|
@@ -5053,6 +5079,7 @@ export class FeishuHarnessBridge {
|
|
|
5053
5079
|
},
|
|
5054
5080
|
});
|
|
5055
5081
|
} catch (error) {
|
|
5082
|
+
await cot?.finish(error);
|
|
5056
5083
|
if (streamingCard) {
|
|
5057
5084
|
this.#stepStopFlags.delete(key);
|
|
5058
5085
|
await this.#finishStepCard(key, { stopped: true });
|
|
@@ -5062,6 +5089,7 @@ export class FeishuHarnessBridge {
|
|
|
5062
5089
|
// 回合结束(成功/失败/中断):停看门狗并撤回残留思考中心跳。
|
|
5063
5090
|
await watchdog?.stop();
|
|
5064
5091
|
}
|
|
5092
|
+
await cot?.finish();
|
|
5065
5093
|
markAskComplete();
|
|
5066
5094
|
const finalStepText = pendingStep ? pendingStep.text : null;
|
|
5067
5095
|
pendingStep = null;
|
|
@@ -5129,7 +5157,7 @@ export class FeishuHarnessBridge {
|
|
|
5129
5157
|
}
|
|
5130
5158
|
textReceipt = createDeliveryReceipt({
|
|
5131
5159
|
deliveryId: messageId,
|
|
5132
|
-
presentation: 'feishu-step-push-post',
|
|
5160
|
+
presentation: liveCot ? 'feishu-live-cot-answer' : 'feishu-step-push-post',
|
|
5133
5161
|
providerMessageIds,
|
|
5134
5162
|
});
|
|
5135
5163
|
this.#status.streamResponses = (this.#status.streamResponses ?? 0) + 1;
|
|
@@ -5214,7 +5242,7 @@ export class FeishuHarnessBridge {
|
|
|
5214
5242
|
return this.#answerWithStepPush(event, key, message, { onAskComplete });
|
|
5215
5243
|
}
|
|
5216
5244
|
let content = hasInboundImages(message) || hasReplyReference(message)
|
|
5217
|
-
? await promptContentForInboundMessage(message, { signal: this.#signal })
|
|
5245
|
+
? await promptContentForInboundMessage(message, { signal: this.#signal, deferImages: true })
|
|
5218
5246
|
: undefined;
|
|
5219
5247
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
5220
5248
|
let contextEnhanced = false;
|
|
@@ -5241,7 +5269,7 @@ export class FeishuHarnessBridge {
|
|
|
5241
5269
|
contextEnhanced,
|
|
5242
5270
|
createOptions: { signal: this.#signal },
|
|
5243
5271
|
existsOptions: { signal: this.#signal },
|
|
5244
|
-
askOptions: this.#interactionAskOptions(event, key, message.files),
|
|
5272
|
+
askOptions: this.#interactionAskOptions(event, key, message.files, message.images),
|
|
5245
5273
|
});
|
|
5246
5274
|
markAskComplete();
|
|
5247
5275
|
let textReceipt;
|
|
@@ -5285,7 +5313,7 @@ export class FeishuHarnessBridge {
|
|
|
5285
5313
|
stream = await this.#channel.stream(chatId, {
|
|
5286
5314
|
markdown: async (controller) => {
|
|
5287
5315
|
promptStarted = true;
|
|
5288
|
-
const baseAskOptions = this.#interactionAskOptions(event, key, message.files);
|
|
5316
|
+
const baseAskOptions = this.#interactionAskOptions(event, key, message.files, message.images);
|
|
5289
5317
|
const askOptions = {
|
|
5290
5318
|
...baseAskOptions,
|
|
5291
5319
|
// issue #86:独立交互消息(提问/审批)会落在占位卡下方,呈现前
|
|
@@ -5394,7 +5422,7 @@ export class FeishuHarnessBridge {
|
|
|
5394
5422
|
contextEnhanced,
|
|
5395
5423
|
createOptions: { signal: this.#signal },
|
|
5396
5424
|
existsOptions: { signal: this.#signal },
|
|
5397
|
-
askOptions: this.#interactionAskOptions(event, key, message.files),
|
|
5425
|
+
askOptions: this.#interactionAskOptions(event, key, message.files, message.images),
|
|
5398
5426
|
});
|
|
5399
5427
|
markAskComplete();
|
|
5400
5428
|
let textReceipt;
|
|
@@ -8,6 +8,7 @@ const STREAM_ELEMENT_ID = 'stream_md';
|
|
|
8
8
|
const DEFAULT_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
|
9
9
|
const MAX_STREAM_CHARS = 28000;
|
|
10
10
|
const MAX_FILE_OPERATION_TIMEOUT_MS = 120_000;
|
|
11
|
+
const COT_API = '/open-apis/im/v1/message_cot';
|
|
11
12
|
|
|
12
13
|
const FILE_DELIVERY_ERRORS = new Map([
|
|
13
14
|
[99991672, ['artifact-permission-required', 'Feishu file delivery requires the im:resource permission.']],
|
|
@@ -365,6 +366,40 @@ export class VerifiedFeishuChannel {
|
|
|
365
366
|
});
|
|
366
367
|
}
|
|
367
368
|
|
|
369
|
+
/** Open one native Feishu thinking-process message. */
|
|
370
|
+
async createCot(chatId, { replyTo, hidden = false } = {}) {
|
|
371
|
+
const response = assertApiSuccess('Feishu message_cot.create', await this.#client.request({
|
|
372
|
+
method: 'POST',
|
|
373
|
+
url: `${COT_API}?receive_id_type=chat_id`,
|
|
374
|
+
data: {
|
|
375
|
+
receive_id: chatId,
|
|
376
|
+
...(replyTo ? { origin_message_id: replyTo } : {}),
|
|
377
|
+
cot_hidden: hidden === true,
|
|
378
|
+
enable_badge: false,
|
|
379
|
+
update_feed_rank: false,
|
|
380
|
+
},
|
|
381
|
+
}));
|
|
382
|
+
const cotId = response?.data?.cot_id;
|
|
383
|
+
const messageId = response?.data?.message_id;
|
|
384
|
+
if (!cotId || !messageId) {
|
|
385
|
+
throw new Error('Feishu message_cot.create returned no cot_id/message_id');
|
|
386
|
+
}
|
|
387
|
+
return { cotId, messageId };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Append ordered AG-UI events to one native thinking process. */
|
|
391
|
+
async writeCotEvents(handle, events) {
|
|
392
|
+
assertApiSuccess('Feishu message_cot.write', await this.#client.request({
|
|
393
|
+
method: 'PUT',
|
|
394
|
+
url: COT_API,
|
|
395
|
+
data: {
|
|
396
|
+
events,
|
|
397
|
+
message_id: handle.messageId,
|
|
398
|
+
cot_id: handle.cotId,
|
|
399
|
+
},
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
|
|
368
403
|
async #sendArtifact(chatId, file, {
|
|
369
404
|
replyTo,
|
|
370
405
|
signal,
|