@larksuite/openclaw-lark 2026.3.29 → 2026.3.30-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -1
- package/src/card/builder.d.ts +4 -2
- package/src/card/builder.js +44 -29
- package/src/card/streaming-card-controller.js +40 -22
- package/src/core/accounts.js +23 -2
- package/src/core/lark-client.d.ts +10 -6
- package/src/core/lark-client.js +22 -7
- package/src/core/version.d.ts +8 -2
- package/src/core/version.js +19 -3
- package/src/messaging/outbound/actions.js +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@larksuite/openclaw-lark",
|
|
3
|
-
"version": "2026.3.
|
|
3
|
+
"version": "2026.3.30-beta.1",
|
|
4
4
|
"description": "OpenClaw Lark/Feishu channel plugin",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"openclaw": ">=2026.3.22"
|
|
33
33
|
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"openclaw": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
34
39
|
"openclaw": {
|
|
35
40
|
"extensions": [
|
|
36
41
|
"./index.js"
|
package/src/card/builder.d.ts
CHANGED
|
@@ -95,8 +95,10 @@ export declare function formatFooterRuntimeSegments(params: {
|
|
|
95
95
|
isError?: boolean;
|
|
96
96
|
isAborted?: boolean;
|
|
97
97
|
}): {
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
primaryZh: string[];
|
|
99
|
+
primaryEn: string[];
|
|
100
|
+
detailZh: string[];
|
|
101
|
+
detailEn: string[];
|
|
100
102
|
};
|
|
101
103
|
/**
|
|
102
104
|
* Build a full Feishu Interactive Message Card JSON object for the
|
package/src/card/builder.js
CHANGED
|
@@ -161,35 +161,46 @@ function compactNumber(value) {
|
|
|
161
161
|
}
|
|
162
162
|
function formatFooterRuntimeSegments(params) {
|
|
163
163
|
const { footer, metrics, elapsedMs, isError, isAborted } = params;
|
|
164
|
-
const
|
|
165
|
-
const
|
|
164
|
+
const primaryZh = [];
|
|
165
|
+
const primaryEn = [];
|
|
166
|
+
const detailZh = [];
|
|
167
|
+
const detailEn = [];
|
|
168
|
+
// --- Primary line: status, elapsed, model ---
|
|
166
169
|
if (footer?.status) {
|
|
167
170
|
if (isError) {
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
primaryZh.push('出错');
|
|
172
|
+
primaryEn.push('Error');
|
|
170
173
|
}
|
|
171
174
|
else if (isAborted) {
|
|
172
|
-
|
|
173
|
-
|
|
175
|
+
primaryZh.push('已停止');
|
|
176
|
+
primaryEn.push('Stopped');
|
|
174
177
|
}
|
|
175
178
|
else {
|
|
176
|
-
|
|
177
|
-
|
|
179
|
+
primaryZh.push('已完成');
|
|
180
|
+
primaryEn.push('Completed');
|
|
178
181
|
}
|
|
179
182
|
}
|
|
180
183
|
if (footer?.elapsed && elapsedMs != null) {
|
|
181
184
|
const d = formatElapsed(elapsedMs);
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
primaryZh.push(`耗时 ${d}`);
|
|
186
|
+
primaryEn.push(`Elapsed ${d}`);
|
|
184
187
|
}
|
|
188
|
+
if (footer?.model && metrics?.model) {
|
|
189
|
+
const model = metrics.model.trim();
|
|
190
|
+
if (model) {
|
|
191
|
+
primaryZh.push(model);
|
|
192
|
+
primaryEn.push(model);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// --- Detail line: tokens, cache, context ---
|
|
185
196
|
if (footer?.tokens && metrics) {
|
|
186
197
|
const inTokens = typeof metrics.inputTokens === 'number' ? Math.max(0, metrics.inputTokens) : undefined;
|
|
187
198
|
const outTokens = typeof metrics.outputTokens === 'number' ? Math.max(0, metrics.outputTokens) : undefined;
|
|
188
199
|
if (inTokens != null && outTokens != null) {
|
|
189
200
|
const inLabel = compactNumber(inTokens);
|
|
190
201
|
const outLabel = compactNumber(outTokens);
|
|
191
|
-
|
|
192
|
-
|
|
202
|
+
detailZh.push(`↑ ${inLabel} ↓ ${outLabel}`);
|
|
203
|
+
detailEn.push(`↑ ${inLabel} ↓ ${outLabel}`);
|
|
193
204
|
}
|
|
194
205
|
}
|
|
195
206
|
if (footer?.cache && metrics) {
|
|
@@ -201,8 +212,8 @@ function formatFooterRuntimeSegments(params) {
|
|
|
201
212
|
const hit = total > 0 ? Math.round((read / total) * 100) : 0;
|
|
202
213
|
const left = compactNumber(read);
|
|
203
214
|
const right = compactNumber(write);
|
|
204
|
-
|
|
205
|
-
|
|
215
|
+
detailZh.push(`缓存 ${left}/${right} (${hit}%)`);
|
|
216
|
+
detailEn.push(`Cache ${left}/${right} (${hit}%)`);
|
|
206
217
|
}
|
|
207
218
|
}
|
|
208
219
|
if (footer?.context && metrics) {
|
|
@@ -214,18 +225,11 @@ function formatFooterRuntimeSegments(params) {
|
|
|
214
225
|
const ctxLabel = compactNumber(ctx);
|
|
215
226
|
const pct = ctx > 0 ? Math.round((total / ctx) * 100) : 0;
|
|
216
227
|
const pctLabel = `${pct}%`;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (footer?.model && metrics?.model) {
|
|
222
|
-
const model = metrics.model.trim();
|
|
223
|
-
if (model) {
|
|
224
|
-
zhParts.push(model);
|
|
225
|
-
enParts.push(model);
|
|
228
|
+
detailZh.push(`上下文 ${totalLabel}/${ctxLabel} (${pctLabel})`);
|
|
229
|
+
detailEn.push(`Context ${totalLabel}/${ctxLabel} (${pctLabel})`);
|
|
226
230
|
}
|
|
227
231
|
}
|
|
228
|
-
return {
|
|
232
|
+
return { primaryZh, primaryEn, detailZh, detailEn };
|
|
229
233
|
}
|
|
230
234
|
// ---------------------------------------------------------------------------
|
|
231
235
|
// buildCardContent
|
|
@@ -369,17 +373,28 @@ function buildCompleteCard(params) {
|
|
|
369
373
|
text_size: 'notation',
|
|
370
374
|
});
|
|
371
375
|
}
|
|
372
|
-
// Footer meta-info:
|
|
373
|
-
//
|
|
374
|
-
|
|
376
|
+
// Footer meta-info: split into two lines for readability.
|
|
377
|
+
// Line 1 (primary): status · elapsed · model
|
|
378
|
+
// Line 2 (detail): tokens · cache · context
|
|
379
|
+
const fp = formatFooterRuntimeSegments({
|
|
375
380
|
footer,
|
|
376
381
|
metrics: footerMetrics,
|
|
377
382
|
elapsedMs,
|
|
378
383
|
isError,
|
|
379
384
|
isAborted,
|
|
380
385
|
});
|
|
381
|
-
|
|
382
|
-
|
|
386
|
+
const footerZhLines = [];
|
|
387
|
+
const footerEnLines = [];
|
|
388
|
+
if (fp.primaryZh.length > 0) {
|
|
389
|
+
footerZhLines.push(fp.primaryZh.join(' · '));
|
|
390
|
+
footerEnLines.push(fp.primaryEn.join(' · '));
|
|
391
|
+
}
|
|
392
|
+
if (fp.detailZh.length > 0) {
|
|
393
|
+
footerZhLines.push(fp.detailZh.join(' · '));
|
|
394
|
+
footerEnLines.push(fp.detailEn.join(' · '));
|
|
395
|
+
}
|
|
396
|
+
if (footerZhLines.length > 0) {
|
|
397
|
+
elements.push(...buildFooter(footerZhLines.join('\n'), footerEnLines.join('\n'), isError));
|
|
383
398
|
}
|
|
384
399
|
// Use the answer text (not reasoning) as the feed preview summary.
|
|
385
400
|
// Strip markdown syntax so the preview reads as plain text.
|
|
@@ -16,6 +16,7 @@ exports.StreamingCardController = void 0;
|
|
|
16
16
|
exports.prepareTerminalCardContent = prepareTerminalCardContent;
|
|
17
17
|
const promises_1 = require("node:fs/promises");
|
|
18
18
|
const reply_runtime_1 = require("openclaw/plugin-sdk/reply-runtime");
|
|
19
|
+
const agent_runtime_1 = require("openclaw/plugin-sdk/agent-runtime");
|
|
19
20
|
const api_error_1 = require("../core/api-error.js");
|
|
20
21
|
const lark_logger_1 = require("../core/lark-logger.js");
|
|
21
22
|
const lark_client_1 = require("../core/lark-client.js");
|
|
@@ -120,15 +121,37 @@ class StreamingCardController {
|
|
|
120
121
|
const cfgWithSession = this.deps.cfg;
|
|
121
122
|
const sessionStorePath = cfgWithSession.sessions?.store ?? cfgWithSession.session?.store;
|
|
122
123
|
const key = this.deps.sessionKey.trim().toLowerCase();
|
|
124
|
+
// WORKAROUND: SDK session key round-trip bug.
|
|
125
|
+
// The SDK's toAgentRequestSessionKey() strips the agent scope from keys
|
|
126
|
+
// like "agent:hr:main" → "main", then toAgentStoreSessionKey() rebuilds
|
|
127
|
+
// using the default agent ID → "agent:main:main". This means metrics
|
|
128
|
+
// written by the SDK always land under "agent:<defaultAgentId>:…"
|
|
129
|
+
// regardless of the account-scoped agent ID the plugin routing generated.
|
|
130
|
+
// Fallback: when the primary key misses, try replacing the agent-id
|
|
131
|
+
// segment with the resolved default agent ID.
|
|
132
|
+
// TODO: remove once the SDK preserves the original agent ID during the
|
|
133
|
+
// request→store key round-trip.
|
|
134
|
+
const defaultAgentId = (0, agent_runtime_1.resolveDefaultAgentId)(this.deps.cfg);
|
|
135
|
+
const fallbackKey = key.replace(/^(agent):[^:]+:/, `$1:${defaultAgentId}:`);
|
|
136
|
+
const candidateKeys = fallbackKey !== key ? [key, fallbackKey] : [key];
|
|
123
137
|
const sessionApi = runtime.agent?.session;
|
|
124
138
|
if (sessionApi?.resolveStorePath && sessionApi?.loadSessionStore) {
|
|
125
139
|
const storePath = sessionApi.resolveStorePath(sessionStorePath);
|
|
126
140
|
const store = sessionApi.loadSessionStore(storePath);
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
let entry;
|
|
142
|
+
let matchedKey;
|
|
143
|
+
for (const candidate of candidateKeys) {
|
|
144
|
+
const val = store[candidate];
|
|
145
|
+
if (val && typeof val === 'object') {
|
|
146
|
+
entry = val;
|
|
147
|
+
matchedKey = candidate;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!entry) {
|
|
129
152
|
log.debug('footer metrics lookup: session entry missing', {
|
|
130
153
|
sessionKey: this.deps.sessionKey,
|
|
131
|
-
|
|
154
|
+
candidateKeys,
|
|
132
155
|
storePath,
|
|
133
156
|
source: 'runtime.agent.session',
|
|
134
157
|
});
|
|
@@ -146,16 +169,9 @@ class StreamingCardController {
|
|
|
146
169
|
};
|
|
147
170
|
log.debug('footer metrics lookup: session entry found', {
|
|
148
171
|
sessionKey: this.deps.sessionKey,
|
|
149
|
-
|
|
172
|
+
matchedKey,
|
|
150
173
|
storePath,
|
|
151
174
|
source: 'runtime.agent.session',
|
|
152
|
-
hasMetrics: !!(metrics.inputTokens != null ||
|
|
153
|
-
metrics.outputTokens != null ||
|
|
154
|
-
metrics.cacheRead != null ||
|
|
155
|
-
metrics.cacheWrite != null ||
|
|
156
|
-
metrics.totalTokens != null ||
|
|
157
|
-
metrics.contextTokens != null ||
|
|
158
|
-
metrics.model),
|
|
159
175
|
});
|
|
160
176
|
return metrics;
|
|
161
177
|
}
|
|
@@ -169,11 +185,20 @@ class StreamingCardController {
|
|
|
169
185
|
const store = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
170
186
|
? parsed
|
|
171
187
|
: {};
|
|
172
|
-
|
|
173
|
-
|
|
188
|
+
let entry;
|
|
189
|
+
let matchedKey;
|
|
190
|
+
for (const candidate of candidateKeys) {
|
|
191
|
+
const val = store[candidate];
|
|
192
|
+
if (val && typeof val === 'object') {
|
|
193
|
+
entry = val;
|
|
194
|
+
matchedKey = candidate;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!entry) {
|
|
174
199
|
log.debug('footer metrics lookup: session entry missing', {
|
|
175
200
|
sessionKey: this.deps.sessionKey,
|
|
176
|
-
|
|
201
|
+
candidateKeys,
|
|
177
202
|
storePath,
|
|
178
203
|
source: 'channel.session.file',
|
|
179
204
|
});
|
|
@@ -191,16 +216,9 @@ class StreamingCardController {
|
|
|
191
216
|
};
|
|
192
217
|
log.debug('footer metrics lookup: session entry found', {
|
|
193
218
|
sessionKey: this.deps.sessionKey,
|
|
194
|
-
|
|
219
|
+
matchedKey,
|
|
195
220
|
storePath,
|
|
196
221
|
source: 'channel.session.file',
|
|
197
|
-
hasMetrics: !!(metrics.inputTokens != null ||
|
|
198
|
-
metrics.outputTokens != null ||
|
|
199
|
-
metrics.cacheRead != null ||
|
|
200
|
-
metrics.cacheWrite != null ||
|
|
201
|
-
metrics.totalTokens != null ||
|
|
202
|
-
metrics.contextTokens != null ||
|
|
203
|
-
metrics.model),
|
|
204
222
|
});
|
|
205
223
|
return metrics;
|
|
206
224
|
}
|
package/src/core/accounts.js
CHANGED
|
@@ -37,9 +37,30 @@ function baseConfig(section) {
|
|
|
37
37
|
const { accounts: _ignored, ...rest } = section;
|
|
38
38
|
return rest;
|
|
39
39
|
}
|
|
40
|
-
/** Merge base config with account override (account fields take precedence).
|
|
40
|
+
/** Merge base config with account override (account fields take precedence).
|
|
41
|
+
* Performs a one-level deep merge for plain-object fields so that partial
|
|
42
|
+
* account overrides (e.g. `footer: { model: false }`) are merged with
|
|
43
|
+
* the base instead of replacing the entire object. */
|
|
41
44
|
function mergeAccountConfig(base, override) {
|
|
42
|
-
|
|
45
|
+
const result = { ...base };
|
|
46
|
+
for (const [key, value] of Object.entries(override)) {
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
continue;
|
|
49
|
+
const baseVal = base[key];
|
|
50
|
+
// Deep-merge plain objects one level (footer, tools, heartbeat, etc.)
|
|
51
|
+
if (value &&
|
|
52
|
+
typeof value === 'object' &&
|
|
53
|
+
!Array.isArray(value) &&
|
|
54
|
+
baseVal &&
|
|
55
|
+
typeof baseVal === 'object' &&
|
|
56
|
+
!Array.isArray(baseVal)) {
|
|
57
|
+
result[key] = { ...baseVal, ...value };
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
result[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
43
64
|
}
|
|
44
65
|
/** Coerce a domain string to `LarkBrand`, defaulting to `"feishu"`. */
|
|
45
66
|
function toBrand(domain) {
|
|
@@ -106,15 +106,19 @@ export declare class LarkClient {
|
|
|
106
106
|
private waitForAbort;
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
|
-
* Returns the
|
|
109
|
+
* Returns the best available config for account resolution.
|
|
110
|
+
*
|
|
111
|
+
* Priority: live config (has `channels.feishu`) > fallback (has
|
|
112
|
+
* `channels.feishu`) > live config (last resort).
|
|
110
113
|
*
|
|
111
114
|
* The `config` object captured in tool-registration closures may be stale
|
|
112
|
-
* after a hot-reload
|
|
113
|
-
*
|
|
114
|
-
* `
|
|
115
|
-
*
|
|
115
|
+
* after a hot-reload, so we prefer the live config from
|
|
116
|
+
* `LarkClient.runtime.config.loadConfig()`. However, `loadConfig()` may
|
|
117
|
+
* return `{}` when the runtime config snapshot has been cleared (e.g. in
|
|
118
|
+
* isolated cron sessions), so we fall back to the closure-captured config
|
|
119
|
+
* when the live result lacks Feishu credentials.
|
|
116
120
|
*
|
|
117
121
|
* @param fallback - Config to use when the runtime is not yet initialised
|
|
118
|
-
*
|
|
122
|
+
* or when `loadConfig()` returns an incomplete config.
|
|
119
123
|
*/
|
|
120
124
|
export declare function getResolvedConfig(fallback: ClawdbotConfig): ClawdbotConfig;
|
package/src/core/lark-client.js
CHANGED
|
@@ -427,20 +427,35 @@ exports.LarkClient = LarkClient;
|
|
|
427
427
|
// Config resolution helper
|
|
428
428
|
// ---------------------------------------------------------------------------
|
|
429
429
|
/**
|
|
430
|
-
* Returns the
|
|
430
|
+
* Returns the best available config for account resolution.
|
|
431
|
+
*
|
|
432
|
+
* Priority: live config (has `channels.feishu`) > fallback (has
|
|
433
|
+
* `channels.feishu`) > live config (last resort).
|
|
431
434
|
*
|
|
432
435
|
* The `config` object captured in tool-registration closures may be stale
|
|
433
|
-
* after a hot-reload
|
|
434
|
-
*
|
|
435
|
-
* `
|
|
436
|
-
*
|
|
436
|
+
* after a hot-reload, so we prefer the live config from
|
|
437
|
+
* `LarkClient.runtime.config.loadConfig()`. However, `loadConfig()` may
|
|
438
|
+
* return `{}` when the runtime config snapshot has been cleared (e.g. in
|
|
439
|
+
* isolated cron sessions), so we fall back to the closure-captured config
|
|
440
|
+
* when the live result lacks Feishu credentials.
|
|
437
441
|
*
|
|
438
442
|
* @param fallback - Config to use when the runtime is not yet initialised
|
|
439
|
-
*
|
|
443
|
+
* or when `loadConfig()` returns an incomplete config.
|
|
440
444
|
*/
|
|
441
445
|
function getResolvedConfig(fallback) {
|
|
442
446
|
try {
|
|
443
|
-
|
|
447
|
+
const live = LarkClient.runtime.config.loadConfig();
|
|
448
|
+
// loadConfig() may return {} (empty config) when runtimeConfigSnapshot
|
|
449
|
+
// has been cleared (e.g. after writeConfigFile, secrets teardown, or
|
|
450
|
+
// concurrent cron race conditions in isolated sessions). In that case
|
|
451
|
+
// the closure-captured fallback still holds a valid resolved config.
|
|
452
|
+
if (live?.channels?.feishu)
|
|
453
|
+
return live;
|
|
454
|
+
if (fallback?.channels?.feishu) {
|
|
455
|
+
log.debug(`loadConfig() returned config without channels.feishu, using fallback`);
|
|
456
|
+
return fallback;
|
|
457
|
+
}
|
|
458
|
+
return live;
|
|
444
459
|
}
|
|
445
460
|
catch {
|
|
446
461
|
// runtime not yet initialised — fall back to passed config
|
package/src/core/version.d.ts
CHANGED
|
@@ -12,14 +12,20 @@
|
|
|
12
12
|
* @returns 版本号字符串,如 "2026.2.28.5";读取失败返回 "unknown"
|
|
13
13
|
*/
|
|
14
14
|
export declare function getPluginVersion(): string;
|
|
15
|
+
/**
|
|
16
|
+
* 获取当前运行平台名称
|
|
17
|
+
*
|
|
18
|
+
* @returns `mac` | `linux` | `windows`
|
|
19
|
+
*/
|
|
20
|
+
export declare function getPlatform(): string;
|
|
15
21
|
/**
|
|
16
22
|
* 生成 User-Agent 字符串
|
|
17
23
|
*
|
|
18
|
-
* @returns User-Agent 字符串,格式:`openclaw-lark/{version}`
|
|
24
|
+
* @returns User-Agent 字符串,格式:`openclaw-lark/{version}/{platform}`
|
|
19
25
|
*
|
|
20
26
|
* @example
|
|
21
27
|
* ```typescript
|
|
22
|
-
* getUserAgent() // => "openclaw-lark/2026.2.28.5"
|
|
28
|
+
* getUserAgent() // => "openclaw-lark/2026.2.28.5/mac"
|
|
23
29
|
* ```
|
|
24
30
|
*/
|
|
25
31
|
export declare function getUserAgent(): string;
|
package/src/core/version.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.getPluginVersion = getPluginVersion;
|
|
12
|
+
exports.getPlatform = getPlatform;
|
|
12
13
|
exports.getUserAgent = getUserAgent;
|
|
13
14
|
const node_url_1 = require("node:url");
|
|
14
15
|
const node_path_1 = require("node:path");
|
|
@@ -38,16 +39,31 @@ function getPluginVersion() {
|
|
|
38
39
|
return cachedVersion;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* 获取当前运行平台名称
|
|
44
|
+
*
|
|
45
|
+
* @returns `mac` | `linux` | `windows`
|
|
46
|
+
*/
|
|
47
|
+
function getPlatform() {
|
|
48
|
+
switch (process.platform) {
|
|
49
|
+
case 'darwin':
|
|
50
|
+
return 'mac';
|
|
51
|
+
case 'win32':
|
|
52
|
+
return 'windows';
|
|
53
|
+
default:
|
|
54
|
+
return 'linux';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
41
57
|
/**
|
|
42
58
|
* 生成 User-Agent 字符串
|
|
43
59
|
*
|
|
44
|
-
* @returns User-Agent 字符串,格式:`openclaw-lark/{version}`
|
|
60
|
+
* @returns User-Agent 字符串,格式:`openclaw-lark/{version}/{platform}`
|
|
45
61
|
*
|
|
46
62
|
* @example
|
|
47
63
|
* ```typescript
|
|
48
|
-
* getUserAgent() // => "openclaw-lark/2026.2.28.5"
|
|
64
|
+
* getUserAgent() // => "openclaw-lark/2026.2.28.5/mac"
|
|
49
65
|
* ```
|
|
50
66
|
*/
|
|
51
67
|
function getUserAgent() {
|
|
52
|
-
return `openclaw-lark/${getPluginVersion()}`;
|
|
68
|
+
return `openclaw-lark/${getPluginVersion()}/${getPlatform()}`;
|
|
53
69
|
}
|
|
@@ -55,9 +55,12 @@ const SUPPORTED_ACTIONS = new Set([
|
|
|
55
55
|
function parseCardParam(raw) {
|
|
56
56
|
if (raw == null)
|
|
57
57
|
return undefined;
|
|
58
|
-
// Already a non-array object — use directly.
|
|
58
|
+
// Already a non-array object — use directly (empty {} is never a valid card).
|
|
59
59
|
if (typeof raw === 'object' && !Array.isArray(raw)) {
|
|
60
|
-
|
|
60
|
+
const obj = raw;
|
|
61
|
+
if (Object.keys(obj).length === 0)
|
|
62
|
+
return undefined;
|
|
63
|
+
return obj;
|
|
61
64
|
}
|
|
62
65
|
// String — attempt JSON.parse.
|
|
63
66
|
if (typeof raw === 'string') {
|