@larksuite/openclaw-lark 2026.3.12 → 2026.3.17-beta.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/bin/openclaw-lark.js +48 -0
- package/package.json +4 -1
- package/src/card/builder.d.ts +8 -3
- package/src/card/builder.js +44 -21
- package/src/card/image-resolver.d.ts +45 -0
- package/src/card/image-resolver.js +113 -0
- package/src/card/markdown-style.js +6 -6
- package/src/card/streaming-card-controller.d.ts +1 -0
- package/src/card/streaming-card-controller.js +25 -7
- package/src/commands/auth.d.ts +7 -1
- package/src/commands/auth.js +95 -18
- package/src/commands/doctor.d.ts +10 -1
- package/src/commands/doctor.js +209 -63
- package/src/commands/index.d.ts +18 -1
- package/src/commands/index.js +122 -48
- package/src/commands/locale.d.ts +7 -0
- package/src/commands/locale.js +8 -0
- package/src/core/auth-errors.d.ts +12 -10
- package/src/core/auth-errors.js +13 -14
- package/src/core/domains.d.ts +18 -0
- package/src/core/domains.js +29 -0
- package/src/core/targets.d.ts +11 -0
- package/src/core/targets.js +38 -1
- package/src/core/tool-scopes.d.ts +3 -3
- package/src/core/tool-scopes.js +3 -2
- package/src/core/tools-config.d.ts +21 -0
- package/src/core/tools-config.js +50 -0
- package/src/core/uat-client.js +31 -16
- package/src/messaging/inbound/dispatch-builders.d.ts +1 -0
- package/src/messaging/inbound/dispatch-builders.js +1 -1
- package/src/messaging/inbound/dispatch.js +72 -0
- package/src/messaging/inbound/reaction-handler.js +5 -4
- package/src/messaging/outbound/media.d.ts +5 -0
- package/src/messaging/outbound/media.js +10 -0
- package/src/messaging/outbound/outbound.js +25 -15
- package/src/messaging/outbound/send.d.ts +17 -0
- package/src/messaging/outbound/send.js +89 -22
- package/src/tools/auto-auth.js +76 -81
- package/src/tools/helpers.d.ts +41 -0
- package/src/tools/helpers.js +67 -0
- package/src/tools/mcp/shared.d.ts +3 -1
- package/src/tools/mcp/shared.js +11 -8
- package/src/tools/oapi/bitable/app-table-field.js +2 -3
- package/src/tools/oapi/bitable/app-table-record.js +4 -16
- package/src/tools/oapi/bitable/app-table-view.js +2 -3
- package/src/tools/oapi/bitable/app-table.js +2 -3
- package/src/tools/oapi/bitable/app.js +2 -3
- package/src/tools/oapi/calendar/calendar.js +2 -3
- package/src/tools/oapi/calendar/event-attendee.js +5 -16
- package/src/tools/oapi/calendar/event.js +9 -20
- package/src/tools/oapi/calendar/freebusy.js +2 -3
- package/src/tools/oapi/chat/chat.js +4 -5
- package/src/tools/oapi/chat/members.js +3 -4
- package/src/tools/oapi/common/get-user.js +3 -4
- package/src/tools/oapi/common/search-user.js +2 -3
- package/src/tools/oapi/drive/doc-comments.js +6 -14
- package/src/tools/oapi/drive/doc-media.js +5 -6
- package/src/tools/oapi/drive/file.js +4 -5
- package/src/tools/oapi/helpers.d.ts +10 -1
- package/src/tools/oapi/helpers.js +15 -1
- package/src/tools/oapi/im/message-read.js +11 -11
- package/src/tools/oapi/im/message.js +5 -26
- package/src/tools/oapi/im/resource.js +3 -4
- package/src/tools/oapi/im/user-name-uat.d.ts +3 -0
- package/src/tools/oapi/im/user-name-uat.js +25 -12
- package/src/tools/oapi/search/doc-search.js +15 -26
- package/src/tools/oapi/sheets/sheet.js +7 -6
- package/src/tools/oapi/task/comment.js +3 -4
- package/src/tools/oapi/task/subtask.js +3 -4
- package/src/tools/oapi/task/task.js +8 -9
- package/src/tools/oapi/task/tasklist.js +5 -6
- package/src/tools/oapi/wiki/space-node.js +5 -27
- package/src/tools/oapi/wiki/space.js +2 -3
- package/src/tools/oauth-batch-auth.js +4 -3
- package/src/tools/oauth-cards.d.ts +18 -5
- package/src/tools/oauth-cards.js +109 -44
- package/src/tools/oauth.js +19 -17
- package/src/tools/tat/im/resource.js +3 -3
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
// --tools-version <ver> lets the user pin a specific version
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
let version = "latest";
|
|
9
|
+
|
|
10
|
+
const vIdx = args.indexOf("--tools-version");
|
|
11
|
+
if (vIdx !== -1) {
|
|
12
|
+
version = args[vIdx + 1];
|
|
13
|
+
// Remove --tools-version <ver> from forwarded args
|
|
14
|
+
args.splice(vIdx, 2);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const allArgs = ["--yes", `@larksuite/openclaw-lark-tools@${version}`, ...args];
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
if (process.platform === "win32") {
|
|
21
|
+
// On Windows, npx is a .cmd shim that can be broken or trigger
|
|
22
|
+
// DEP0190. Bypass it entirely: run node with the npx-cli.js
|
|
23
|
+
// script located next to the running node binary.
|
|
24
|
+
const npxCli = join(
|
|
25
|
+
dirname(process.execPath),
|
|
26
|
+
"node_modules",
|
|
27
|
+
"npm",
|
|
28
|
+
"bin",
|
|
29
|
+
"npx-cli.js",
|
|
30
|
+
);
|
|
31
|
+
execFileSync(process.execPath, [npxCli, ...allArgs], {
|
|
32
|
+
stdio: "inherit",
|
|
33
|
+
env: {
|
|
34
|
+
...process.env,
|
|
35
|
+
NODE_OPTIONS: [
|
|
36
|
+
process.env.NODE_OPTIONS,
|
|
37
|
+
"--disable-warning=DEP0190",
|
|
38
|
+
]
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join(" "),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
execFileSync("npx", allArgs, { stdio: "inherit" });
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
process.exit(error.status ?? 1);
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@larksuite/openclaw-lark",
|
|
3
|
-
"version": "2026.3.
|
|
3
|
+
"version": "2026.3.17-beta.0",
|
|
4
4
|
"description": "OpenClaw Lark/Feishu channel plugin",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openclaw-lark": "bin/openclaw-lark.js"
|
|
8
|
+
},
|
|
6
9
|
"files": [
|
|
7
10
|
"**/*"
|
|
8
11
|
],
|
package/src/card/builder.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface FeishuCard {
|
|
|
28
28
|
config: {
|
|
29
29
|
wide_screen_mode: boolean;
|
|
30
30
|
update_multi?: boolean;
|
|
31
|
+
locales?: string[];
|
|
31
32
|
summary?: {
|
|
32
33
|
content: string;
|
|
33
34
|
};
|
|
@@ -36,6 +37,7 @@ export interface FeishuCard {
|
|
|
36
37
|
title: {
|
|
37
38
|
tag: 'plain_text';
|
|
38
39
|
content: string;
|
|
40
|
+
i18n_content?: Record<string, string>;
|
|
39
41
|
};
|
|
40
42
|
template: string;
|
|
41
43
|
};
|
|
@@ -66,10 +68,13 @@ export declare function splitReasoningText(text?: string): {
|
|
|
66
68
|
*/
|
|
67
69
|
export declare function stripReasoningTags(text: string): string;
|
|
68
70
|
/**
|
|
69
|
-
* Format reasoning duration into a human-readable
|
|
70
|
-
* e.g. "
|
|
71
|
+
* Format reasoning duration into a human-readable i18n pair.
|
|
72
|
+
* e.g. { zh: "思考了 3.2s", en: "Thought for 3.2s" }
|
|
71
73
|
*/
|
|
72
|
-
export declare function formatReasoningDuration(ms: number):
|
|
74
|
+
export declare function formatReasoningDuration(ms: number): {
|
|
75
|
+
zh: string;
|
|
76
|
+
en: string;
|
|
77
|
+
};
|
|
73
78
|
/**
|
|
74
79
|
* Format milliseconds into a human-readable duration string.
|
|
75
80
|
*/
|
package/src/card/builder.js
CHANGED
|
@@ -107,11 +107,12 @@ function cleanReasoningPrefix(text) {
|
|
|
107
107
|
return cleaned.trim();
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
|
-
* Format reasoning duration into a human-readable
|
|
111
|
-
* e.g. "
|
|
110
|
+
* Format reasoning duration into a human-readable i18n pair.
|
|
111
|
+
* e.g. { zh: "思考了 3.2s", en: "Thought for 3.2s" }
|
|
112
112
|
*/
|
|
113
113
|
export function formatReasoningDuration(ms) {
|
|
114
|
-
|
|
114
|
+
const d = formatElapsed(ms);
|
|
115
|
+
return { zh: `思考了 ${d}`, en: `Thought for ${d}` };
|
|
115
116
|
}
|
|
116
117
|
/**
|
|
117
118
|
* Format milliseconds into a human-readable duration string.
|
|
@@ -121,12 +122,18 @@ export function formatElapsed(ms) {
|
|
|
121
122
|
return seconds < 60 ? `${seconds.toFixed(1)}s` : `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
|
|
122
123
|
}
|
|
123
124
|
/**
|
|
124
|
-
* Build footer meta-info:
|
|
125
|
+
* Build footer meta-info: notation-sized text with i18n support.
|
|
125
126
|
* Error text is rendered in red; normal text uses default grey (notation).
|
|
126
127
|
*/
|
|
127
|
-
function buildFooter(
|
|
128
|
-
const
|
|
129
|
-
|
|
128
|
+
function buildFooter(zhText, enText, isError) {
|
|
129
|
+
const zhContent = isError ? `<font color='red'>${zhText}</font>` : zhText;
|
|
130
|
+
const enContent = isError ? `<font color='red'>${enText}</font>` : enText;
|
|
131
|
+
return [{
|
|
132
|
+
tag: 'markdown',
|
|
133
|
+
content: enContent,
|
|
134
|
+
i18n_content: { zh_cn: zhContent, en_us: enContent },
|
|
135
|
+
text_size: 'notation',
|
|
136
|
+
}];
|
|
130
137
|
}
|
|
131
138
|
// ---------------------------------------------------------------------------
|
|
132
139
|
// buildCardContent
|
|
@@ -163,11 +170,12 @@ export function buildCardContent(state, data = {}) {
|
|
|
163
170
|
// ---------------------------------------------------------------------------
|
|
164
171
|
function buildThinkingCard() {
|
|
165
172
|
return {
|
|
166
|
-
config: { wide_screen_mode: true, update_multi: true },
|
|
173
|
+
config: { wide_screen_mode: true, update_multi: true, locales: ['zh_cn', 'en_us'] },
|
|
167
174
|
elements: [
|
|
168
175
|
{
|
|
169
176
|
tag: 'markdown',
|
|
170
|
-
content: '
|
|
177
|
+
content: 'Thinking...',
|
|
178
|
+
i18n_content: { zh_cn: '思考中...', en_us: 'Thinking...' },
|
|
171
179
|
},
|
|
172
180
|
],
|
|
173
181
|
};
|
|
@@ -179,6 +187,10 @@ function buildStreamingCard(partialText, toolCalls, reasoningText) {
|
|
|
179
187
|
elements.push({
|
|
180
188
|
tag: 'markdown',
|
|
181
189
|
content: `💭 **Thinking...**\n\n${reasoningText}`,
|
|
190
|
+
i18n_content: {
|
|
191
|
+
zh_cn: `💭 **思考中...**\n\n${reasoningText}`,
|
|
192
|
+
en_us: `💭 **Thinking...**\n\n${reasoningText}`,
|
|
193
|
+
},
|
|
182
194
|
text_size: 'notation',
|
|
183
195
|
});
|
|
184
196
|
}
|
|
@@ -202,7 +214,7 @@ function buildStreamingCard(partialText, toolCalls, reasoningText) {
|
|
|
202
214
|
});
|
|
203
215
|
}
|
|
204
216
|
return {
|
|
205
|
-
config: { wide_screen_mode: true, update_multi: true },
|
|
217
|
+
config: { wide_screen_mode: true, update_multi: true, locales: ['zh_cn', 'en_us'] },
|
|
206
218
|
elements,
|
|
207
219
|
};
|
|
208
220
|
}
|
|
@@ -211,14 +223,20 @@ function buildCompleteCard(params) {
|
|
|
211
223
|
const elements = [];
|
|
212
224
|
// Collapsible reasoning panel (before main content)
|
|
213
225
|
if (reasoningText) {
|
|
214
|
-
const
|
|
226
|
+
const dur = reasoningElapsedMs ? formatReasoningDuration(reasoningElapsedMs) : null;
|
|
227
|
+
const zhLabel = dur ? dur.zh : '思考';
|
|
228
|
+
const enLabel = dur ? dur.en : 'Thought';
|
|
215
229
|
elements.push({
|
|
216
230
|
tag: 'collapsible_panel',
|
|
217
231
|
expanded: false,
|
|
218
232
|
header: {
|
|
219
233
|
title: {
|
|
220
234
|
tag: 'markdown',
|
|
221
|
-
content: `💭 ${
|
|
235
|
+
content: `💭 ${enLabel}`,
|
|
236
|
+
i18n_content: {
|
|
237
|
+
zh_cn: `💭 ${zhLabel}`,
|
|
238
|
+
en_us: `💭 ${enLabel}`,
|
|
239
|
+
},
|
|
222
240
|
},
|
|
223
241
|
vertical_align: 'center',
|
|
224
242
|
icon: {
|
|
@@ -260,31 +278,36 @@ function buildCompleteCard(params) {
|
|
|
260
278
|
}
|
|
261
279
|
// Footer meta-info: each metadata item is independently controlled via
|
|
262
280
|
// the `footer` config. Both status and elapsed default to hidden.
|
|
263
|
-
const
|
|
281
|
+
const zhParts = [];
|
|
282
|
+
const enParts = [];
|
|
264
283
|
if (footer?.status) {
|
|
265
284
|
if (isError) {
|
|
266
|
-
|
|
285
|
+
zhParts.push('出错');
|
|
286
|
+
enParts.push('Error');
|
|
267
287
|
}
|
|
268
288
|
else if (isAborted) {
|
|
269
|
-
|
|
289
|
+
zhParts.push('已停止');
|
|
290
|
+
enParts.push('Stopped');
|
|
270
291
|
}
|
|
271
292
|
else {
|
|
272
|
-
|
|
293
|
+
zhParts.push('已完成');
|
|
294
|
+
enParts.push('Completed');
|
|
273
295
|
}
|
|
274
296
|
}
|
|
275
297
|
if (footer?.elapsed && elapsedMs != null) {
|
|
276
|
-
|
|
298
|
+
const d = formatElapsed(elapsedMs);
|
|
299
|
+
zhParts.push(`耗时 ${d}`);
|
|
300
|
+
enParts.push(`Elapsed ${d}`);
|
|
277
301
|
}
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
elements.push(...buildFooter(footerText, isError));
|
|
302
|
+
if (zhParts.length > 0) {
|
|
303
|
+
elements.push(...buildFooter(zhParts.join(' · '), enParts.join(' · '), isError));
|
|
281
304
|
}
|
|
282
305
|
// Use the answer text (not reasoning) as the feed preview summary.
|
|
283
306
|
// Strip markdown syntax so the preview reads as plain text.
|
|
284
307
|
const summaryText = text.replace(/[*_`#>\[\]()~]/g, '').trim();
|
|
285
308
|
const summary = summaryText ? { content: summaryText.slice(0, 120) } : undefined;
|
|
286
309
|
return {
|
|
287
|
-
config: { wide_screen_mode: true, update_multi: true, summary },
|
|
310
|
+
config: { wide_screen_mode: true, update_multi: true, locales: ['zh_cn', 'en_us'], summary },
|
|
288
311
|
elements,
|
|
289
312
|
};
|
|
290
313
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* ImageResolver — converts image URLs in markdown to Feishu image keys.
|
|
6
|
+
*
|
|
7
|
+
* Used by StreamingCardController to asynchronously download and upload
|
|
8
|
+
* images referenced via `` in model-generated markdown,
|
|
9
|
+
* replacing them with `` that Feishu cards can render.
|
|
10
|
+
*/
|
|
11
|
+
import type { ClawdbotConfig } from 'openclaw/plugin-sdk';
|
|
12
|
+
export interface ImageResolverOptions {
|
|
13
|
+
cfg: ClawdbotConfig;
|
|
14
|
+
accountId: string | undefined;
|
|
15
|
+
/** Called when a previously-pending image upload completes. */
|
|
16
|
+
onImageResolved: () => void;
|
|
17
|
+
}
|
|
18
|
+
export declare class ImageResolver {
|
|
19
|
+
/** URL → imageKey for successfully uploaded images. */
|
|
20
|
+
private readonly resolved;
|
|
21
|
+
/** URL → upload Promise for in-flight uploads (dedup). */
|
|
22
|
+
private readonly pending;
|
|
23
|
+
/** URLs that have already failed — skip retries. */
|
|
24
|
+
private readonly failed;
|
|
25
|
+
private readonly cfg;
|
|
26
|
+
private readonly accountId;
|
|
27
|
+
private readonly onImageResolved;
|
|
28
|
+
constructor(opts: ImageResolverOptions);
|
|
29
|
+
/**
|
|
30
|
+
* Synchronously resolve image URLs in markdown text.
|
|
31
|
+
*
|
|
32
|
+
* - `img_xxx` references are kept as-is.
|
|
33
|
+
* - URLs with a cached imageKey are replaced inline.
|
|
34
|
+
* - URLs with an in-flight upload are stripped (will appear after re-flush).
|
|
35
|
+
* - New URLs trigger an async upload and are stripped for now.
|
|
36
|
+
*/
|
|
37
|
+
resolveImages(text: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve all image URLs in text synchronously: trigger uploads for new
|
|
40
|
+
* URLs, wait for all pending uploads, then return text with image keys.
|
|
41
|
+
*/
|
|
42
|
+
resolveImagesAwait(text: string, timeoutMs: number): Promise<string>;
|
|
43
|
+
private startUpload;
|
|
44
|
+
private doUpload;
|
|
45
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* ImageResolver — converts image URLs in markdown to Feishu image keys.
|
|
7
|
+
*
|
|
8
|
+
* Used by StreamingCardController to asynchronously download and upload
|
|
9
|
+
* images referenced via `` in model-generated markdown,
|
|
10
|
+
* replacing them with `` that Feishu cards can render.
|
|
11
|
+
*/
|
|
12
|
+
import { fetchRemoteImageBuffer, uploadImageLark } from '../messaging/outbound/media';
|
|
13
|
+
import { larkLogger } from '../core/lark-logger';
|
|
14
|
+
const log = larkLogger('card/image-resolver');
|
|
15
|
+
/** Matches complete markdown image syntax: `` */
|
|
16
|
+
const IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g;
|
|
17
|
+
export class ImageResolver {
|
|
18
|
+
/** URL → imageKey for successfully uploaded images. */
|
|
19
|
+
resolved = new Map();
|
|
20
|
+
/** URL → upload Promise for in-flight uploads (dedup). */
|
|
21
|
+
pending = new Map();
|
|
22
|
+
/** URLs that have already failed — skip retries. */
|
|
23
|
+
failed = new Set();
|
|
24
|
+
cfg;
|
|
25
|
+
accountId;
|
|
26
|
+
onImageResolved;
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
this.cfg = opts.cfg;
|
|
29
|
+
this.accountId = opts.accountId;
|
|
30
|
+
this.onImageResolved = opts.onImageResolved;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Synchronously resolve image URLs in markdown text.
|
|
34
|
+
*
|
|
35
|
+
* - `img_xxx` references are kept as-is.
|
|
36
|
+
* - URLs with a cached imageKey are replaced inline.
|
|
37
|
+
* - URLs with an in-flight upload are stripped (will appear after re-flush).
|
|
38
|
+
* - New URLs trigger an async upload and are stripped for now.
|
|
39
|
+
*/
|
|
40
|
+
resolveImages(text) {
|
|
41
|
+
if (!text.includes('`;
|
|
54
|
+
// Already failed — don't retry, strip.
|
|
55
|
+
if (this.failed.has(value))
|
|
56
|
+
return '';
|
|
57
|
+
// Upload in progress — strip for now.
|
|
58
|
+
if (this.pending.has(value))
|
|
59
|
+
return '';
|
|
60
|
+
// New URL — kick off async upload, strip for now.
|
|
61
|
+
this.startUpload(value);
|
|
62
|
+
return '';
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve all image URLs in text synchronously: trigger uploads for new
|
|
67
|
+
* URLs, wait for all pending uploads, then return text with image keys.
|
|
68
|
+
*/
|
|
69
|
+
async resolveImagesAwait(text, timeoutMs) {
|
|
70
|
+
// First pass: trigger uploads for any new URLs
|
|
71
|
+
this.resolveImages(text);
|
|
72
|
+
if (this.pending.size > 0) {
|
|
73
|
+
log.info('resolveImagesAwait: waiting for uploads', { count: this.pending.size, timeoutMs });
|
|
74
|
+
const allUploads = Promise.all(this.pending.values());
|
|
75
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
76
|
+
await Promise.race([allUploads, timeout]);
|
|
77
|
+
if (this.pending.size > 0) {
|
|
78
|
+
log.warn('resolveImagesAwait: timed out with pending uploads', {
|
|
79
|
+
remaining: this.pending.size,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Second pass: replace URLs with resolved image keys
|
|
84
|
+
return this.resolveImages(text);
|
|
85
|
+
}
|
|
86
|
+
startUpload(url) {
|
|
87
|
+
const uploadPromise = this.doUpload(url);
|
|
88
|
+
this.pending.set(url, uploadPromise);
|
|
89
|
+
}
|
|
90
|
+
async doUpload(url) {
|
|
91
|
+
try {
|
|
92
|
+
log.info('uploading image', { url });
|
|
93
|
+
const buffer = await fetchRemoteImageBuffer(url);
|
|
94
|
+
const { imageKey } = await uploadImageLark({
|
|
95
|
+
cfg: this.cfg,
|
|
96
|
+
image: buffer,
|
|
97
|
+
imageType: 'message',
|
|
98
|
+
accountId: this.accountId,
|
|
99
|
+
});
|
|
100
|
+
log.info('image uploaded', { url, imageKey });
|
|
101
|
+
this.resolved.set(url, imageKey);
|
|
102
|
+
this.pending.delete(url);
|
|
103
|
+
this.onImageResolved();
|
|
104
|
+
return imageKey;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
log.warn('image upload failed', { url, error: String(err) });
|
|
108
|
+
this.pending.delete(url);
|
|
109
|
+
this.failed.add(url);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -81,7 +81,11 @@ function _optimizeMarkdownStyle(text, cardVersion = 2) {
|
|
|
81
81
|
const IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g;
|
|
82
82
|
/**
|
|
83
83
|
* Strip `` where value is not a valid Feishu image key
|
|
84
|
-
* (`img_xxx`)
|
|
84
|
+
* (`img_xxx`). Prevents CardKit error 200570.
|
|
85
|
+
*
|
|
86
|
+
* HTTP URLs are stripped as well — ImageResolver should have already
|
|
87
|
+
* replaced them with `img_xxx` keys before this point. This serves
|
|
88
|
+
* as a safety net for any unresolved URLs.
|
|
85
89
|
*/
|
|
86
90
|
function stripInvalidImageKeys(text) {
|
|
87
91
|
if (!text.includes('!['))
|
|
@@ -89,10 +93,6 @@ function stripInvalidImageKeys(text) {
|
|
|
89
93
|
return text.replace(IMAGE_RE, (fullMatch, _alt, value) => {
|
|
90
94
|
if (value.startsWith('img_'))
|
|
91
95
|
return fullMatch;
|
|
92
|
-
|
|
93
|
-
return fullMatch;
|
|
94
|
-
if (value.startsWith('https://'))
|
|
95
|
-
return fullMatch;
|
|
96
|
-
return value;
|
|
96
|
+
return ''; // strip all non-img_ image references (URLs, local paths, etc.)
|
|
97
97
|
});
|
|
98
98
|
}
|
|
@@ -18,6 +18,7 @@ import { sendCardFeishu, updateCardFeishu } from '../messaging/outbound/send';
|
|
|
18
18
|
import { createCardEntity, sendCardByCardId, streamCardContent, updateCardKitCard, setCardStreamingMode, } from './cardkit';
|
|
19
19
|
import { buildCardContent, splitReasoningText, stripReasoningTags, STREAMING_ELEMENT_ID, toCardKit2 } from './builder';
|
|
20
20
|
import { optimizeMarkdownStyle } from './markdown-style';
|
|
21
|
+
import { ImageResolver } from './image-resolver';
|
|
21
22
|
import { registerShutdownHook } from '../core/shutdown-hooks';
|
|
22
23
|
import { FlushController } from './flush-controller';
|
|
23
24
|
import { UnavailableGuard } from './unavailable-guard';
|
|
@@ -30,7 +31,11 @@ const STREAMING_THINKING_CARD = {
|
|
|
30
31
|
schema: '2.0',
|
|
31
32
|
config: {
|
|
32
33
|
streaming_mode: true,
|
|
33
|
-
|
|
34
|
+
locales: ['zh_cn', 'en_us'],
|
|
35
|
+
summary: {
|
|
36
|
+
content: 'Thinking...',
|
|
37
|
+
i18n_content: { zh_cn: '思考中...', en_us: 'Thinking...' },
|
|
38
|
+
},
|
|
34
39
|
},
|
|
35
40
|
body: {
|
|
36
41
|
elements: [
|
|
@@ -83,6 +88,7 @@ export class StreamingCardController {
|
|
|
83
88
|
// ---- Sub-controllers ----
|
|
84
89
|
flush;
|
|
85
90
|
guard;
|
|
91
|
+
imageResolver;
|
|
86
92
|
// ---- Lifecycle ----
|
|
87
93
|
createEpoch = 0;
|
|
88
94
|
_terminalReason = null;
|
|
@@ -105,6 +111,15 @@ export class StreamingCardController {
|
|
|
105
111
|
},
|
|
106
112
|
});
|
|
107
113
|
this.flush = new FlushController(() => this.performFlush());
|
|
114
|
+
this.imageResolver = new ImageResolver({
|
|
115
|
+
cfg: deps.cfg,
|
|
116
|
+
accountId: deps.accountId,
|
|
117
|
+
onImageResolved: () => {
|
|
118
|
+
if (!this.isTerminalPhase && this.cardKit.cardMessageId) {
|
|
119
|
+
void this.throttledCardUpdate();
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
});
|
|
108
123
|
}
|
|
109
124
|
// ------------------------------------------------------------------
|
|
110
125
|
// Public accessors
|
|
@@ -300,9 +315,10 @@ export class StreamingCardController {
|
|
|
300
315
|
const errorEffectiveCardId = this.cardKit.cardKitCardId ?? this.cardKit.originalCardKitCardId;
|
|
301
316
|
if (this.cardKit.cardMessageId) {
|
|
302
317
|
try {
|
|
303
|
-
const
|
|
318
|
+
const rawErrorText = this.text.accumulatedText
|
|
304
319
|
? `${this.text.accumulatedText}\n\n---\n**Error**: An error occurred while generating the response.`
|
|
305
320
|
: '**Error**: An error occurred while generating the response.';
|
|
321
|
+
const errorText = this.imageResolver.resolveImages(rawErrorText);
|
|
306
322
|
const errorCard = buildCardContent('complete', {
|
|
307
323
|
text: errorText,
|
|
308
324
|
reasoningText: this.reasoning.accumulatedReasoningText || undefined,
|
|
@@ -365,8 +381,9 @@ export class StreamingCardController {
|
|
|
365
381
|
if (!this.text.completedText && !this.text.accumulatedText) {
|
|
366
382
|
log.warn('reply completed without visible text, using empty-reply fallback');
|
|
367
383
|
}
|
|
384
|
+
const resolvedDisplayText = await this.imageResolver.resolveImagesAwait(displayText, 15_000);
|
|
368
385
|
const completeCard = buildCardContent('complete', {
|
|
369
|
-
text:
|
|
386
|
+
text: resolvedDisplayText,
|
|
370
387
|
reasoningText: this.reasoning.accumulatedReasoningText || undefined,
|
|
371
388
|
reasoningElapsedMs: this.reasoning.reasoningElapsedMs || undefined,
|
|
372
389
|
elapsedMs: this.elapsed(),
|
|
@@ -427,7 +444,7 @@ export class StreamingCardController {
|
|
|
427
444
|
const effectiveCardId = this.cardKit.cardKitCardId ?? this.cardKit.originalCardKitCardId;
|
|
428
445
|
if (effectiveCardId) {
|
|
429
446
|
const elapsedMs = Date.now() - this.dispatchStartTime;
|
|
430
|
-
const abortText = this.text.accumulatedText || 'Aborted.';
|
|
447
|
+
const abortText = this.imageResolver.resolveImages(this.text.accumulatedText || 'Aborted.');
|
|
431
448
|
const abortCardContent = buildCardContent('complete', {
|
|
432
449
|
text: abortText,
|
|
433
450
|
reasoningText: this.reasoning.accumulatedReasoningText || undefined,
|
|
@@ -442,7 +459,7 @@ export class StreamingCardController {
|
|
|
442
459
|
else if (this.cardKit.cardMessageId) {
|
|
443
460
|
// IM fallback: 卡片不是通过 CardKit 发的,用 im.message.patch 更新
|
|
444
461
|
const elapsedMs = Date.now() - this.dispatchStartTime;
|
|
445
|
-
const abortText = this.text.accumulatedText || 'Aborted.';
|
|
462
|
+
const abortText = this.imageResolver.resolveImages(this.text.accumulatedText || 'Aborted.');
|
|
446
463
|
const abortCard = buildCardContent('complete', {
|
|
447
464
|
text: abortText,
|
|
448
465
|
reasoningText: this.reasoning.accumulatedReasoningText || undefined,
|
|
@@ -602,6 +619,7 @@ export class StreamingCardController {
|
|
|
602
619
|
});
|
|
603
620
|
try {
|
|
604
621
|
const displayText = this.buildDisplayText();
|
|
622
|
+
const resolvedText = this.imageResolver.resolveImages(displayText);
|
|
605
623
|
if (this.cardKit.cardKitCardId) {
|
|
606
624
|
// CardKit streaming — typewriter effect
|
|
607
625
|
const prevSeq = this.cardKit.cardKitSequence;
|
|
@@ -614,7 +632,7 @@ export class StreamingCardController {
|
|
|
614
632
|
cfg: this.deps.cfg,
|
|
615
633
|
cardId: this.cardKit.cardKitCardId,
|
|
616
634
|
elementId: STREAMING_ELEMENT_ID,
|
|
617
|
-
content: optimizeMarkdownStyle(
|
|
635
|
+
content: optimizeMarkdownStyle(resolvedText),
|
|
618
636
|
sequence: this.cardKit.cardKitSequence,
|
|
619
637
|
accountId: this.deps.accountId,
|
|
620
638
|
});
|
|
@@ -622,7 +640,7 @@ export class StreamingCardController {
|
|
|
622
640
|
else {
|
|
623
641
|
log.debug('flushCardUpdate: IM patch fallback');
|
|
624
642
|
const card = buildCardContent('streaming', {
|
|
625
|
-
text: this.reasoning.isReasoningPhase ? '' :
|
|
643
|
+
text: this.reasoning.isReasoningPhase ? '' : resolvedText,
|
|
626
644
|
reasoningText: this.reasoning.isReasoningPhase ? this.reasoning.accumulatedReasoningText : undefined,
|
|
627
645
|
});
|
|
628
646
|
await updateCardFeishu({
|
package/src/commands/auth.d.ts
CHANGED
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
* 注意:此命令仅限应用 owner 执行(与 onboarding 逻辑一致)
|
|
9
9
|
*/
|
|
10
10
|
import type { OpenClawConfig } from 'openclaw/plugin-sdk';
|
|
11
|
+
import type { FeishuLocale } from './locale';
|
|
11
12
|
/**
|
|
12
13
|
* 执行飞书用户权限批量授权命令
|
|
13
14
|
* 直接调用 triggerOnboarding(),包含 owner 检查
|
|
14
15
|
*/
|
|
15
|
-
export declare function runFeishuAuth(config: OpenClawConfig): Promise<string>;
|
|
16
|
+
export declare function runFeishuAuth(config: OpenClawConfig, locale?: FeishuLocale): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* 运行飞书授权命令,同时生成中英双语结果。
|
|
19
|
+
* 副作用(triggerOnboarding)只执行一次,结果格式化为双语文本。
|
|
20
|
+
*/
|
|
21
|
+
export declare function runFeishuAuthI18n(config: OpenClawConfig): Promise<Record<FeishuLocale, string>>;
|