@openclaw/line 2026.9.6 → 2026.9.7

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 (77) hide show
  1. package/assets/icon.png +0 -0
  2. package/dist/.setup/{accounts-BBEFMYbY.mjs → accounts-DOiM8-y3.mjs} +1 -4
  3. package/dist/.setup/api-4RgpQmHW.mjs +3 -0
  4. package/dist/.setup/{api-D_06cuHl.mjs → api-w4QdlCCB.mjs} +2 -2
  5. package/dist/.setup/{card-command-710u8FBH.mjs → card-command-DYf4gnmI.mjs} +25 -33
  6. package/dist/.setup/{channel-kc3VRglA.mjs → channel-DN6Za8Vm.mjs} +789 -26
  7. package/dist/.setup/channel.runtime-bd5vGeSo.mjs +4 -0
  8. package/dist/.setup/{monitor-morJPcPd.mjs → monitor-C01hAm3f.mjs} +262 -333
  9. package/dist/.setup/monitor.runtime-DwH2zK48.mjs +2 -0
  10. package/dist/.setup/outbound.runtime-D_Gs99Dm.mjs +3 -0
  11. package/dist/.setup/probe.runtime-Cj08-BZ1.mjs +2 -0
  12. package/dist/.setup/runtime-V7yh9OeC.mjs +8 -0
  13. package/dist/.setup/{send-BmsHY4WC.mjs → send-DOY1WTxv.mjs} +7 -26
  14. package/dist/.setup/{send-retry-DbfiHBPd.mjs → send-retry-DV6I0muq.mjs} +104 -208
  15. package/dist/.setup/{setup-surface-CcfKJKtr.mjs → setup-surface-CbfkmkIS.mjs} +15 -4
  16. package/dist/.setup/{markdown-to-line-CRfEHFs0.mjs → template-messages-CxCnYsWN.mjs} +168 -189
  17. package/dist/api.js +2 -2
  18. package/dist/channel-plugin-api.js +1 -1
  19. package/dist/contract-api.js +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/runtime-api.js +2 -260
  22. package/dist/setup-api.js +1 -1
  23. package/dist/setup-entry.js +1 -1
  24. package/node_modules/@line/bot-sdk/node_modules/@types/node/README.md +1 -1
  25. package/node_modules/@line/bot-sdk/node_modules/@types/node/child_process.d.ts +1 -1
  26. package/node_modules/@line/bot-sdk/node_modules/@types/node/package.json +2 -2
  27. package/node_modules/typebox/build/compile/code.mjs +1 -2
  28. package/node_modules/typebox/build/compile/validator.mjs +5 -1
  29. package/node_modules/typebox/build/format/iri.mjs +16 -9
  30. package/node_modules/typebox/build/guard/emit.mjs +2 -2
  31. package/node_modules/typebox/build/guard/guard.d.mts +7 -5
  32. package/node_modules/typebox/build/guard/guard.mjs +14 -9
  33. package/node_modules/typebox/build/guard/index.mjs +6 -0
  34. package/node_modules/typebox/build/guard/unicode/unicode.d.mts +6 -0
  35. package/node_modules/typebox/build/guard/unicode/unicode.mjs +35 -0
  36. package/node_modules/typebox/build/guard/{string.mjs → unicode/unicode_segment.mjs} +16 -19
  37. package/node_modules/typebox/build/schema/build.mjs +2 -3
  38. package/node_modules/typebox/build/schema/engine/_pathing.d.mts +1 -0
  39. package/node_modules/typebox/build/schema/engine/_pathing.mjs +11 -0
  40. package/node_modules/typebox/build/schema/engine/_refine.mjs +13 -5
  41. package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +2 -1
  42. package/node_modules/typebox/build/schema/engine/dependencies.mjs +2 -1
  43. package/node_modules/typebox/build/schema/engine/dependentSchemas.mjs +2 -1
  44. package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +1 -1
  45. package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +2 -2
  46. package/node_modules/typebox/build/schema/engine/format.mjs +9 -2
  47. package/node_modules/typebox/build/schema/engine/patternProperties.mjs +3 -2
  48. package/node_modules/typebox/build/schema/engine/properties.mjs +3 -2
  49. package/node_modules/typebox/build/schema/engine/propertyNames.mjs +2 -1
  50. package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +1 -1
  51. package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +2 -2
  52. package/node_modules/typebox/build/schema/engine/ref.d.mts +1 -1
  53. package/node_modules/typebox/build/schema/engine/ref.mjs +2 -2
  54. package/node_modules/typebox/build/schema/engine/schema.mjs +24 -24
  55. package/node_modules/typebox/build/schema/parse.mjs +3 -0
  56. package/node_modules/typebox/build/schema/pointer/pointer.d.mts +1 -6
  57. package/node_modules/typebox/build/schema/pointer/pointer.mjs +47 -64
  58. package/node_modules/typebox/build/system/settings/internal.d.mts +2 -0
  59. package/node_modules/typebox/build/system/settings/internal.mjs +20 -0
  60. package/node_modules/typebox/build/system/settings/settings.d.mts +11 -0
  61. package/node_modules/typebox/build/system/settings/settings.mjs +14 -0
  62. package/node_modules/typebox/build/type/extends/inference.d.mts +1 -1
  63. package/node_modules/typebox/build/type/extends/inference.mjs +15 -2
  64. package/node_modules/typebox/build/type/extends/tuple.d.mts +2 -1
  65. package/node_modules/typebox/build/type/extends/tuple.mjs +21 -1
  66. package/node_modules/typebox/build/value/parse/parse.mjs +10 -4
  67. package/node_modules/typebox/package.json +1 -1
  68. package/node_modules/typebox/readme.md +3 -1
  69. package/package.json +5 -5
  70. package/dist/.setup/account-helpers-BSUyZwLm.mjs +0 -12
  71. package/dist/.setup/api-RdEuQgW-.mjs +0 -3
  72. package/dist/.setup/channel.runtime-BtkeDELb.mjs +0 -4
  73. package/dist/.setup/monitor.runtime-BGhxYDKG.mjs +0 -2
  74. package/dist/.setup/outbound.runtime-QGvZy8Em.mjs +0 -3
  75. package/dist/.setup/probe.runtime-CMf8b-jB.mjs +0 -2
  76. package/dist/.setup/rich-messages-d8CmBVJj.mjs +0 -872
  77. /package/node_modules/typebox/build/guard/{string.d.mts → unicode/unicode_segment.d.mts} +0 -0
