@openclaw/line 2026.9.1-beta.1 → 2026.9.2
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/dist/account-helpers-BSUyZwLm.js +12 -0
- package/dist/{accounts-BqEVsDoB.js → accounts-BBEFMYbY.js} +2 -1
- package/dist/api.js +2 -2
- package/dist/card-command-pwvE48Wb.js +252 -0
- package/dist/{channel-CsQLcrsA.js → channel-BJvELOIg.js} +111 -380
- package/dist/channel-plugin-api.js +1 -1
- package/dist/channel.runtime-CZOszXXj.js +4 -0
- package/dist/contract-api.js +1 -1
- package/dist/doctor-contract-api.js +141 -0
- package/dist/index.js +5 -17
- package/dist/{markdown-to-line-BmZ4hhfC.js → markdown-to-line-B-sj-7II.js} +22 -23
- package/dist/{monitor-Bd9XDV1i.js → monitor-m9h5osFz.js} +339 -237
- package/dist/monitor.runtime-C90LL-P3.js +2 -0
- package/dist/outbound.runtime-CGqyJ-Ug.js +3 -0
- package/dist/probe.runtime-CbtMgU4s.js +2 -0
- package/dist/rich-messages-DY1AGKAJ.js +742 -0
- package/dist/runtime-api.js +14 -15
- package/dist/{send-Dce0m539.js → send-CQQxpxPp.js} +142 -112
- package/dist/{send-receipt-CCPTalf9.js → send-retry-ChpfJ-LM.js} +326 -86
- package/dist/setup-api.js +1 -1
- package/dist/{setup-surface-W0Dbgu58.js → setup-surface-CDlgkTKi.js} +47 -39
- package/dist/webhook-spool-contract-1rY8OJw8.js +59 -0
- package/node_modules/typebox/build/format/email.mjs +1 -1
- package/node_modules/typebox/build/format/idna/idn-hostname.mjs +19 -1
- package/node_modules/typebox/build/format/json_pointer.d.mts +2 -2
- package/node_modules/typebox/build/format/json_pointer.mjs +2 -2
- package/node_modules/typebox/build/format/json_pointer_uri_fragment.d.mts +2 -2
- package/node_modules/typebox/build/format/json_pointer_uri_fragment.mjs +2 -2
- package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
- package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
- package/node_modules/typebox/build/system/memory/assign.mjs +2 -1
- package/node_modules/typebox/build/system/memory/create.mjs +3 -4
- package/node_modules/typebox/build/system/memory/discard.mjs +2 -2
- package/node_modules/typebox/build/system/memory/freeze.d.mts +4 -0
- package/node_modules/typebox/build/system/memory/freeze.mjs +6 -0
- package/node_modules/typebox/build/system/memory/update.mjs +3 -2
- package/node_modules/typebox/package.json +1 -1
- package/openclaw.plugin.json +16 -3
- package/package.json +5 -5
- package/dist/card-command-CmU47gD9.js +0 -230
- package/dist/channel.runtime-B83x7k_i.js +0 -4
- package/dist/media-control-cards-CksNB8Wk.js +0 -474
- package/dist/monitor.runtime-BCCa6qTX.js +0 -2
- package/dist/outbound.runtime-D4w0-Gkc.js +0 -3
- package/dist/probe-CM03wo6r.js +0 -28
- package/dist/probe.runtime-D13ZtXsd.js +0 -2
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isRecord, normalizeNullableString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
2
|
+
//#region extensions/line/src/webhook-spool-contract.ts
|
|
3
|
+
/** Message the canonical decoder attaches when it rejects a spool payload. The
|
|
4
|
+
* upgrade migration matches this signature to recover rows the pre-fix decoder
|
|
5
|
+
* dead-lettered; the identity fence writes a different message on purpose. */
|
|
6
|
+
const LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE = "LINE webhook spool payload is invalid.";
|
|
7
|
+
/** Dead-letter reason for undecodable events; shared so the migration's
|
|
8
|
+
* recovery signature can never drift from what the spool writes. */
|
|
9
|
+
const LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON = "invalid-event";
|
|
10
|
+
/** Defined locally (not via createChannelIngressError) because this module sits
|
|
11
|
+
* in the doctor contract closure, which must stay off the channel-outbound
|
|
12
|
+
* runtime barrel; the spool's permanent-failure classifier matches by class. */
|
|
13
|
+
var LineWebhookPayloadError = class extends Error {
|
|
14
|
+
constructor(message, options) {
|
|
15
|
+
super(message, options);
|
|
16
|
+
this.name = "LineWebhookPayloadError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/** Message ids preserve the shipped replay-guard keyspace; other events use LINE's delivery id. */
|
|
20
|
+
function eventIdFor(event) {
|
|
21
|
+
if (!isRecord(event)) throw new LineWebhookPayloadError("LINE webhook event must be an object.");
|
|
22
|
+
if (event.type === "message") {
|
|
23
|
+
const message = isRecord(event.message) ? event.message : void 0;
|
|
24
|
+
const messageId = normalizeNullableString(message?.id);
|
|
25
|
+
if (messageId) return `message:${messageId}`;
|
|
26
|
+
}
|
|
27
|
+
const webhookEventId = normalizeNullableString(event.webhookEventId);
|
|
28
|
+
if (webhookEventId) return `event:${webhookEventId}`;
|
|
29
|
+
throw new LineWebhookPayloadError("LINE webhook event is missing a stable delivery id.");
|
|
30
|
+
}
|
|
31
|
+
/** Pre-drain (#109655) rows were keyed by the raw webhookEventId, before the
|
|
32
|
+
* message:/event: keyspace. The upgrade migration uses this prior derivation as its
|
|
33
|
+
* identity fence so a genuinely changed event still dead-letters. */
|
|
34
|
+
function legacyEventIdFor(event) {
|
|
35
|
+
if (!isRecord(event)) return null;
|
|
36
|
+
return normalizeNullableString(event.webhookEventId);
|
|
37
|
+
}
|
|
38
|
+
function laneKeyFor(event, eventId) {
|
|
39
|
+
if (!isRecord(event)) return eventId;
|
|
40
|
+
const source = isRecord(event.source) ? event.source : void 0;
|
|
41
|
+
if (source?.type === "group") {
|
|
42
|
+
const groupId = normalizeNullableString(source.groupId);
|
|
43
|
+
if (groupId) return `group:${groupId}`;
|
|
44
|
+
}
|
|
45
|
+
if (source?.type === "room") {
|
|
46
|
+
const roomId = normalizeNullableString(source.roomId);
|
|
47
|
+
if (roomId) return `room:${roomId}`;
|
|
48
|
+
}
|
|
49
|
+
if (source?.type === "user") {
|
|
50
|
+
const userId = normalizeNullableString(source.userId);
|
|
51
|
+
if (userId) return `user:${userId}`;
|
|
52
|
+
}
|
|
53
|
+
return eventId;
|
|
54
|
+
}
|
|
55
|
+
function errorText(error) {
|
|
56
|
+
return error instanceof Error ? error.message : String(error);
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { eventIdFor as a, errorText as i, LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE as n, laneKeyFor as o, LineWebhookPayloadError as r, legacyEventIdFor as s, LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const Email = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[^"\\]
|
|
1
|
+
const Email = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[^"\\]|\\[\x20-\x7e])*")@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*|\[(?:IPv6:[a-f0-9:]+|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})\])$/i;
|
|
2
2
|
/**
|
|
3
3
|
* Returns true if the value is an Email
|
|
4
4
|
* @specification https://datatracker.ietf.org/doc/html/rfc5322
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as FormatBidi from './format/bidi.mjs';
|
|
2
2
|
import * as LabelUnicode from './label/unicode.mjs';
|
|
3
3
|
import * as LabelPuny from './label/puny.mjs';
|
|
4
|
+
// ------------------------------------------------------------------
|
|
5
|
+
// IsLabel
|
|
6
|
+
// ------------------------------------------------------------------
|
|
4
7
|
function IsValidLabelLength(value) {
|
|
5
8
|
return value.length > 0 && value.length <= 63;
|
|
6
9
|
}
|
|
@@ -8,9 +11,24 @@ function IsLabel(value) {
|
|
|
8
11
|
return IsValidLabelLength(value) && (LabelPuny.IsPunyLabel(value) ||
|
|
9
12
|
LabelUnicode.IsUnicodeLabel(value));
|
|
10
13
|
}
|
|
14
|
+
// ------------------------------------------------------------------
|
|
15
|
+
// NormalizeHostname
|
|
16
|
+
//
|
|
17
|
+
// Normalizes a hostname for label validation. Normalizes for NFC
|
|
18
|
+
// such that equivalent codepoint sequences can be uniformly
|
|
19
|
+
// compared. It also removes codepoints that IDNA mapping ignores,
|
|
20
|
+
// and converts full stop variants to ASCII '.' so the hostname can
|
|
21
|
+
// be split into labels.
|
|
22
|
+
// ------------------------------------------------------------------
|
|
11
23
|
function NormalizeHostname(value) {
|
|
12
|
-
return value
|
|
24
|
+
return value
|
|
25
|
+
.normalize('NFC')
|
|
26
|
+
.replace(/[\u00ad\u034f\u180b-\u180d\u200b\ufe00-\ufe0f\u{e0100}-\u{e01ef}]/gu, '') // RE_IGNORED_MAPPING
|
|
27
|
+
.replace(/[\u002E\u3002\uFF0E\uFF61]/g, '.'); // RE_FULL_STOP_MAPPING
|
|
13
28
|
}
|
|
29
|
+
// ------------------------------------------------------------------
|
|
30
|
+
// IsIdnHostname
|
|
31
|
+
// ------------------------------------------------------------------
|
|
14
32
|
export function IsIdnHostname(value) {
|
|
15
33
|
if (value.length === 0 || value.includes(' '))
|
|
16
34
|
return false;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Returns true if the value is a json pointer
|
|
3
|
-
* @specification
|
|
4
|
-
* @source ajv-formats
|
|
3
|
+
* @specification https://datatracker.ietf.org/doc/html/rfc6901
|
|
4
|
+
* @source https://github.com/ajv-validator/ajv-formats
|
|
5
5
|
*/
|
|
6
6
|
export declare function IsJsonPointer(value: string): boolean;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const JsonPointer = /^(?:\/(?:[^~/]|~0|~1)*)*$/;
|
|
2
2
|
/**
|
|
3
3
|
* Returns true if the value is a json pointer
|
|
4
|
-
* @specification
|
|
5
|
-
* @source ajv-formats
|
|
4
|
+
* @specification https://datatracker.ietf.org/doc/html/rfc6901
|
|
5
|
+
* @source https://github.com/ajv-validator/ajv-formats
|
|
6
6
|
*/
|
|
7
7
|
export function IsJsonPointer(value) {
|
|
8
8
|
return JsonPointer.test(value);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Returns true if the value is a json pointer uri fragment
|
|
3
|
-
* @specification
|
|
4
|
-
* @source ajv-formats
|
|
3
|
+
* @specification https://datatracker.ietf.org/doc/html/rfc6901
|
|
4
|
+
* @source https://github.com/ajv-validator/ajv-formats
|
|
5
5
|
*/
|
|
6
6
|
export declare function IsJsonPointerUriFragment(value: string): boolean;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const JsonPointerUriFragment = /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i;
|
|
2
2
|
/**
|
|
3
3
|
* Returns true if the value is a json pointer uri fragment
|
|
4
|
-
* @specification
|
|
5
|
-
* @source ajv-formats
|
|
4
|
+
* @specification https://datatracker.ietf.org/doc/html/rfc6901
|
|
5
|
+
* @source https://github.com/ajv-validator/ajv-formats
|
|
6
6
|
*/
|
|
7
7
|
export function IsJsonPointerUriFragment(value) {
|
|
8
8
|
return JsonPointerUriFragment.test(value);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Returns true if the value is a relative json pointer
|
|
3
|
-
* @specification
|
|
4
|
-
* @source ajv-formats
|
|
3
|
+
* @specification https://datatracker.ietf.org/doc/html/rfc6901
|
|
4
|
+
* @source https://github.com/ajv-validator/ajv-formats
|
|
5
5
|
*/
|
|
6
6
|
export declare function IsRelativeJsonPointer(value: string): boolean;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const RelativeJsonPointer = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/;
|
|
2
2
|
/**
|
|
3
3
|
* Returns true if the value is a relative json pointer
|
|
4
|
-
* @specification
|
|
5
|
-
* @source ajv-formats
|
|
4
|
+
* @specification https://datatracker.ietf.org/doc/html/rfc6901
|
|
5
|
+
* @source https://github.com/ajv-validator/ajv-formats
|
|
6
6
|
*/
|
|
7
7
|
export function IsRelativeJsonPointer(value) {
|
|
8
8
|
return RelativeJsonPointer.test(value);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// deno-lint-ignore-file ban-types no-explicit-any
|
|
2
2
|
// deno-fmt-ignore-file
|
|
3
3
|
import { Metrics } from './metrics.mjs';
|
|
4
|
+
import { Freeze } from './freeze.mjs';
|
|
4
5
|
/**
|
|
5
6
|
* Performs an Object assign using the Left and Right object types. We track this operation as it
|
|
6
7
|
* creates a new GC handle per assignment.
|
|
7
8
|
*/
|
|
8
9
|
export function Assign(left, right) {
|
|
9
10
|
Metrics.assign += 1;
|
|
10
|
-
return { ...left, ...right };
|
|
11
|
+
return Freeze({ ...left, ...right });
|
|
11
12
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// deno-lint-ignore-file no-explicit-any
|
|
2
|
-
// deno-fmt-ignore-file
|
|
3
2
|
import { Settings } from '../settings/index.mjs';
|
|
4
3
|
import { Metrics } from './metrics.mjs';
|
|
4
|
+
import { Freeze } from './freeze.mjs';
|
|
5
5
|
function MergeHidden(left, right) {
|
|
6
6
|
for (const key of Object.keys(right)) {
|
|
7
7
|
Object.defineProperty(left, key, {
|
|
@@ -23,8 +23,7 @@ function Merge(left, right) {
|
|
|
23
23
|
*/
|
|
24
24
|
export function Create(hidden, enumerable, options = {}) {
|
|
25
25
|
Metrics.create += 1;
|
|
26
|
-
const settings = Settings.Get();
|
|
27
26
|
const withOptions = Merge(enumerable, options);
|
|
28
|
-
const withHidden =
|
|
29
|
-
return
|
|
27
|
+
const withHidden = Settings.Get().enumerableKind ? Merge(withOptions, hidden) : MergeHidden(withOptions, hidden);
|
|
28
|
+
return Freeze(withHidden);
|
|
30
29
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// deno-lint-ignore-file no-explicit-any
|
|
2
|
-
// deno-fmt-ignore-file
|
|
3
2
|
import { Guard } from '../../guard/index.mjs';
|
|
4
3
|
import { Metrics } from './metrics.mjs';
|
|
4
|
+
import { Freeze } from './freeze.mjs';
|
|
5
5
|
import { Clone } from './clone.mjs';
|
|
6
6
|
/** Discards multiple property keys from the given object value */
|
|
7
7
|
export function Discard(value, propertyKeys) {
|
|
@@ -14,5 +14,5 @@ export function Discard(value, propertyKeys) {
|
|
|
14
14
|
descriptor.value = Clone(descriptor.value);
|
|
15
15
|
Object.defineProperty(result, key, descriptor);
|
|
16
16
|
}
|
|
17
|
-
return result;
|
|
17
|
+
return Freeze(result);
|
|
18
18
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// deno-lint-ignore-file no-explicit-any
|
|
2
|
+
import { Settings } from '../settings/index.mjs';
|
|
3
|
+
/** Conditionally freezes the value if `immutableTypes` is true, otherwise no action. */
|
|
4
|
+
export function Freeze(value) {
|
|
5
|
+
return Settings.Get().immutableTypes ? Object.freeze(value) : value;
|
|
6
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// deno-
|
|
1
|
+
// deno-lint-ignore-file no-explicit-any
|
|
2
2
|
import { Settings } from '../settings/index.mjs';
|
|
3
3
|
import { Metrics } from './metrics.mjs';
|
|
4
|
+
import { Freeze } from './freeze.mjs';
|
|
4
5
|
import { Clone } from './clone.mjs';
|
|
5
6
|
/**
|
|
6
7
|
* Updates a value with new properties while preserving property enumerability. Use this function to modify
|
|
@@ -28,5 +29,5 @@ export function Update(current, hidden, enumerable) {
|
|
|
28
29
|
value: enumerable[key]
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
|
-
return result;
|
|
32
|
+
return Freeze(result);
|
|
32
33
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "line",
|
|
3
|
-
"doctorContract": {
|
|
3
|
+
"doctorContract": {
|
|
4
|
+
"stateMigrations": true
|
|
5
|
+
},
|
|
4
6
|
"name": "LINE",
|
|
5
7
|
"description": "OpenClaw LINE channel plugin for LINE Bot API chats.",
|
|
6
|
-
"icon": "https://cdn.simpleicons.org/line",
|
|
7
8
|
"activation": {
|
|
8
9
|
"onStartup": false
|
|
9
10
|
},
|
|
@@ -27,6 +28,9 @@
|
|
|
27
28
|
"configWrites": {
|
|
28
29
|
"type": "boolean"
|
|
29
30
|
},
|
|
31
|
+
"joinIntro": {
|
|
32
|
+
"type": "boolean"
|
|
33
|
+
},
|
|
30
34
|
"historyLimit": {
|
|
31
35
|
"type": "integer",
|
|
32
36
|
"minimum": 0,
|
|
@@ -180,6 +184,9 @@
|
|
|
180
184
|
"configWrites": {
|
|
181
185
|
"type": "boolean"
|
|
182
186
|
},
|
|
187
|
+
"joinIntro": {
|
|
188
|
+
"type": "boolean"
|
|
189
|
+
},
|
|
183
190
|
"historyLimit": {
|
|
184
191
|
"type": "integer",
|
|
185
192
|
"minimum": 0,
|
|
@@ -338,7 +345,13 @@
|
|
|
338
345
|
"additionalProperties": false
|
|
339
346
|
},
|
|
340
347
|
"label": "LINE",
|
|
341
|
-
"description": "LINE Messaging API webhook bot."
|
|
348
|
+
"description": "LINE Messaging API webhook bot.",
|
|
349
|
+
"uiHints": {
|
|
350
|
+
"joinIntro": {
|
|
351
|
+
"label": "LINE Group Join Introduction",
|
|
352
|
+
"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."
|
|
353
|
+
}
|
|
354
|
+
}
|
|
342
355
|
}
|
|
343
356
|
}
|
|
344
357
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/line",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.2",
|
|
4
4
|
"description": "OpenClaw LINE channel plugin for LINE Bot API chats.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"type": "module",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@line/bot-sdk": "11.2.0",
|
|
12
|
-
"typebox": "1.3.
|
|
12
|
+
"typebox": "1.3.18",
|
|
13
13
|
"zod": "4.4.3"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"openclaw": ">=2026.9.
|
|
16
|
+
"openclaw": ">=2026.9.2"
|
|
17
17
|
},
|
|
18
18
|
"peerDependenciesMeta": {
|
|
19
19
|
"openclaw": {
|
|
@@ -112,10 +112,10 @@
|
|
|
112
112
|
"minHostVersion": ">=2026.4.10"
|
|
113
113
|
},
|
|
114
114
|
"compat": {
|
|
115
|
-
"pluginApi": ">=2026.9.
|
|
115
|
+
"pluginApi": ">=2026.9.2"
|
|
116
116
|
},
|
|
117
117
|
"build": {
|
|
118
|
-
"openclawVersion": "2026.9.
|
|
118
|
+
"openclawVersion": "2026.9.2"
|
|
119
119
|
},
|
|
120
120
|
"release": {
|
|
121
121
|
"publishToClawHub": true,
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { a as createActionCard, c as createListCard, i as createReceiptCard, o as createImageCard, s as createInfoCard } from "./send-receipt-CCPTalf9.js";
|
|
2
|
-
import { n as createFlexMessage } from "./send-Dce0m539.js";
|
|
3
|
-
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
|
-
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
5
|
-
//#region extensions/line/src/card-command.ts
|
|
6
|
-
const CARD_USAGE = `Usage: /card <type> "title" "body" [options]
|
|
7
|
-
|
|
8
|
-
Types:
|
|
9
|
-
info "Title" "Body" ["Footer"]
|
|
10
|
-
image "Title" "Caption" --url <image-url>
|
|
11
|
-
action "Title" "Body" --actions "Btn1|url1,Btn2|text2"
|
|
12
|
-
list "Title" "Item1|Desc1,Item2|Desc2"
|
|
13
|
-
receipt "Title" "Item1:$10,Item2:$20" --total "$30"
|
|
14
|
-
confirm "Question?" --yes "Yes|data" --no "No|data"
|
|
15
|
-
buttons "Title" "Text" --actions "Btn1|url1,Btn2|data2"
|
|
16
|
-
|
|
17
|
-
Examples:
|
|
18
|
-
/card info "Welcome" "Thanks for joining!"
|
|
19
|
-
/card image "Product" "Check it out" --url https://example.com/img.jpg
|
|
20
|
-
/card action "Menu" "Choose an option" --actions "Order|/order,Help|/help"`;
|
|
21
|
-
function buildLineReply(lineData) {
|
|
22
|
-
return { channelData: { line: lineData } };
|
|
23
|
-
}
|
|
24
|
-
function buildLineFlexReply(altText, contents) {
|
|
25
|
-
const message = createFlexMessage(altText, contents);
|
|
26
|
-
return buildLineReply({ flexMessage: {
|
|
27
|
-
altText: message.altText,
|
|
28
|
-
contents: message.contents
|
|
29
|
-
} });
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Parse action string format: "Label|data,Label2|data2"
|
|
33
|
-
* Data can be a URL (uri action) or plain text (message action) or key=value (postback)
|
|
34
|
-
*/
|
|
35
|
-
function parseActions(actionsStr) {
|
|
36
|
-
if (!actionsStr) return [];
|
|
37
|
-
const results = [];
|
|
38
|
-
for (const part of actionsStr.split(",")) {
|
|
39
|
-
const [label, data] = part.trim().split("|").map((s) => s.trim());
|
|
40
|
-
if (!label) continue;
|
|
41
|
-
const actionData = data || label;
|
|
42
|
-
const action = actionData.startsWith("http://") || actionData.startsWith("https://") ? {
|
|
43
|
-
type: "uri",
|
|
44
|
-
label,
|
|
45
|
-
uri: actionData
|
|
46
|
-
} : actionData.includes("=") ? {
|
|
47
|
-
type: "postback",
|
|
48
|
-
label,
|
|
49
|
-
data: actionData,
|
|
50
|
-
displayText: label
|
|
51
|
-
} : {
|
|
52
|
-
type: "message",
|
|
53
|
-
label,
|
|
54
|
-
text: actionData
|
|
55
|
-
};
|
|
56
|
-
results.push({
|
|
57
|
-
label,
|
|
58
|
-
action
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
return results;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Parse list items format: "Item1|Subtitle1,Item2|Subtitle2"
|
|
65
|
-
*/
|
|
66
|
-
function parseListItems(itemsStr) {
|
|
67
|
-
return itemsStr.split(",").map((part) => {
|
|
68
|
-
const [title, subtitle] = part.trim().split("|").map((s) => s.trim());
|
|
69
|
-
return {
|
|
70
|
-
title: title || "",
|
|
71
|
-
subtitle
|
|
72
|
-
};
|
|
73
|
-
}).filter((item) => item.title);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Parse receipt items format: "Item1:$10,Item2:$20"
|
|
77
|
-
*/
|
|
78
|
-
function parseReceiptItems(itemsStr) {
|
|
79
|
-
return itemsStr.split(",").map((part) => {
|
|
80
|
-
const colonIndex = part.lastIndexOf(":");
|
|
81
|
-
if (colonIndex === -1) return {
|
|
82
|
-
name: part.trim(),
|
|
83
|
-
value: ""
|
|
84
|
-
};
|
|
85
|
-
return {
|
|
86
|
-
name: part.slice(0, colonIndex).trim(),
|
|
87
|
-
value: part.slice(colonIndex + 1).trim()
|
|
88
|
-
};
|
|
89
|
-
}).filter((item) => item.name);
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Parse quoted arguments from command string
|
|
93
|
-
* Supports: /card type "arg1" "arg2" "arg3" --flag value
|
|
94
|
-
*/
|
|
95
|
-
function parseCardArgs(argsStrInput) {
|
|
96
|
-
let argsStr = argsStrInput;
|
|
97
|
-
const result = {
|
|
98
|
-
type: "",
|
|
99
|
-
args: [],
|
|
100
|
-
flags: {}
|
|
101
|
-
};
|
|
102
|
-
const typeMatch = argsStr.match(/^(\w+)/);
|
|
103
|
-
if (typeMatch) {
|
|
104
|
-
result.type = normalizeLowercaseStringOrEmpty(typeMatch[1]);
|
|
105
|
-
argsStr = argsStr.slice(typeMatch[0].length).trim();
|
|
106
|
-
}
|
|
107
|
-
const quotedRegex = /"([^"]*?)"/g;
|
|
108
|
-
let match;
|
|
109
|
-
while ((match = quotedRegex.exec(argsStr)) !== null) result.args.push(expectDefined(match[1], "quoted card argument capture"));
|
|
110
|
-
const flagRegex = /--(\w+)\s+(?:"([^"]*?)"|(\S+))/g;
|
|
111
|
-
while ((match = flagRegex.exec(argsStr)) !== null) {
|
|
112
|
-
const key = expectDefined(match[1], "card flag name capture");
|
|
113
|
-
result.flags[key] = expectDefined(match[2] ?? match[3], "card flag value capture");
|
|
114
|
-
}
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
function registerLineCardCommand(api) {
|
|
118
|
-
api.registerCommand({
|
|
119
|
-
name: "card",
|
|
120
|
-
description: "Send a rich card message (LINE).",
|
|
121
|
-
acceptsArgs: true,
|
|
122
|
-
requireAuth: false,
|
|
123
|
-
handler: async (ctx) => {
|
|
124
|
-
const argsStr = ctx.args?.trim() ?? "";
|
|
125
|
-
if (!argsStr) return { text: CARD_USAGE };
|
|
126
|
-
const { type, args, flags } = parseCardArgs(argsStr);
|
|
127
|
-
if (!type) return { text: CARD_USAGE };
|
|
128
|
-
if (ctx.channel !== "line") return { text: `[${type} card] ${args.join(" - ")}`.trim() };
|
|
129
|
-
try {
|
|
130
|
-
switch (type) {
|
|
131
|
-
case "info": {
|
|
132
|
-
const [title = "Info", body = "", footer] = args;
|
|
133
|
-
const bubble = createInfoCard(title, body, footer);
|
|
134
|
-
return buildLineFlexReply(`${title}: ${body}`, bubble);
|
|
135
|
-
}
|
|
136
|
-
case "image": {
|
|
137
|
-
const [title = "Image", caption = ""] = args;
|
|
138
|
-
const imageUrl = flags.url || flags.image;
|
|
139
|
-
if (!imageUrl) return { text: "Error: Image card requires --url <image-url>" };
|
|
140
|
-
const bubble = createImageCard(imageUrl, title, caption);
|
|
141
|
-
return buildLineFlexReply(`${title}: ${caption}`, bubble);
|
|
142
|
-
}
|
|
143
|
-
case "action": {
|
|
144
|
-
const [title = "Actions", body = ""] = args;
|
|
145
|
-
const actions = parseActions(flags.actions);
|
|
146
|
-
if (actions.length === 0) return { text: "Error: Action card requires --actions \"Label1|data1,Label2|data2\"" };
|
|
147
|
-
const bubble = createActionCard(title, body, actions, { imageUrl: flags.url || flags.image });
|
|
148
|
-
return buildLineFlexReply(`${title}: ${body}`, bubble);
|
|
149
|
-
}
|
|
150
|
-
case "list": {
|
|
151
|
-
const [title = "List", itemsStr = ""] = args;
|
|
152
|
-
const items = parseListItems(itemsStr || flags.items || "");
|
|
153
|
-
if (items.length === 0) return { text: "Error: List card requires items. Usage: /card list \"Title\" \"Item1|Desc1,Item2|Desc2\"" };
|
|
154
|
-
const bubble = createListCard(title, items);
|
|
155
|
-
return buildLineFlexReply(`${title}: ${items.map((item) => item.title).join(", ")}`, bubble);
|
|
156
|
-
}
|
|
157
|
-
case "receipt": {
|
|
158
|
-
const [title = "Receipt", itemsStr = ""] = args;
|
|
159
|
-
const items = parseReceiptItems(itemsStr || flags.items || "");
|
|
160
|
-
const total = flags.total ? {
|
|
161
|
-
label: "Total",
|
|
162
|
-
value: flags.total
|
|
163
|
-
} : void 0;
|
|
164
|
-
const footer = flags.footer;
|
|
165
|
-
if (items.length === 0) return { text: "Error: Receipt card requires items. Usage: /card receipt \"Title\" \"Item1:$10,Item2:$20\" --total \"$30\"" };
|
|
166
|
-
const bubble = createReceiptCard({
|
|
167
|
-
title,
|
|
168
|
-
items,
|
|
169
|
-
total,
|
|
170
|
-
footer
|
|
171
|
-
});
|
|
172
|
-
return buildLineFlexReply(`${title}: ${items.map((item) => `${item.name} ${item.value}`).join(", ")}`, bubble);
|
|
173
|
-
}
|
|
174
|
-
case "confirm": {
|
|
175
|
-
const [question = "Confirm?"] = args;
|
|
176
|
-
const yesStr = flags.yes || "Yes|yes";
|
|
177
|
-
const noStr = flags.no || "No|no";
|
|
178
|
-
const [yesLabel, yesData] = yesStr.split("|").map((s) => s.trim());
|
|
179
|
-
const [noLabel, noData] = noStr.split("|").map((s) => s.trim());
|
|
180
|
-
return buildLineReply({ templateMessage: {
|
|
181
|
-
type: "confirm",
|
|
182
|
-
text: question,
|
|
183
|
-
confirmLabel: yesLabel || "Yes",
|
|
184
|
-
confirmData: yesData || "yes",
|
|
185
|
-
cancelLabel: noLabel || "No",
|
|
186
|
-
cancelData: noData || "no",
|
|
187
|
-
altText: question
|
|
188
|
-
} });
|
|
189
|
-
}
|
|
190
|
-
case "buttons": {
|
|
191
|
-
const [title = "Menu", text = "Choose an option"] = args;
|
|
192
|
-
const actionParts = parseActions(flags.actions || "");
|
|
193
|
-
if (actionParts.length === 0) return { text: "Error: Buttons card requires --actions \"Label1|data1,Label2|data2\"" };
|
|
194
|
-
const templateActions = actionParts.map((a) => {
|
|
195
|
-
const action = a.action;
|
|
196
|
-
const label = action.label ?? a.label;
|
|
197
|
-
if (action.type === "uri") return {
|
|
198
|
-
type: "uri",
|
|
199
|
-
label,
|
|
200
|
-
uri: action.uri
|
|
201
|
-
};
|
|
202
|
-
if (action.type === "postback") return {
|
|
203
|
-
type: "postback",
|
|
204
|
-
label,
|
|
205
|
-
data: action.data
|
|
206
|
-
};
|
|
207
|
-
return {
|
|
208
|
-
type: "message",
|
|
209
|
-
label,
|
|
210
|
-
data: action.text
|
|
211
|
-
};
|
|
212
|
-
});
|
|
213
|
-
return buildLineReply({ templateMessage: {
|
|
214
|
-
type: "buttons",
|
|
215
|
-
title,
|
|
216
|
-
text,
|
|
217
|
-
thumbnailImageUrl: flags.url || flags.image,
|
|
218
|
-
actions: templateActions
|
|
219
|
-
} });
|
|
220
|
-
}
|
|
221
|
-
default: return { text: `Unknown card type: "${type}". Available types: info, image, action, list, receipt, confirm, buttons` };
|
|
222
|
-
}
|
|
223
|
-
} catch (err) {
|
|
224
|
-
return { text: `Error creating card: ${String(err)}` };
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
//#endregion
|
|
230
|
-
export { registerLineCardCommand };
|