@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.
- package/assets/icon.png +0 -0
- package/dist/.setup/{accounts-BBEFMYbY.mjs → accounts-DOiM8-y3.mjs} +1 -4
- package/dist/.setup/api-4RgpQmHW.mjs +3 -0
- package/dist/.setup/{api-D_06cuHl.mjs → api-w4QdlCCB.mjs} +2 -2
- package/dist/.setup/{card-command-710u8FBH.mjs → card-command-DYf4gnmI.mjs} +25 -33
- package/dist/.setup/{channel-kc3VRglA.mjs → channel-DN6Za8Vm.mjs} +789 -26
- package/dist/.setup/channel.runtime-bd5vGeSo.mjs +4 -0
- package/dist/.setup/{monitor-morJPcPd.mjs → monitor-C01hAm3f.mjs} +262 -333
- package/dist/.setup/monitor.runtime-DwH2zK48.mjs +2 -0
- package/dist/.setup/outbound.runtime-D_Gs99Dm.mjs +3 -0
- package/dist/.setup/probe.runtime-Cj08-BZ1.mjs +2 -0
- package/dist/.setup/runtime-V7yh9OeC.mjs +8 -0
- package/dist/.setup/{send-BmsHY4WC.mjs → send-DOY1WTxv.mjs} +7 -26
- package/dist/.setup/{send-retry-DbfiHBPd.mjs → send-retry-DV6I0muq.mjs} +104 -208
- package/dist/.setup/{setup-surface-CcfKJKtr.mjs → setup-surface-CbfkmkIS.mjs} +15 -4
- package/dist/.setup/{markdown-to-line-CRfEHFs0.mjs → template-messages-CxCnYsWN.mjs} +168 -189
- package/dist/api.js +2 -2
- package/dist/channel-plugin-api.js +1 -1
- package/dist/contract-api.js +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime-api.js +2 -260
- package/dist/setup-api.js +1 -1
- package/dist/setup-entry.js +1 -1
- package/node_modules/@line/bot-sdk/node_modules/@types/node/README.md +1 -1
- package/node_modules/@line/bot-sdk/node_modules/@types/node/child_process.d.ts +1 -1
- package/node_modules/@line/bot-sdk/node_modules/@types/node/package.json +2 -2
- package/node_modules/typebox/build/compile/code.mjs +1 -2
- package/node_modules/typebox/build/compile/validator.mjs +5 -1
- package/node_modules/typebox/build/format/iri.mjs +16 -9
- package/node_modules/typebox/build/guard/emit.mjs +2 -2
- package/node_modules/typebox/build/guard/guard.d.mts +7 -5
- package/node_modules/typebox/build/guard/guard.mjs +14 -9
- package/node_modules/typebox/build/guard/index.mjs +6 -0
- package/node_modules/typebox/build/guard/unicode/unicode.d.mts +6 -0
- package/node_modules/typebox/build/guard/unicode/unicode.mjs +35 -0
- package/node_modules/typebox/build/guard/{string.mjs → unicode/unicode_segment.mjs} +16 -19
- package/node_modules/typebox/build/schema/build.mjs +2 -3
- package/node_modules/typebox/build/schema/engine/_pathing.d.mts +1 -0
- package/node_modules/typebox/build/schema/engine/_pathing.mjs +11 -0
- package/node_modules/typebox/build/schema/engine/_refine.mjs +13 -5
- package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/dependencies.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/dependentSchemas.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +1 -1
- package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/format.mjs +9 -2
- package/node_modules/typebox/build/schema/engine/patternProperties.mjs +3 -2
- package/node_modules/typebox/build/schema/engine/properties.mjs +3 -2
- package/node_modules/typebox/build/schema/engine/propertyNames.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +1 -1
- package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/ref.d.mts +1 -1
- package/node_modules/typebox/build/schema/engine/ref.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/schema.mjs +24 -24
- package/node_modules/typebox/build/schema/parse.mjs +3 -0
- package/node_modules/typebox/build/schema/pointer/pointer.d.mts +1 -6
- package/node_modules/typebox/build/schema/pointer/pointer.mjs +47 -64
- package/node_modules/typebox/build/system/settings/internal.d.mts +2 -0
- package/node_modules/typebox/build/system/settings/internal.mjs +20 -0
- package/node_modules/typebox/build/system/settings/settings.d.mts +11 -0
- package/node_modules/typebox/build/system/settings/settings.mjs +14 -0
- package/node_modules/typebox/build/type/extends/inference.d.mts +1 -1
- package/node_modules/typebox/build/type/extends/inference.mjs +15 -2
- package/node_modules/typebox/build/type/extends/tuple.d.mts +2 -1
- package/node_modules/typebox/build/type/extends/tuple.mjs +21 -1
- package/node_modules/typebox/build/value/parse/parse.mjs +10 -4
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/typebox/readme.md +3 -1
- package/package.json +5 -5
- package/dist/.setup/account-helpers-BSUyZwLm.mjs +0 -12
- package/dist/.setup/api-RdEuQgW-.mjs +0 -3
- package/dist/.setup/channel.runtime-BtkeDELb.mjs +0 -4
- package/dist/.setup/monitor.runtime-BGhxYDKG.mjs +0 -2
- package/dist/.setup/outbound.runtime-QGvZy8Em.mjs +0 -3
- package/dist/.setup/probe.runtime-CMf8b-jB.mjs +0 -2
- package/dist/.setup/rich-messages-d8CmBVJj.mjs +0 -872
- /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 };
|