@rikaidev/yomi 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/ambient/ambient-event-graph.js +172 -0
- package/dist/ambient/ambient-observer.js +71 -0
- package/dist/ambient/event-graph-reducer.js +300 -0
- package/dist/ambient/ingestion-adapter.js +203 -0
- package/dist/ambient/journal/crypto.js +58 -0
- package/dist/ambient/journal/journal-store.js +284 -0
- package/dist/ambient/journal/types.js +7 -0
- package/dist/ambient/summaries/retriever.js +55 -0
- package/dist/ambient/summaries/summary-engine.js +128 -0
- package/dist/ambient/summaries/types.js +4 -0
- package/dist/attention/policy.js +22 -16
- package/dist/auth/credential-store.js +15 -6
- package/dist/cli/query.js +276 -69
- package/dist/cli/work-item.js +24 -0
- package/dist/line/client/auth-service/client.js +7 -8
- package/dist/line/client/buddy-service/client.js +73 -0
- package/dist/line/client/index.js +28 -1
- package/dist/line/client/obs-media-client.js +2 -1
- package/dist/line/client/square-service/client.js +298 -0
- package/dist/line/client/talk-service/command-client.js +9 -1
- package/dist/line/client/talk-service/message-id-query.js +27 -3
- package/dist/line/client/talk-service/requests.js +9 -0
- package/dist/line/core/auth-session-runtime.js +1 -1
- package/dist/line/core/chat-event.js +2 -0
- package/dist/line/core/chat-runtime-service.js +18 -3
- package/dist/line/core/config.js +1 -0
- package/dist/line/core/flex-message.js +323 -0
- package/dist/line/core/service.js +0 -1
- package/dist/mcp/handlers/contacts.js +12 -6
- package/dist/mcp/handlers/conversations.js +54 -0
- package/dist/mcp/handlers/send.js +8 -11
- package/dist/mcp/media.js +101 -2
- package/dist/mcp/names.js +78 -6
- package/dist/search/default-embedder.js +12 -3
- package/dist/version.js +1 -1
- package/dist/work-item/adapter.js +1 -0
- package/dist/work-item/destination-contracts.js +1 -0
- package/dist/work-item/event-context.js +73 -0
- package/dist/work-item/gold-contract.js +101 -0
- package/dist/work-item/gold-coverage.js +96 -0
- package/dist/work-item/gold-derivation.js +67 -0
- package/dist/work-item/gold-split.js +143 -0
- package/dist/work-item/index.js +23 -0
- package/dist/work-item/line-adapter.js +111 -0
- package/dist/work-item/llama-cpp-work-event-adapter.js +239 -0
- package/dist/work-item/local-provider.js +38 -0
- package/dist/work-item/local-work-event-provider.js +56 -0
- package/dist/work-item/onnx-work-event-adapter.js +223 -0
- package/dist/work-item/provider.js +21 -0
- package/dist/work-item/registry.js +19 -0
- package/dist/work-item/service.js +85 -0
- package/dist/work-item/store.js +97 -0
- package/dist/work-item/types.js +79 -0
- package/dist/work-item/work-event-graph-engine.js +63 -0
- package/dist/work-item/work-event-inference.js +267 -0
- package/dist/work-item/work-event-matcher.js +105 -0
- package/dist/work-item/work-event-model-manifest.js +39 -0
- package/dist/work-item/work-event-store.js +125 -0
- package/dist/work-item/work-event.js +123 -0
- package/package.json +24 -3
- package/run.mjs +162 -9
|
@@ -1,3 +1,132 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
function flexLayoutFingerprint(root) {
|
|
3
|
+
return createHash('sha256').update(JSON.stringify(root)).digest('hex');
|
|
4
|
+
}
|
|
5
|
+
const FLEX_METRIC = {
|
|
6
|
+
none: 0,
|
|
7
|
+
xxs: 2,
|
|
8
|
+
xs: 4,
|
|
9
|
+
sm: 6,
|
|
10
|
+
md: 8,
|
|
11
|
+
lg: 12,
|
|
12
|
+
xl: 16,
|
|
13
|
+
xxl: 20,
|
|
14
|
+
};
|
|
15
|
+
function resolveMetric(value, fallback = 0) {
|
|
16
|
+
if (typeof value !== 'string')
|
|
17
|
+
return fallback;
|
|
18
|
+
if (/^\d+(\.\d+)?px$/.test(value))
|
|
19
|
+
return Number(value.slice(0, -2));
|
|
20
|
+
return FLEX_METRIC[value] ?? fallback;
|
|
21
|
+
}
|
|
22
|
+
function resolveOptionalMetric(value) {
|
|
23
|
+
return typeof value === 'string' ? resolveMetric(value) : null;
|
|
24
|
+
}
|
|
25
|
+
function resolveDimension(value) {
|
|
26
|
+
if (typeof value !== 'string')
|
|
27
|
+
return { points: null, fraction: null };
|
|
28
|
+
if (/^\d+(\.\d+)?px$/.test(value))
|
|
29
|
+
return { points: Number(value.slice(0, -2)), fraction: null };
|
|
30
|
+
if (/^\d+(\.\d+)?%$/.test(value))
|
|
31
|
+
return { points: null, fraction: Number(value.slice(0, -1)) / 100 };
|
|
32
|
+
return { points: null, fraction: null };
|
|
33
|
+
}
|
|
34
|
+
function resolveFontSize(value) {
|
|
35
|
+
if (typeof value === 'string' && /^\d+(\.\d+)?px$/.test(value))
|
|
36
|
+
return Number(value.slice(0, -2));
|
|
37
|
+
const sizes = {
|
|
38
|
+
xxs: 10,
|
|
39
|
+
xs: 11,
|
|
40
|
+
sm: 12,
|
|
41
|
+
md: 13,
|
|
42
|
+
lg: 15,
|
|
43
|
+
xl: 18,
|
|
44
|
+
xxl: 22,
|
|
45
|
+
'3xl': 26,
|
|
46
|
+
'4xl': 30,
|
|
47
|
+
'5xl': 34,
|
|
48
|
+
};
|
|
49
|
+
return sizes[typeof value === 'string' ? value : 'md'] ?? 13;
|
|
50
|
+
}
|
|
51
|
+
function resolveAspectRatio(value) {
|
|
52
|
+
if (typeof value !== 'string')
|
|
53
|
+
return 1;
|
|
54
|
+
const [width, height] = value.split(':').map(Number);
|
|
55
|
+
return width > 0 && height > 0 ? width / height : 1;
|
|
56
|
+
}
|
|
57
|
+
function normalizeGradient(value) {
|
|
58
|
+
const raw = parseObject(value);
|
|
59
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
60
|
+
return null;
|
|
61
|
+
const gradient = raw;
|
|
62
|
+
if (gradient.type !== 'linearGradient' ||
|
|
63
|
+
typeof gradient.startColor !== 'string' ||
|
|
64
|
+
typeof gradient.endColor !== 'string')
|
|
65
|
+
return null;
|
|
66
|
+
const angle = typeof gradient.angle === 'string'
|
|
67
|
+
? Number(gradient.angle.replace(/deg$/, ''))
|
|
68
|
+
: 0;
|
|
69
|
+
const centerPosition = typeof gradient.centerPosition === 'string'
|
|
70
|
+
? Number(gradient.centerPosition.replace(/%$/, '')) / 100
|
|
71
|
+
: null;
|
|
72
|
+
return {
|
|
73
|
+
type: 'linearGradient',
|
|
74
|
+
angle: Number.isFinite(angle) ? angle : 0,
|
|
75
|
+
startColor: gradient.startColor,
|
|
76
|
+
centerColor: typeof gradient.centerColor === 'string' ? gradient.centerColor : null,
|
|
77
|
+
endColor: gradient.endColor,
|
|
78
|
+
centerPosition: centerPosition !== null && Number.isFinite(centerPosition)
|
|
79
|
+
? centerPosition
|
|
80
|
+
: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function resolveFlexStyle(node) {
|
|
84
|
+
const all = resolveMetric(node.paddingAll);
|
|
85
|
+
const width = resolveDimension(node.width);
|
|
86
|
+
const maxWidth = resolveDimension(node.maxWidth);
|
|
87
|
+
const height = resolveDimension(node.height);
|
|
88
|
+
const maxHeight = resolveDimension(node.maxHeight);
|
|
89
|
+
return {
|
|
90
|
+
axis: ['horizontal', 'vertical', 'baseline'].includes(node.layout)
|
|
91
|
+
? node.layout
|
|
92
|
+
: null,
|
|
93
|
+
spacing: resolveMetric(node.spacing, 8),
|
|
94
|
+
paddingTop: resolveMetric(node.paddingTop, all),
|
|
95
|
+
paddingEnd: resolveMetric(node.paddingEnd, all),
|
|
96
|
+
paddingBottom: resolveMetric(node.paddingBottom, all),
|
|
97
|
+
paddingStart: resolveMetric(node.paddingStart, all),
|
|
98
|
+
marginBefore: resolveMetric(node.margin),
|
|
99
|
+
fontSize: resolveFontSize(node.size),
|
|
100
|
+
fontWeight: node.weight === 'bold' ? 700 : 400,
|
|
101
|
+
lineLimit: Number.isFinite(node.maxLines)
|
|
102
|
+
? Number(node.maxLines)
|
|
103
|
+
: node.wrap === true
|
|
104
|
+
? null
|
|
105
|
+
: 1,
|
|
106
|
+
aspectRatio: resolveAspectRatio(node.aspectRatio),
|
|
107
|
+
contentMode: node.aspectMode === 'cover' ? 'cover' : 'fit',
|
|
108
|
+
buttonStyle: node.style === 'primary' || node.style === 'secondary'
|
|
109
|
+
? node.style
|
|
110
|
+
: 'link',
|
|
111
|
+
buttonHeight: node.height === 'sm' ? 34 : 40,
|
|
112
|
+
cornerRadius: resolveMetric(node.cornerRadius),
|
|
113
|
+
borderWidth: resolveMetric(node.borderWidth),
|
|
114
|
+
flexGrow: Number.isFinite(node.flex) ? Math.max(0, Number(node.flex)) : 0,
|
|
115
|
+
position: node.position === 'absolute' ? 'absolute' : 'relative',
|
|
116
|
+
offsetTop: resolveOptionalMetric(node.offsetTop),
|
|
117
|
+
offsetBottom: resolveOptionalMetric(node.offsetBottom),
|
|
118
|
+
offsetStart: resolveOptionalMetric(node.offsetStart),
|
|
119
|
+
offsetEnd: resolveOptionalMetric(node.offsetEnd),
|
|
120
|
+
widthPoints: width.points,
|
|
121
|
+
widthFraction: width.fraction,
|
|
122
|
+
maxWidthPoints: maxWidth.points,
|
|
123
|
+
maxWidthFraction: maxWidth.fraction,
|
|
124
|
+
heightPoints: height.points,
|
|
125
|
+
heightFraction: height.fraction,
|
|
126
|
+
maxHeightPoints: maxHeight.points,
|
|
127
|
+
maxHeightFraction: maxHeight.fraction,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
1
130
|
const FLEX_PAYLOAD_KEYS = [
|
|
2
131
|
'FLEX_JSON',
|
|
3
132
|
'FLEX_CONTENTS',
|
|
@@ -31,6 +160,200 @@ function webUrl(value) {
|
|
|
31
160
|
return null;
|
|
32
161
|
}
|
|
33
162
|
}
|
|
163
|
+
export function resolveImagemapImageUrl(rawUrl, _canvasWidth, targetPixelWidth = 640) {
|
|
164
|
+
const base = webUrl(rawUrl);
|
|
165
|
+
if (!base)
|
|
166
|
+
return null;
|
|
167
|
+
const supported = [240, 300, 460, 700, 1040];
|
|
168
|
+
const requested = Math.max(1, targetPixelWidth);
|
|
169
|
+
const width = supported.find((candidate) => candidate >= requested) ??
|
|
170
|
+
supported[supported.length - 1];
|
|
171
|
+
const url = new URL(base);
|
|
172
|
+
if (!url.pathname.endsWith(`/${width}`)) {
|
|
173
|
+
url.pathname = `${url.pathname.replace(/\/$/, '')}/${width}`;
|
|
174
|
+
}
|
|
175
|
+
return url.toString();
|
|
176
|
+
}
|
|
177
|
+
function flexAction(raw) {
|
|
178
|
+
const parsed = parseObject(raw);
|
|
179
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
180
|
+
return null;
|
|
181
|
+
const record = parsed;
|
|
182
|
+
const params = record.params && typeof record.params === 'object' ? record.params : record;
|
|
183
|
+
const rawType = String(record.type ?? '').toLowerCase();
|
|
184
|
+
const uri = webUrl(params.linkUri) || webUrl(record.uri);
|
|
185
|
+
const text = typeof params.text === 'string'
|
|
186
|
+
? params.text
|
|
187
|
+
: typeof record.text === 'string'
|
|
188
|
+
? record.text
|
|
189
|
+
: null;
|
|
190
|
+
const label = String(record.label ?? record.text ?? params.displayText ?? '').trim();
|
|
191
|
+
const data = typeof record.data === 'string'
|
|
192
|
+
? record.data
|
|
193
|
+
: typeof params.data === 'string'
|
|
194
|
+
? params.data
|
|
195
|
+
: null;
|
|
196
|
+
const type = rawType === 'web' || rawType === 'uri'
|
|
197
|
+
? 'uri'
|
|
198
|
+
: rawType === 'message' || rawType === 'sendmessage'
|
|
199
|
+
? 'message'
|
|
200
|
+
: rawType === 'postback'
|
|
201
|
+
? 'postback'
|
|
202
|
+
: 'unknown';
|
|
203
|
+
return { type, label, uri, text, data };
|
|
204
|
+
}
|
|
205
|
+
function normalizeFlexNode(raw, depth = 0, block = null, blockStyle = null) {
|
|
206
|
+
if (depth > 24)
|
|
207
|
+
return null;
|
|
208
|
+
const parsed = parseObject(raw);
|
|
209
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
210
|
+
return null;
|
|
211
|
+
const node = parsed;
|
|
212
|
+
const nodeType = String(node.type ?? 'box').toLowerCase();
|
|
213
|
+
const parsedBlockStyle = parseObject(blockStyle);
|
|
214
|
+
const inherited = parsedBlockStyle &&
|
|
215
|
+
typeof parsedBlockStyle === 'object' &&
|
|
216
|
+
!Array.isArray(parsedBlockStyle)
|
|
217
|
+
? parsedBlockStyle
|
|
218
|
+
: {};
|
|
219
|
+
const childValues = Array.isArray(node.contents)
|
|
220
|
+
? node.contents.map((child) => [child, null, null])
|
|
221
|
+
: ['header', 'hero', 'body', 'footer']
|
|
222
|
+
.filter((key) => Boolean(node[key]))
|
|
223
|
+
.map((key) => [node[key], key, node.styles?.[key]]);
|
|
224
|
+
if (node.altContent)
|
|
225
|
+
childValues.push([node.altContent, null, null]);
|
|
226
|
+
return {
|
|
227
|
+
type: nodeType,
|
|
228
|
+
block,
|
|
229
|
+
layout: typeof node.layout === 'string' ? node.layout : null,
|
|
230
|
+
text: typeof node.text === 'string' ? node.text : null,
|
|
231
|
+
url: webUrl(node.url) || webUrl(node.previewUrl),
|
|
232
|
+
color: typeof node.color === 'string' ? node.color : null,
|
|
233
|
+
backgroundColor: typeof node.backgroundColor === 'string'
|
|
234
|
+
? node.backgroundColor
|
|
235
|
+
: typeof inherited.backgroundColor === 'string'
|
|
236
|
+
? inherited.backgroundColor
|
|
237
|
+
: nodeType === 'bubble'
|
|
238
|
+
? '#FFFFFF'
|
|
239
|
+
: null,
|
|
240
|
+
background: normalizeGradient(node.background),
|
|
241
|
+
size: typeof node.size === 'string' ? node.size : null,
|
|
242
|
+
weight: typeof node.weight === 'string' ? node.weight : null,
|
|
243
|
+
wrap: typeof node.wrap === 'boolean' ? node.wrap : null,
|
|
244
|
+
margin: typeof node.margin === 'string' ? node.margin : null,
|
|
245
|
+
spacing: typeof node.spacing === 'string' ? node.spacing : null,
|
|
246
|
+
paddingAll: typeof node.paddingAll === 'string' ? node.paddingAll : null,
|
|
247
|
+
paddingTop: typeof node.paddingTop === 'string' ? node.paddingTop : null,
|
|
248
|
+
paddingBottom: typeof node.paddingBottom === 'string' ? node.paddingBottom : null,
|
|
249
|
+
paddingStart: typeof node.paddingStart === 'string' ? node.paddingStart : null,
|
|
250
|
+
paddingEnd: typeof node.paddingEnd === 'string' ? node.paddingEnd : null,
|
|
251
|
+
style: typeof node.style === 'string' ? node.style : null,
|
|
252
|
+
height: typeof node.height === 'string' ? node.height : null,
|
|
253
|
+
width: typeof node.width === 'string' ? node.width : null,
|
|
254
|
+
maxWidth: typeof node.maxWidth === 'string' ? node.maxWidth : null,
|
|
255
|
+
maxHeight: typeof node.maxHeight === 'string' ? node.maxHeight : null,
|
|
256
|
+
justifyContent: typeof node.justifyContent === 'string' ? node.justifyContent : null,
|
|
257
|
+
alignItems: typeof node.alignItems === 'string' ? node.alignItems : null,
|
|
258
|
+
cornerRadius: typeof node.cornerRadius === 'string' ? node.cornerRadius : null,
|
|
259
|
+
borderColor: typeof node.borderColor === 'string' ? node.borderColor : null,
|
|
260
|
+
borderWidth: typeof node.borderWidth === 'string' ? node.borderWidth : null,
|
|
261
|
+
separator: inherited.separator === true,
|
|
262
|
+
separatorColor: typeof inherited.separatorColor === 'string'
|
|
263
|
+
? inherited.separatorColor
|
|
264
|
+
: null,
|
|
265
|
+
maxLines: Number.isFinite(node.maxLines) ? Number(node.maxLines) : null,
|
|
266
|
+
decoration: typeof node.decoration === 'string' ? node.decoration : null,
|
|
267
|
+
position: typeof node.position === 'string' ? node.position : null,
|
|
268
|
+
offsetTop: typeof node.offsetTop === 'string' ? node.offsetTop : null,
|
|
269
|
+
offsetBottom: typeof node.offsetBottom === 'string' ? node.offsetBottom : null,
|
|
270
|
+
offsetStart: typeof node.offsetStart === 'string' ? node.offsetStart : null,
|
|
271
|
+
offsetEnd: typeof node.offsetEnd === 'string' ? node.offsetEnd : null,
|
|
272
|
+
resolved: resolveFlexStyle(node),
|
|
273
|
+
aspectRatio: typeof node.aspectRatio === 'string' ? node.aspectRatio : null,
|
|
274
|
+
aspectMode: node.aspectMode === 'fit' || node.aspectMode === 'cover'
|
|
275
|
+
? node.aspectMode
|
|
276
|
+
: null,
|
|
277
|
+
align: typeof node.align === 'string' ? node.align : null,
|
|
278
|
+
gravity: typeof node.gravity === 'string' ? node.gravity : null,
|
|
279
|
+
flex: Number.isFinite(node.flex) ? Number(node.flex) : null,
|
|
280
|
+
action: flexAction(node.action),
|
|
281
|
+
children: childValues
|
|
282
|
+
.map(([child, childBlock, childStyle]) => normalizeFlexNode(child, depth + 1, childBlock, childStyle))
|
|
283
|
+
.filter((child) => Boolean(child)),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
export function processFlexMessage(message) {
|
|
287
|
+
const metadata = message?.contentMetadata && typeof message.contentMetadata === 'object'
|
|
288
|
+
? message.contentMetadata
|
|
289
|
+
: {};
|
|
290
|
+
const explicitAlt = typeof metadata.ALT_TEXT === 'string' ? metadata.ALT_TEXT : '';
|
|
291
|
+
const markup = parseObject(metadata.MARKUP_JSON);
|
|
292
|
+
if (markup && typeof markup === 'object' && !Array.isArray(markup)) {
|
|
293
|
+
const record = markup;
|
|
294
|
+
const canvas = record.canvas;
|
|
295
|
+
const scene = record.scenes?.[canvas?.initialScene];
|
|
296
|
+
if (canvas && scene && Array.isArray(scene.listeners)) {
|
|
297
|
+
const regions = scene.listeners.flatMap((listener) => {
|
|
298
|
+
const [x, y, width, height] = listener?.params ?? [];
|
|
299
|
+
const action = flexAction(record.actions?.[listener?.action]);
|
|
300
|
+
if (![x, y, width, height].every(Number.isFinite) || !action)
|
|
301
|
+
return [];
|
|
302
|
+
return [{ x, y, width, height, action }];
|
|
303
|
+
});
|
|
304
|
+
return {
|
|
305
|
+
version: 1,
|
|
306
|
+
layoutVersion: 1,
|
|
307
|
+
layoutFingerprint: flexLayoutFingerprint(null),
|
|
308
|
+
kind: 'imagemap',
|
|
309
|
+
altText: explicitAlt,
|
|
310
|
+
width: Number(canvas.width) || null,
|
|
311
|
+
height: Number(canvas.height) || null,
|
|
312
|
+
imageUrl: resolveImagemapImageUrl(metadata.DOWNLOAD_URL, canvas.width),
|
|
313
|
+
regions,
|
|
314
|
+
root: null,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const key of FLEX_PAYLOAD_KEYS) {
|
|
319
|
+
if (key === 'MARKUP_JSON')
|
|
320
|
+
continue;
|
|
321
|
+
const parsed = parseObject(metadata[key]);
|
|
322
|
+
if (!parsed)
|
|
323
|
+
continue;
|
|
324
|
+
const root = normalizeFlexNode(parsed);
|
|
325
|
+
if (root) {
|
|
326
|
+
return {
|
|
327
|
+
version: 1,
|
|
328
|
+
layoutVersion: 1,
|
|
329
|
+
layoutFingerprint: flexLayoutFingerprint(root),
|
|
330
|
+
kind: 'flex',
|
|
331
|
+
altText: explicitAlt,
|
|
332
|
+
width: null,
|
|
333
|
+
height: null,
|
|
334
|
+
imageUrl: null,
|
|
335
|
+
regions: [],
|
|
336
|
+
root,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const textPayload = parseObject(message?.text);
|
|
341
|
+
const root = normalizeFlexNode(textPayload);
|
|
342
|
+
return root
|
|
343
|
+
? {
|
|
344
|
+
version: 1,
|
|
345
|
+
layoutVersion: 1,
|
|
346
|
+
layoutFingerprint: flexLayoutFingerprint(root),
|
|
347
|
+
kind: 'flex',
|
|
348
|
+
altText: explicitAlt,
|
|
349
|
+
width: null,
|
|
350
|
+
height: null,
|
|
351
|
+
imageUrl: null,
|
|
352
|
+
regions: [],
|
|
353
|
+
root,
|
|
354
|
+
}
|
|
355
|
+
: null;
|
|
356
|
+
}
|
|
34
357
|
/** Interpret LINE bubble/carousel content in header→hero→body→footer order. */
|
|
35
358
|
export function interpretFlexPayload(payload) {
|
|
36
359
|
const texts = [];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createCliLogger } from '../../util/log.js';
|
|
2
|
-
import { resolveUserNames } from '../names.js';
|
|
2
|
+
import { contactDisplayName, resolveUserNames } from '../names.js';
|
|
3
3
|
import { jsonResult, toolError, toonResult } from './shared.js';
|
|
4
4
|
const log = createCliLogger('Yomi');
|
|
5
5
|
/**
|
|
@@ -23,12 +23,16 @@ async function fetchAllContacts(service) {
|
|
|
23
23
|
if (!contact?.mid) {
|
|
24
24
|
continue;
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const name = contactDisplayName(contact);
|
|
27
|
+
if (name) {
|
|
28
|
+
service.nameCache.set(contact.mid, name);
|
|
28
29
|
}
|
|
29
30
|
summaries.push({
|
|
30
31
|
mid: contact.mid,
|
|
31
|
-
displayName:
|
|
32
|
+
displayName: name,
|
|
33
|
+
...(contact.displayNameOverridden && contact.displayName
|
|
34
|
+
? { profileName: contact.displayName }
|
|
35
|
+
: {}),
|
|
32
36
|
});
|
|
33
37
|
}
|
|
34
38
|
return summaries;
|
|
@@ -47,7 +51,8 @@ export async function handleListContacts(service) {
|
|
|
47
51
|
}
|
|
48
52
|
/**
|
|
49
53
|
* Handle `find_contact` — case-insensitive substring match over the
|
|
50
|
-
* friend list's displayName
|
|
54
|
+
* friend list's displayName (and profileName, when the user renamed the
|
|
55
|
+
* friend), so a caller can resolve a person's name to
|
|
51
56
|
* the MID `send_message` needs for a 1:1. Pure lookup: no fuzzy scoring,
|
|
52
57
|
* no ranking by interaction history.
|
|
53
58
|
*
|
|
@@ -61,7 +66,8 @@ export async function handleFindContact(service, args) {
|
|
|
61
66
|
}
|
|
62
67
|
const needle = args.name.toLowerCase();
|
|
63
68
|
const contacts = await fetchAllContacts(service);
|
|
64
|
-
const matches = contacts.filter((contact) => contact.displayName?.toLowerCase().includes(needle)
|
|
69
|
+
const matches = contacts.filter((contact) => contact.displayName?.toLowerCase().includes(needle) ||
|
|
70
|
+
contact.profileName?.toLowerCase().includes(needle));
|
|
65
71
|
return toonResult(matches);
|
|
66
72
|
}
|
|
67
73
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSquareChatMid } from '../../line/client/square-service/client.js';
|
|
1
2
|
import { sanitizeMessagePreview } from '../../line/core/message-preview.js';
|
|
2
3
|
import { decryptLineMessage } from '../../line/core/message-query-service.js';
|
|
3
4
|
import { getExcludedChatIds } from '../../search/scope.js';
|
|
@@ -73,6 +74,38 @@ export async function handleListConversations(service, args) {
|
|
|
73
74
|
unreadCount: box.unreadCount ?? 0,
|
|
74
75
|
};
|
|
75
76
|
}));
|
|
77
|
+
try {
|
|
78
|
+
const squareChats = await service.client.getJoinedSquareChats(args.limit ?? 20);
|
|
79
|
+
for (const chat of squareChats) {
|
|
80
|
+
let last = chat.lastMessage;
|
|
81
|
+
let senderName = chat.lastSenderName;
|
|
82
|
+
if (!last) {
|
|
83
|
+
const recent = await service.client.getSquareChatMessages(chat.id, 1);
|
|
84
|
+
const normalized = recent.at(-1);
|
|
85
|
+
if (normalized) {
|
|
86
|
+
last = {
|
|
87
|
+
4: normalized.id,
|
|
88
|
+
5: normalized.createdTime,
|
|
89
|
+
10: normalized.text,
|
|
90
|
+
15: normalized.contentType,
|
|
91
|
+
};
|
|
92
|
+
senderName = normalized.fromName;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const preview = sanitizeMessagePreview(last?.[10]);
|
|
96
|
+
conversations.push({
|
|
97
|
+
id: chat.id,
|
|
98
|
+
lastMessagePreview: maskInto(acc, preview || (last ? '[社群訊息]' : null)),
|
|
99
|
+
name: maskInto(acc, chat.name),
|
|
100
|
+
unreadCount: chat.unreadCount,
|
|
101
|
+
...(senderName ? { lastSenderName: maskInto(acc, senderName) } : {}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// OpenChat availability differs by LINE account/region. TalkService
|
|
107
|
+
// conversations remain authoritative when SquareService is unavailable.
|
|
108
|
+
}
|
|
76
109
|
const content = [
|
|
77
110
|
{ type: 'text', text: toonText(conversations) },
|
|
78
111
|
];
|
|
@@ -109,6 +142,27 @@ export async function handleGetChatMessages(service, args) {
|
|
|
109
142
|
if (!args.chatId) {
|
|
110
143
|
return toolError('chatId is required.');
|
|
111
144
|
}
|
|
145
|
+
if (isSquareChatMid(args.chatId)) {
|
|
146
|
+
const squareMessages = await service.client.getSquareChatMessages(args.chatId, args.count ?? 50);
|
|
147
|
+
const acc = createPhiAccumulator();
|
|
148
|
+
const shaped = squareMessages.map((message) => ({
|
|
149
|
+
createdTime: message.createdTime,
|
|
150
|
+
deliveredTime: message.createdTime,
|
|
151
|
+
from: message.fromMid,
|
|
152
|
+
fromName: maskInto(acc, message.fromName),
|
|
153
|
+
id: message.id,
|
|
154
|
+
mediaType: resolveLineMediaType(Number(message.contentType)),
|
|
155
|
+
mentions: message.contentMetadata?.MENTION ?? null,
|
|
156
|
+
text: maskInto(acc, sanitizeMessagePreview(message.text) || '[社群訊息]'),
|
|
157
|
+
e2eeDecrypted: null,
|
|
158
|
+
service: 'SQUARE',
|
|
159
|
+
}));
|
|
160
|
+
const content = [{ type: 'text', text: toonText(shaped) }];
|
|
161
|
+
const note = phiNote(acc);
|
|
162
|
+
if (note)
|
|
163
|
+
content.push(note);
|
|
164
|
+
return { content };
|
|
165
|
+
}
|
|
112
166
|
const fetched = args.before
|
|
113
167
|
? await service.getPreviousMessages(args.chatId, args.count ?? 50, args.before)
|
|
114
168
|
: await service.getRecentMessages(args.chatId, args.count ?? 50);
|
|
@@ -226,6 +226,11 @@ export async function handleSendFile(service, args) {
|
|
|
226
226
|
* person's mid in contentMetadata. `displayName` is resolved from the mid when
|
|
227
227
|
* the caller omits it. One send per call; honest error on failure.
|
|
228
228
|
*
|
|
229
|
+
* The recipient reads this string, so it is always the contact's own profile
|
|
230
|
+
* name (contact-query field 22) and never the name a shared cache holds. That
|
|
231
|
+
* cache exists to show the user the name they gave a friend locally, which is
|
|
232
|
+
* a private label and must not travel to a third party.
|
|
233
|
+
*
|
|
229
234
|
* @param service - Resumed LineProtocolService.
|
|
230
235
|
* @param args - Tool arguments.
|
|
231
236
|
* @returns MCP tool result.
|
|
@@ -240,19 +245,11 @@ export async function handleSendContact(service, args) {
|
|
|
240
245
|
let displayName = args.displayName ?? '';
|
|
241
246
|
if (!displayName) {
|
|
242
247
|
try {
|
|
243
|
-
|
|
248
|
+
const contact = await service.getContact(args.contactMid);
|
|
249
|
+
displayName = contact?.displayName ?? '';
|
|
244
250
|
}
|
|
245
251
|
catch {
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
if (!displayName) {
|
|
249
|
-
try {
|
|
250
|
-
const contact = await service.getContact(args.contactMid);
|
|
251
|
-
displayName = contact?.displayName ?? '';
|
|
252
|
-
}
|
|
253
|
-
catch {
|
|
254
|
-
// Leave empty — LINE resolves the name from the mid on the recipient side.
|
|
255
|
-
}
|
|
252
|
+
// Leave empty — LINE resolves the name from the mid on the recipient side.
|
|
256
253
|
}
|
|
257
254
|
}
|
|
258
255
|
const result = await service.sendContact(args.chatId, args.contactMid, displayName);
|
package/dist/mcp/media.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* callers get an honest error naming the reason.
|
|
15
15
|
*/
|
|
16
16
|
import { downloadLineMessageData, downloadLineObsMediaObject, } from '../line/client/obs-media-client.js';
|
|
17
|
+
import { isSquareChatMid } from '../line/client/square-service/client.js';
|
|
17
18
|
import { CONTENT_TYPE } from '../line/core/constants.js';
|
|
18
19
|
import { decryptLineMediaBytes } from '../line/core/media-decrypt.js';
|
|
19
20
|
/** LINE content types this server can download and decrypt via the OBS path. */
|
|
@@ -57,6 +58,51 @@ const MIME_BY_EXTENSION = {
|
|
|
57
58
|
webp: 'image/webp',
|
|
58
59
|
zip: 'application/zip',
|
|
59
60
|
};
|
|
61
|
+
const TRUSTED_PUBLIC_MEDIA_HOST_SUFFIXES = [
|
|
62
|
+
'line-scdn.net',
|
|
63
|
+
'line-apps.com',
|
|
64
|
+
'cdn.hinet.net',
|
|
65
|
+
];
|
|
66
|
+
export function resolvePublicMediaUrl(message, preview) {
|
|
67
|
+
const metadata = message?.contentMetadata ?? {};
|
|
68
|
+
if (metadata.PUBLIC !== 'true')
|
|
69
|
+
return null;
|
|
70
|
+
const raw = preview
|
|
71
|
+
? metadata.PREVIEW_URL || metadata.DOWNLOAD_URL
|
|
72
|
+
: metadata.DOWNLOAD_URL || metadata.PREVIEW_URL;
|
|
73
|
+
if (typeof raw !== 'string' || !raw)
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(raw);
|
|
77
|
+
const hostname = url.hostname.toLowerCase();
|
|
78
|
+
if (url.protocol !== 'https:' ||
|
|
79
|
+
!TRUSTED_PUBLIC_MEDIA_HOST_SUFFIXES.some((suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`))) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return url;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function downloadPublicMedia(url) {
|
|
89
|
+
const response = await fetch(url, { redirect: 'error' });
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`Public LINE media download failed (${response.status})`);
|
|
92
|
+
}
|
|
93
|
+
const length = Number(response.headers.get('content-length') ?? 0);
|
|
94
|
+
if (Number.isFinite(length) && length > 25 * 1024 * 1024) {
|
|
95
|
+
throw new Error('Public LINE media exceeds 25 MB');
|
|
96
|
+
}
|
|
97
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
98
|
+
if (bytes.length === 0 || bytes.length > 25 * 1024 * 1024) {
|
|
99
|
+
throw new Error('Public LINE media returned invalid bytes');
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
bytes,
|
|
103
|
+
mimeType: response.headers.get('content-type')?.split(';')[0] || null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
60
106
|
/**
|
|
61
107
|
* Resolve the short media-kind label for a LINE content type.
|
|
62
108
|
*
|
|
@@ -125,10 +171,18 @@ export function resolveLineMediaDescriptor(message) {
|
|
|
125
171
|
}
|
|
126
172
|
const dataPayload = parseJson(message?.text, {});
|
|
127
173
|
const mediaInfo = parseJson(contentMetadata.MEDIA_CONTENT_INFO, {});
|
|
174
|
+
const fileName = dataPayload.fileName ||
|
|
175
|
+
(typeof contentMetadata.FILE_NAME === 'string' &&
|
|
176
|
+
contentMetadata.FILE_NAME) ||
|
|
177
|
+
(typeof contentMetadata.FILE_NAME_ORIGINAL === 'string' &&
|
|
178
|
+
contentMetadata.FILE_NAME_ORIGINAL) ||
|
|
179
|
+
(typeof contentMetadata.name === 'string' && contentMetadata.name) ||
|
|
180
|
+
null;
|
|
181
|
+
const extension = mediaInfo.extension || (fileName ? fileName.split('.').pop() : null) || null;
|
|
128
182
|
return {
|
|
129
|
-
fileName
|
|
183
|
+
fileName,
|
|
130
184
|
keyMaterial: dataPayload.keyMaterial || null,
|
|
131
|
-
mimeType: resolveMimeType(
|
|
185
|
+
mimeType: resolveMimeType(extension, Number(message?.contentType || 0)),
|
|
132
186
|
oid: String(contentMetadata.OID),
|
|
133
187
|
sid: String(contentMetadata.SID),
|
|
134
188
|
};
|
|
@@ -165,6 +219,38 @@ class UnsupportedMediaTypeError extends LineMediaAccessError {
|
|
|
165
219
|
* @returns Decrypted media bytes, resolved MIME type, filename, and content type.
|
|
166
220
|
*/
|
|
167
221
|
export async function fetchLineMessageMedia(service, chatId, messageId, preview) {
|
|
222
|
+
if (isSquareChatMid(chatId)) {
|
|
223
|
+
const mainMessages = await service.client.getSquareChatMessages(chatId, 100);
|
|
224
|
+
let message = mainMessages.find((candidate) => candidate.id === messageId);
|
|
225
|
+
if (!message) {
|
|
226
|
+
for (const root of mainMessages.filter((candidate) => candidate.isThreadRoot && candidate.threadMid)) {
|
|
227
|
+
const replies = await service.client.getSquareChatMessages(chatId, 100, root.threadMid);
|
|
228
|
+
message = replies.find((candidate) => candidate.id === messageId);
|
|
229
|
+
if (message)
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!message) {
|
|
234
|
+
throw new MissingDecryptMaterialError(`Square message ${messageId} not found in ${chatId}`);
|
|
235
|
+
}
|
|
236
|
+
const contentType = Number(message.contentType);
|
|
237
|
+
if (!DOWNLOADABLE_CONTENT_TYPES.has(contentType)) {
|
|
238
|
+
throw new UnsupportedMediaTypeError(contentType, messageId);
|
|
239
|
+
}
|
|
240
|
+
const downloaded = await downloadLineMessageData(service.client, {
|
|
241
|
+
messageId,
|
|
242
|
+
preview,
|
|
243
|
+
square: true,
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
bytes: downloaded.bytes,
|
|
247
|
+
contentType,
|
|
248
|
+
fileName: message.contentMetadata?.FILE_NAME || null,
|
|
249
|
+
mimeType: downloaded.mimeType ||
|
|
250
|
+
DEFAULT_MIME_BY_CONTENT_TYPE[contentType] ||
|
|
251
|
+
'application/octet-stream',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
168
254
|
const messages = await service.getRecentMessages(chatId, 100);
|
|
169
255
|
const message = messages.find((candidate) => candidate.id === messageId);
|
|
170
256
|
if (!message) {
|
|
@@ -174,6 +260,19 @@ export async function fetchLineMessageMedia(service, chatId, messageId, preview)
|
|
|
174
260
|
if (!DOWNLOADABLE_CONTENT_TYPES.has(contentType)) {
|
|
175
261
|
throw new UnsupportedMediaTypeError(contentType, messageId);
|
|
176
262
|
}
|
|
263
|
+
const publicMediaUrl = resolvePublicMediaUrl(message, preview);
|
|
264
|
+
if (publicMediaUrl) {
|
|
265
|
+
const downloaded = await downloadPublicMedia(publicMediaUrl);
|
|
266
|
+
const extension = publicMediaUrl.pathname.split('.').pop() ?? null;
|
|
267
|
+
return {
|
|
268
|
+
bytes: downloaded.bytes,
|
|
269
|
+
contentType,
|
|
270
|
+
fileName: null,
|
|
271
|
+
mimeType: downloaded.mimeType ||
|
|
272
|
+
resolveMimeType(extension, contentType) ||
|
|
273
|
+
'application/octet-stream',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
177
276
|
const descriptor = resolveLineMediaDescriptor(message);
|
|
178
277
|
if (!descriptor) {
|
|
179
278
|
// Older/non-E2EE Talk messages carry MEDIA_CONTENT_INFO but omit OID/SID.
|