@@ -1,872 +0,0 @@
1
- import { i as resolveLineAccount } from "./accounts-BBEFMYbY.mjs";
2
- import { t as hasLineCredentials } from "./account-helpers-BSUyZwLm.mjs";
3
- import { F as inferLineTargetChatType, S as fitsLineFlexBubble, b as truncateLineActionLabel, c as createAgendaCard, d as createActionCard, g as messageAction, l as createEventCard, y as postbackAction } from "./send-retry-DbfiHBPd.mjs";
4
- import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
5
- import { resolveAccountEntry } from "openclaw/plugin-sdk/account-resolution";
6
- import { isRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
7
- import { ChannelDeliveryStreamingConfigSchema, DmPolicySchema, GroupPolicySchema, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
8
- import { requireChannelOpenAllowFrom } from "openclaw/plugin-sdk/extension-shared";
9
- import { z } from "zod";
10
- import { firstDefined as firstDefined$1 } from "openclaw/plugin-sdk/allow-from";
11
- import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
12
- import { resolveAskUserQuestionOptionIndex, resolveAskUserQuestionOptionIndices } from "openclaw/plugin-sdk/reply-payload";
13
- import { normalizeMessagePresentation, renderMessagePresentationFallbackText, renderPresentationForDelivery, resolveMessagePresentationButtonAction, resolveMessagePresentationOptionAction } from "openclaw/plugin-sdk/interactive-runtime";
14
- import { Type } from "typebox";
15
- //#region extensions/line/src/config-schema.ts
16
- const ThreadBindingsSchema = z.object({
17
- enabled: z.boolean().optional(),
18
- idleHours: z.number().optional(),
19
- maxAgeHours: z.number().optional(),
20
- spawnSessions: z.boolean().optional(),
21
- defaultSpawnContext: z.enum(["isolated", "fork"]).optional()
22
- }).strict();
23
- const LineReplyToModeSchema = z.enum([
24
- "off",
25
- "first",
26
- "all"
27
- ]);
28
- const LineCommonConfigSchemaBase = z.object({
29
- enabled: z.boolean().optional(),
30
- configWrites: z.boolean().optional(),
31
- joinIntro: z.boolean().optional(),
32
- historyLimit: z.number().int().min(0).optional(),
33
- channelAccessToken: z.string().optional(),
34
- channelSecret: z.string().optional(),
35
- tokenFile: z.string().optional(),
36
- secretFile: z.string().optional(),
37
- name: z.string().optional(),
38
- allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
39
- groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
40
- dmPolicy: DmPolicySchema.optional().default("pairing"),
41
- groupPolicy: GroupPolicySchema.optional().default("allowlist"),
42
- responsePrefix: z.string().optional(),
43
- replyToMode: LineReplyToModeSchema.optional(),
44
- streaming: ChannelDeliveryStreamingConfigSchema.optional(),
45
- mediaMaxMb: z.number().optional(),
46
- webhookPath: z.string().optional(),
47
- threadBindings: ThreadBindingsSchema.optional()
48
- });
49
- const LineGroupConfigSchema = buildGroupEntrySchema().omit({
50
- tools: true,
51
- toolsBySender: true
52
- });
53
- const LineAccountConfigSchema = LineCommonConfigSchemaBase.extend({ groups: z.record(z.string(), LineGroupConfigSchema.optional()).optional() }).strict();
54
- const LineConfigSchema = buildMultiAccountChannelSchema(LineAccountConfigSchema, {
55
- optionalAccount: true,
56
- refine: (value, ctx) => {
57
- requireChannelOpenAllowFrom({
58
- channel: "line",
59
- policy: value.dmPolicy,
60
- allowFrom: value.allowFrom,
61
- ctx,
62
- requireOpenAllowFrom
63
- });
64
- }
65
- });
66
- const LineChannelConfigSchema = buildChannelConfigSchema(LineConfigSchema, { uiHints: { joinIntro: {
67
- label: "LINE Group Join Introduction",
68
- help: "Post one brief introduction when the bot joins an allowed LINE group or multi-person room (default: true). Account settings override the channel-wide setting."
69
- } } });
70
- //#endregion
71
- //#region extensions/line/src/bot-access.ts
72
- function normalizeLineAllowEntry(value) {
73
- const trimmed = String(value).trim();
74
- if (!trimmed) return "";
75
- if (trimmed === "*") return "*";
76
- return trimmed.replace(/^line:(?:user:)?/i, "");
77
- }
78
- const normalizeAllowFrom = (list) => {
79
- const entries = (list ?? []).map((value) => normalizeLineAllowEntry(value)).filter(Boolean);
80
- return {
81
- entries,
82
- hasWildcard: entries.includes("*"),
83
- hasEntries: entries.length > 0
84
- };
85
- };
86
- //#endregion
87
- //#region extensions/line/src/group-keys.ts
88
- function resolveLineGroupLookupIds(groupId) {
89
- const normalized = groupId?.trim();
90
- if (!normalized) return [];
91
- if (normalized.startsWith("group:") || normalized.startsWith("room:")) {
92
- const rawId = normalized.split(":").slice(1).join(":");
93
- return rawId ? [rawId, normalized] : [normalized];
94
- }
95
- return [
96
- normalized,
97
- `group:${normalized}`,
98
- `room:${normalized}`
99
- ];
100
- }
101
- function resolveLineGroupConfigEntry(groups, params) {
102
- if (!groups) return;
103
- const defaults = groups["*"];
104
- for (const candidate of [...resolveLineGroupLookupIds(params.groupId), ...resolveLineGroupLookupIds(params.roomId)]) {
105
- const hit = groups[candidate];
106
- if (hit) return defaults && defaults !== hit ? {
107
- ...defaults,
108
- ...hit
109
- } : hit;
110
- }
111
- return defaults;
112
- }
113
- function resolveLineGroupsConfig(cfg, accountId) {
114
- const lineConfig = cfg.channels?.line;
115
- if (!lineConfig) return;
116
- const normalizedAccountId = normalizeAccountId(accountId);
117
- return resolveAccountEntry(lineConfig.accounts, normalizedAccountId)?.groups ?? lineConfig.groups;
118
- }
119
- function resolveExactLineGroupConfigKey(params) {
120
- const { groups } = params;
121
- if (!groups) return;
122
- return resolveLineGroupLookupIds(params.groupId).find((candidate) => Object.hasOwn(groups, candidate));
123
- }
124
- //#endregion
125
- //#region extensions/line/src/runtime.ts
126
- const { setRuntime: setLineRuntime, getRuntime: getLineRuntime } = createPluginRuntimeStore({
127
- pluginId: "line",
128
- errorMessage: "LINE runtime not initialized - plugin not registered"
129
- });
130
- //#endregion
131
- //#region extensions/line/src/quick-reply-fallback.ts
132
- function buildLineQuickReplyFallbackText(labels) {
133
- const normalized = normalizeStringEntries(labels ?? []).slice(0, 13);
134
- if (normalized.length === 0) return "Choose an option.";
135
- return `Options:\n${normalized.map((label) => `- ${label}`).join("\n")}`;
136
- }
137
- //#endregion
138
- //#region extensions/line/src/flex-templates/media-control-cards.ts
139
- function horizontalRow(contents, options = {}) {
140
- return {
141
- type: "box",
142
- layout: "horizontal",
143
- contents,
144
- ...options
145
- };
146
- }
147
- /**
148
- * Create a media player card for Sonos, Spotify, Apple Music, etc.
149
- *
150
- * Editorial design: Album art hero with gradient overlay for text,
151
- * prominent now-playing indicator, refined playback controls.
152
- */
153
- function createMediaPlayerCard(params) {
154
- const { title, subtitle, source, imageUrl, isPlaying, progress, controls, extraActions } = params;
155
- const trackInfo = [{
156
- type: "text",
157
- text: title,
158
- weight: "bold",
159
- size: "xl",
160
- color: "#111111",
161
- wrap: true
162
- }];
163
- if (subtitle) trackInfo.push({
164
- type: "text",
165
- text: subtitle,
166
- size: "md",
167
- color: "#666666",
168
- wrap: true,
169
- margin: "sm"
170
- });
171
- const statusItems = [];
172
- if (isPlaying !== void 0) statusItems.push(horizontalRow([{
173
- type: "box",
174
- layout: "vertical",
175
- contents: [],
176
- width: "8px",
177
- height: "8px",
178
- backgroundColor: isPlaying ? "#06C755" : "#CCCCCC",
179
- cornerRadius: "4px"
180
- }, {
181
- type: "text",
182
- text: isPlaying ? "Now Playing" : "Paused",
183
- size: "xs",
184
- color: isPlaying ? "#06C755" : "#888888",
185
- weight: "bold",
186
- margin: "sm"
187
- }], { alignItems: "center" }));
188
- if (source) statusItems.push({
189
- type: "text",
190
- text: source,
191
- size: "xs",
192
- color: "#AAAAAA",
193
- margin: statusItems.length > 0 ? "lg" : void 0
194
- });
195
- if (progress) statusItems.push({
196
- type: "text",
197
- text: progress,
198
- size: "xs",
199
- color: "#888888",
200
- align: "end",
201
- flex: 1
202
- });
203
- const bodyContents = [{
204
- type: "box",
205
- layout: "vertical",
206
- contents: trackInfo
207
- }];
208
- if (statusItems.length > 0) bodyContents.push(horizontalRow(statusItems, {
209
- margin: "lg",
210
- alignItems: "center"
211
- }));
212
- const bubble = {
213
- type: "bubble",
214
- size: "mega",
215
- body: {
216
- type: "box",
217
- layout: "vertical",
218
- contents: bodyContents,
219
- paddingAll: "xl",
220
- backgroundColor: "#FFFFFF"
221
- }
222
- };
223
- if (imageUrl) bubble.hero = {
224
- type: "image",
225
- url: imageUrl,
226
- size: "full",
227
- aspectRatio: "1:1",
228
- aspectMode: "cover"
229
- };
230
- if (controls || extraActions?.length) {
231
- const footerContents = [];
232
- if (controls) {
233
- const controlButtons = [];
234
- for (const [key, label, style] of [
235
- [
236
- "previous",
237
- "⏮",
238
- "secondary"
239
- ],
240
- [
241
- "play",
242
- "▶",
243
- isPlaying ? "secondary" : "primary"
244
- ],
245
- [
246
- "pause",
247
- "⏸",
248
- isPlaying ? "primary" : "secondary"
249
- ],
250
- [
251
- "next",
252
- "⏭",
253
- "secondary"
254
- ]
255
- ]) {
256
- const control = controls[key];
257
- if (!control) continue;
258
- const button = {
259
- type: "button",
260
- action: postbackAction(label, control.data),
261
- style,
262
- flex: 1,
263
- height: "sm"
264
- };
265
- if (key !== "previous") button.margin = controlButtons.length > 0 ? "md" : void 0;
266
- controlButtons.push(button);
267
- }
268
- if (controlButtons.length > 0) footerContents.push(horizontalRow(controlButtons));
269
- }
270
- if (extraActions?.length) footerContents.push(horizontalRow(extraActions.slice(0, 2).map((action, index) => ({
271
- type: "button",
272
- action: postbackAction(truncateLineActionLabel(action.label, 15), action.data),
273
- style: "secondary",
274
- flex: 1,
275
- height: "sm",
276
- margin: index > 0 ? "md" : void 0
277
- })), { margin: "md" }));
278
- if (footerContents.length > 0) bubble.footer = {
279
- type: "box",
280
- layout: "vertical",
281
- contents: footerContents,
282
- paddingAll: "lg",
283
- backgroundColor: "#FAFAFA"
284
- };
285
- }
286
- return bubble;
287
- }
288
- /**
289
- * Create an Apple TV remote card with a D-pad and control rows.
290
- */
291
- function createAppleTvRemoteCard(params) {
292
- const { deviceName, status, actionData } = params;
293
- const headerContents = [{
294
- type: "text",
295
- text: deviceName,
296
- weight: "bold",
297
- size: "xl",
298
- color: "#111111",
299
- wrap: true
300
- }];
301
- if (status) headerContents.push({
302
- type: "text",
303
- text: status,
304
- size: "sm",
305
- color: "#666666",
306
- wrap: true,
307
- margin: "sm"
308
- });
309
- const makeButton = (label, data, style = "secondary") => ({
310
- type: "button",
311
- action: postbackAction(label, data),
312
- style,
313
- height: "sm",
314
- flex: 1
315
- });
316
- const controlRows = [
317
- horizontalRow([
318
- { type: "filler" },
319
- makeButton("↑", actionData.up),
320
- { type: "filler" }
321
- ]),
322
- horizontalRow([
323
- makeButton("←", actionData.left),
324
- makeButton("OK", actionData.select, "primary"),
325
- makeButton("→", actionData.right)
326
- ], { margin: "md" }),
327
- horizontalRow([
328
- { type: "filler" },
329
- makeButton("↓", actionData.down),
330
- { type: "filler" }
331
- ], { margin: "md" }),
332
- horizontalRow([makeButton("Menu", actionData.menu), makeButton("Home", actionData.home)], { margin: "lg" }),
333
- horizontalRow([makeButton("Play", actionData.play), makeButton("Pause", actionData.pause)], { margin: "md" }),
334
- horizontalRow([
335
- makeButton("Vol +", actionData.volumeUp),
336
- makeButton("Mute", actionData.mute),
337
- makeButton("Vol -", actionData.volumeDown)
338
- ], { margin: "md" })
339
- ];
340
- return {
341
- type: "bubble",
342
- size: "mega",
343
- body: {
344
- type: "box",
345
- layout: "vertical",
346
- contents: [
347
- {
348
- type: "box",
349
- layout: "vertical",
350
- contents: headerContents
351
- },
352
- {
353
- type: "separator",
354
- margin: "lg",
355
- color: "#EEEEEE"
356
- },
357
- ...controlRows
358
- ],
359
- paddingAll: "xl",
360
- backgroundColor: "#FFFFFF"
361
- }
362
- };
363
- }
364
- /**
365
- * Create a device control card for Apple TV, smart home devices, etc.
366
- *
367
- * Editorial design: Device-focused header with status indicator,
368
- * clean control grid with clear visual hierarchy.
369
- */
370
- function createDeviceControlCard(params) {
371
- const { deviceName, deviceType, status, isOnline, imageUrl, controls } = params;
372
- const headerContents = [horizontalRow([{
373
- type: "box",
374
- layout: "vertical",
375
- contents: [],
376
- width: "10px",
377
- height: "10px",
378
- backgroundColor: isOnline !== false ? "#06C755" : "#FF5555",
379
- cornerRadius: "5px"
380
- }, {
381
- type: "text",
382
- text: deviceName,
383
- weight: "bold",
384
- size: "xl",
385
- color: "#111111",
386
- wrap: true,
387
- flex: 1,
388
- margin: "md"
389
- }], { alignItems: "center" })];
390
- if (deviceType) headerContents.push({
391
- type: "text",
392
- text: deviceType,
393
- size: "sm",
394
- color: "#888888",
395
- margin: "sm"
396
- });
397
- if (status) headerContents.push({
398
- type: "box",
399
- layout: "vertical",
400
- contents: [{
401
- type: "text",
402
- text: status,
403
- size: "sm",
404
- color: "#444444",
405
- wrap: true
406
- }],
407
- margin: "lg",
408
- paddingAll: "md",
409
- backgroundColor: "#F8F9FA",
410
- cornerRadius: "md"
411
- });
412
- const bubble = {
413
- type: "bubble",
414
- size: "mega",
415
- body: {
416
- type: "box",
417
- layout: "vertical",
418
- contents: headerContents,
419
- paddingAll: "xl",
420
- backgroundColor: "#FFFFFF"
421
- }
422
- };
423
- if (imageUrl) bubble.hero = {
424
- type: "image",
425
- url: imageUrl,
426
- size: "full",
427
- aspectRatio: "16:9",
428
- aspectMode: "cover"
429
- };
430
- if (controls.length > 0) {
431
- const rows = [];
432
- const limitedControls = controls.slice(0, 6);
433
- for (let i = 0; i < limitedControls.length; i += 2) {
434
- const rowButtons = [];
435
- for (const [offset, ctrl] of limitedControls.slice(i, i + 2).entries()) {
436
- const buttonLabel = ctrl.icon ? `${ctrl.icon} ${ctrl.label}` : ctrl.label;
437
- rowButtons.push({
438
- type: "button",
439
- action: postbackAction(truncateLineActionLabel(buttonLabel, 18), ctrl.data),
440
- style: ctrl.style ?? "secondary",
441
- flex: 1,
442
- height: "sm",
443
- margin: offset > 0 ? "md" : void 0
444
- });
445
- }
446
- if (rowButtons.length === 1) rowButtons.push({ type: "filler" });
447
- rows.push(horizontalRow(rowButtons, { margin: i > 0 ? "md" : void 0 }));
448
- }
449
- bubble.footer = {
450
- type: "box",
451
- layout: "vertical",
452
- contents: rows,
453
- paddingAll: "lg",
454
- backgroundColor: "#FAFAFA"
455
- };
456
- }
457
- return bubble;
458
- }
459
- //#endregion
460
- //#region extensions/line/src/question-postback.ts
461
- const QUESTION_ID_PARAM = "line.question";
462
- const OPTION_INDEX_PARAM = "line.option";
463
- const POSTBACK_DATA_MAX_BYTES = 300;
464
- function withinPostbackLimit(data) {
465
- return Buffer.byteLength(data, "utf8") <= POSTBACK_DATA_MAX_BYTES ? data : void 0;
466
- }
467
- /** Encodes one ask_user choice into LINE postback data, or nothing when it cannot fit. */
468
- function buildLineQuestionPostbackData(callback) {
469
- const questionId = normalizeOptionalString(callback.questionId);
470
- if (!questionId) return;
471
- const params = new URLSearchParams({ [QUESTION_ID_PARAM]: questionId });
472
- if (!Number.isInteger(callback.optionIndex) || callback.optionIndex < 0) return;
473
- params.set(OPTION_INDEX_PARAM, String(callback.optionIndex));
474
- return withinPostbackLimit(params.toString());
475
- }
476
- /** Reads a question choice back out of inbound postback data, if it carries one. */
477
- function parseLineQuestionPostbackData(data) {
478
- if (!data.includes(QUESTION_ID_PARAM)) return;
479
- const params = new URLSearchParams(data);
480
- const questionId = normalizeOptionalString(params.get(QUESTION_ID_PARAM));
481
- if (!questionId) return;
482
- const rawIndex = normalizeOptionalString(params.get(OPTION_INDEX_PARAM));
483
- if (!rawIndex || !/^\d+$/.test(rawIndex)) return;
484
- return {
485
- questionId,
486
- optionIndex: Number(rawIndex)
487
- };
488
- }
489
- /**
490
- * Submit an ask_user choice a LINE tap carried, and report only what the user needs.
491
- *
492
- * A successful answer needs no acknowledgement: the agent's own next reply is the
493
- * feedback, and LINE already echoes the chosen label through the action's
494
- * `displayText`. Only an answer that cannot land says so.
495
- */
496
- async function resolveLineQuestionPostback(params) {
497
- try {
498
- const { questionGatewayRuntime } = await import("openclaw/plugin-sdk/question-gateway-runtime");
499
- const result = await questionGatewayRuntime.resolveOption({
500
- cfg: params.cfg,
501
- questionId: params.callback.questionId,
502
- senderId: params.senderId,
503
- clientDisplayName: `LINE question (${params.accountId})`,
504
- optionIndex: params.callback.optionIndex,
505
- authorize: params.authorize
506
- });
507
- if (result.status === "answered" || result.status === "already-terminal" || result.status === "denied") return { status: result.status };
508
- return { status: "failed" };
509
- } catch {
510
- return { status: "failed" };
511
- }
512
- }
513
- //#endregion
514
- //#region extensions/line/src/rich-messages.ts
515
- const nonempty = () => Type.String({ minLength: 1 });
516
- const closed = (properties) => Type.Object(properties, { additionalProperties: false });
517
- const lineCardSchema = Type.Union([
518
- closed({
519
- type: Type.Literal("media_player"),
520
- title: nonempty(),
521
- artist: Type.Optional(nonempty()),
522
- source: Type.Optional(nonempty()),
523
- imageUrl: Type.Optional(Type.String({ pattern: "^https://" })),
524
- status: Type.Optional(Type.Union([Type.Literal("playing"), Type.Literal("paused")]))
525
- }),
526
- closed({
527
- type: Type.Literal("event"),
528
- title: nonempty(),
529
- date: nonempty(),
530
- time: Type.Optional(nonempty()),
531
- location: Type.Optional(nonempty()),
532
- description: Type.Optional(nonempty())
533
- }),
534
- closed({
535
- type: Type.Literal("agenda"),
536
- title: nonempty(),
537
- events: Type.Array(closed({
538
- title: nonempty(),
539
- time: Type.Optional(nonempty()),
540
- location: Type.Optional(nonempty())
541
- }), {
542
- minItems: 1,
543
- maxItems: 6
544
- })
545
- }),
546
- closed({
547
- type: Type.Literal("device"),
548
- name: nonempty(),
549
- deviceType: Type.Optional(nonempty()),
550
- status: Type.Optional(nonempty()),
551
- controls: Type.Optional(Type.Array(closed({
552
- label: nonempty(),
553
- action: nonempty()
554
- }), { maxItems: 6 }))
555
- }),
556
- closed({
557
- type: Type.Literal("appletv_remote"),
558
- name: Type.Optional(nonempty()),
559
- status: Type.Optional(nonempty())
560
- })
561
- ]);
562
- const lineChannelDataSchema = Type.Optional(closed({ line: closed({
563
- location: Type.Optional(closed({
564
- title: nonempty(),
565
- address: nonempty(),
566
- latitude: Type.Number({
567
- minimum: -90,
568
- maximum: 90
569
- }),
570
- longitude: Type.Number({
571
- minimum: -180,
572
- maximum: 180
573
- })
574
- })),
575
- card: Type.Optional(lineCardSchema),
576
- mediaKind: Type.Optional(Type.Union([
577
- Type.Literal("image"),
578
- Type.Literal("video"),
579
- Type.Literal("audio")
580
- ])),
581
- previewImageUrl: Type.Optional(Type.String({ pattern: "^https://" })),
582
- durationMs: Type.Optional(Type.Integer({ minimum: 1 })),
583
- trackingId: Type.Optional(nonempty())
584
- }) }));
585
- const lineMessageActions = {
586
- describeMessageTool: ({ cfg, accountId }) => {
587
- const account = resolveLineAccount({
588
- cfg,
589
- accountId: accountId ?? void 0
590
- });
591
- return account.enabled && hasLineCredentials(account) ? {
592
- actions: ["send"],
593
- capabilities: ["presentation"],
594
- schema: {
595
- actions: ["send"],
596
- properties: { channelData: lineChannelDataSchema }
597
- }
598
- } : {
599
- actions: [],
600
- capabilities: [],
601
- schema: null
602
- };
603
- },
604
- prepareSendPayload: ({ payload }) => payload
605
- };
606
- const LINE_QUICK_REPLY_LIMIT = 13;
607
- const LINE_PRESENTATION_CAPABILITIES = {
608
- supported: true,
609
- buttons: true,
610
- selects: true,
611
- context: true,
612
- limits: {
613
- actions: {
614
- maxActions: 4,
615
- maxActionsPerRow: 1,
616
- maxRows: 4,
617
- maxLabelLength: 40
618
- },
619
- selects: {
620
- maxOptions: LINE_QUICK_REPLY_LIMIT,
621
- maxValueBytes: 300
622
- },
623
- text: { markdownDialect: "plain" }
624
- }
625
- };
626
- /**
627
- * Reads the choice one question button carries. The Gateway owns option order, so a
628
- * tap sends the index it published, never the rendered label; a choice it no longer
629
- * lists renders no button at all rather than a tap that answers the wrong option.
630
- */
631
- function toLineQuestionChoice(action, questionOptionIndices) {
632
- if ("intent" in action) return;
633
- const optionIndex = resolveAskUserQuestionOptionIndex({
634
- questionOptionIndices,
635
- questionId: action.questionId,
636
- optionValue: action.optionValue
637
- });
638
- return optionIndex === void 0 ? void 0 : {
639
- questionId: action.questionId,
640
- optionIndex
641
- };
642
- }
643
- /**
644
- * The free-text control is not drawn. LINE can open the composer on a tap
645
- * (`inputOption: "openKeyboard"`), so the platform is not the reason: an answer
646
- * is claimed only on the plain-text inbound path, which no postback reaches, so
647
- * the button cannot change whether what follows it counts as the answer. It
648
- * would add a tap that changes nothing the card's own words already offer, which
649
- * is why Discord and Slack leave that route in text too.
650
- */
651
- function isLineTextFallbackButton(button) {
652
- const action = resolveMessagePresentationButtonAction(button);
653
- return action?.type === "question" && "intent" in action && action.intent === "custom-input";
654
- }
655
- /** A control the Gateway owns, whose label the operator cannot disambiguate. */
656
- function isLineQuestionButton(button) {
657
- return resolveMessagePresentationButtonAction(button)?.type === "question";
658
- }
659
- function toLineAction(button, questionOptionIndices) {
660
- const normalized = resolveMessagePresentationButtonAction(button);
661
- const { label } = button;
662
- if (normalized?.type === "question") {
663
- const choice = toLineQuestionChoice(normalized, questionOptionIndices);
664
- const data = choice && buildLineQuestionPostbackData(choice);
665
- if (!data) return;
666
- return {
667
- type: "postback",
668
- label,
669
- data,
670
- displayText: label
671
- };
672
- }
673
- if (normalized?.type === "command") return {
674
- type: "message",
675
- label,
676
- text: normalized.command
677
- };
678
- if (normalized?.type === "callback") return {
679
- type: "postback",
680
- label,
681
- data: normalized.value,
682
- displayText: label
683
- };
684
- if (normalized?.type === "url") return {
685
- type: "uri",
686
- label,
687
- uri: normalized.url
688
- };
689
- if (normalized?.type === "web-app" && normalized.url) return {
690
- type: "uri",
691
- label,
692
- uri: normalized.url
693
- };
694
- }
695
- function renderLinePresentation(payload, presentation, to, sourcePresentation = presentation) {
696
- const hasQuestion = sourcePresentation.blocks.some((block) => block.type === "buttons" && block.buttons.some(isLineQuestionButton));
697
- const hasAuthoredPrompt = Boolean(sourcePresentation.title?.trim()) || sourcePresentation.blocks.some((block) => (block.type === "text" || block.type === "context") && block.text.trim());
698
- if (hasQuestion && !hasAuthoredPrompt) return null;
699
- if (inferLineTargetChatType(to ?? "") !== "direct" && hasQuestion) return null;
700
- const hasCard = presentation.blocks.some((block) => block.type === "buttons" && block.buttons.length > 0);
701
- const buttons = [];
702
- const quickReplyItems = [];
703
- const carriedBlocks = [];
704
- const cardBody = [];
705
- const omittedControlLabels = [];
706
- const questionLabels = /* @__PURE__ */ new Set();
707
- const questionOptionIndices = resolveAskUserQuestionOptionIndices(payload);
708
- for (const block of presentation.blocks) if (block.type === "buttons") for (const button of block.buttons) {
709
- if (isLineTextFallbackButton(button)) {
710
- omittedControlLabels.push(button.label);
711
- continue;
712
- }
713
- const action = toLineAction(button, questionOptionIndices);
714
- if (!action) return null;
715
- if (isLineQuestionButton(button)) {
716
- if (questionLabels.has(button.label)) return null;
717
- questionLabels.add(button.label);
718
- }
719
- buttons.push({
720
- label: button.label,
721
- action
722
- });
723
- }
724
- else if (block.type === "select") {
725
- const overflow = [];
726
- for (const option of block.options) {
727
- const action = resolveMessagePresentationOptionAction(option);
728
- if (!action) return null;
729
- if (quickReplyItems.length < LINE_QUICK_REPLY_LIMIT) quickReplyItems.push({
730
- label: option.label,
731
- action
732
- });
733
- else overflow.push(option);
734
- }
735
- if (overflow.length > 0) carriedBlocks.push({
736
- ...block,
737
- options: overflow
738
- });
739
- else if (block.placeholder) carriedBlocks.push({
740
- type: "context",
741
- text: block.placeholder
742
- });
743
- } else if (!hasCard) carriedBlocks.push(block);
744
- else if (block.type === "text" || block.type === "context") cardBody.push(block.text);
745
- if (buttons.length === 0 && quickReplyItems.length === 0) return null;
746
- if (hasCard && omittedControlLabels.length > 0) cardBody.push(`Actions:\n${omittedControlLabels.map((label) => `- ${label}`).join("\n")}`);
747
- const lineData = isRecord(payload.channelData?.line) ? payload.channelData.line : {};
748
- const title = presentation.title || "Choose an option";
749
- const altText = presentation.title || cardBody[0] || title;
750
- const flexMessage = hasCard ? {
751
- altText,
752
- contents: createActionCard(title, cardBody.join("\n") || "Choose an option.", buttons)
753
- } : void 0;
754
- if (flexMessage && !fitsLineFlexBubble(flexMessage.contents)) return null;
755
- const text = renderMessagePresentationFallbackText({
756
- text: payload.text,
757
- presentation: {
758
- title: hasCard ? void 0 : presentation.title,
759
- blocks: carriedBlocks
760
- }
761
- });
762
- return {
763
- ...payload,
764
- ...text ? { text } : {},
765
- channelData: {
766
- ...payload.channelData,
767
- line: {
768
- ...lineData,
769
- ...flexMessage ? { flexMessage } : {},
770
- quickReplyItems
771
- }
772
- }
773
- };
774
- }
775
- /**
776
- * Resolve a reply's portable presentation into LINE-native controls.
777
- *
778
- * Core runs the presentation renderer inside the outbound send pipeline only, so
779
- * replies the plugin delivers itself reach delivery with the controls still
780
- * portable. Preparing them here keeps both LINE delivery paths on one rendering.
781
- */
782
- async function prepareLineReplyPayload(payload, to) {
783
- if (!normalizeMessagePresentation(payload.presentation)) return payload;
784
- const usesFallbackText = payload.presentationTextMode === "fallback" && Boolean(payload.text?.trim());
785
- return renderPresentationForDelivery({
786
- presentationCapabilities: LINE_PRESENTATION_CAPABILITIES,
787
- renderPresentation: (adapted, sourcePresentation) => {
788
- const rendered = renderLinePresentation(adapted, adapted.presentation, to, sourcePresentation);
789
- return rendered && usesFallbackText && rendered.channelData.line.flexMessage === void 0 ? {
790
- ...rendered,
791
- text: payload.text
792
- } : rendered;
793
- }
794
- }, {
795
- ...payload,
796
- presentationTextMode: usesFallbackText ? "fallback" : void 0
797
- });
798
- }
799
- const toSlug = (value) => normalizeLowercaseStringOrEmpty(value).replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "device";
800
- const lineActionData = (action, device) => `line.action=${encodeURIComponent(action)}&line.device=${encodeURIComponent(device)}`;
801
- function renderLineCard(card) {
802
- if (card.type === "media_player") {
803
- const device = toSlug(card.source || card.title);
804
- return {
805
- altText: `🎵 ${card.title}${card.artist ? ` - ${card.artist}` : ""}`,
806
- contents: createMediaPlayerCard({
807
- title: card.title,
808
- subtitle: card.artist,
809
- source: card.source,
810
- imageUrl: card.imageUrl,
811
- isPlaying: card.status ? card.status === "playing" : void 0,
812
- controls: Object.fromEntries([
813
- "previous",
814
- "play",
815
- "pause",
816
- "next"
817
- ].map((action) => [action, { data: lineActionData(action, device) }]))
818
- })
819
- };
820
- }
821
- if (card.type === "event") return {
822
- altText: `📅 ${card.title} - ${card.date}${card.time ? ` ${card.time}` : ""}`,
823
- contents: createEventCard(card)
824
- };
825
- if (card.type === "agenda") return {
826
- altText: `📋 ${card.title} (${card.events.length} events)`,
827
- contents: createAgendaCard(card)
828
- };
829
- const device = toSlug(card.type === "device" ? card.name : card.name || "apple_tv");
830
- if (card.type === "device") return {
831
- altText: `📱 ${card.name}${card.status ? `: ${card.status}` : ""}`,
832
- contents: createDeviceControlCard({
833
- deviceName: card.name,
834
- deviceType: card.deviceType,
835
- status: card.status,
836
- controls: (card.controls ?? []).map((control) => ({
837
- label: control.label,
838
- data: lineActionData(control.action, device)
839
- }))
840
- })
841
- };
842
- const actionData = {
843
- up: lineActionData("up", device),
844
- down: lineActionData("down", device),
845
- left: lineActionData("left", device),
846
- right: lineActionData("right", device),
847
- select: lineActionData("select", device),
848
- menu: lineActionData("menu", device),
849
- home: lineActionData("home", device),
850
- play: lineActionData("play", device),
851
- pause: lineActionData("pause", device),
852
- volumeUp: lineActionData("volume_up", device),
853
- volumeDown: lineActionData("volume_down", device),
854
- mute: lineActionData("mute", device)
855
- };
856
- return {
857
- altText: `📺 ${card.name || "Apple TV"} Remote`,
858
- contents: createAppleTvRemoteCard({
859
- deviceName: card.name || "Apple TV",
860
- status: card.status,
861
- actionData
862
- })
863
- };
864
- }
865
- function createLineQuickReply(items) {
866
- return { items: items.slice(0, LINE_QUICK_REPLY_LIMIT).map((item) => ({
867
- type: "action",
868
- action: item.action.type === "command" ? messageAction(item.label, item.action.command) : postbackAction(item.label, item.action.value, item.label)
869
- })) };
870
- }
871
- //#endregion
872
- export { LineConfigSchema as C, LineChannelConfigSchema as S, resolveLineGroupLookupIds as _, renderLineCard as a, normalizeAllowFrom as b, resolveLineQuestionPostback as c, createMediaPlayerCard as d, buildLineQuickReplyFallbackText as f, resolveLineGroupConfigEntry as g, resolveExactLineGroupConfigKey as h, prepareLineReplyPayload as i, createAppleTvRemoteCard as l, setLineRuntime as m, createLineQuickReply as n, renderLinePresentation as o, getLineRuntime as p, lineMessageActions as r, parseLineQuestionPostbackData as s, LINE_PRESENTATION_CAPABILITIES as t, createDeviceControlCard as u, resolveLineGroupsConfig as v, normalizeLineAllowEntry as x, firstDefined$1 as y };