@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.
Files changed (62) hide show
  1. package/README.md +28 -0
  2. package/dist/ambient/ambient-event-graph.js +172 -0
  3. package/dist/ambient/ambient-observer.js +71 -0
  4. package/dist/ambient/event-graph-reducer.js +300 -0
  5. package/dist/ambient/ingestion-adapter.js +203 -0
  6. package/dist/ambient/journal/crypto.js +58 -0
  7. package/dist/ambient/journal/journal-store.js +284 -0
  8. package/dist/ambient/journal/types.js +7 -0
  9. package/dist/ambient/summaries/retriever.js +55 -0
  10. package/dist/ambient/summaries/summary-engine.js +128 -0
  11. package/dist/ambient/summaries/types.js +4 -0
  12. package/dist/attention/policy.js +22 -16
  13. package/dist/auth/credential-store.js +15 -6
  14. package/dist/cli/query.js +276 -69
  15. package/dist/cli/work-item.js +24 -0
  16. package/dist/line/client/auth-service/client.js +7 -8
  17. package/dist/line/client/buddy-service/client.js +73 -0
  18. package/dist/line/client/index.js +28 -1
  19. package/dist/line/client/obs-media-client.js +2 -1
  20. package/dist/line/client/square-service/client.js +298 -0
  21. package/dist/line/client/talk-service/command-client.js +9 -1
  22. package/dist/line/client/talk-service/message-id-query.js +27 -3
  23. package/dist/line/client/talk-service/requests.js +9 -0
  24. package/dist/line/core/auth-session-runtime.js +1 -1
  25. package/dist/line/core/chat-event.js +2 -0
  26. package/dist/line/core/chat-runtime-service.js +18 -3
  27. package/dist/line/core/config.js +1 -0
  28. package/dist/line/core/flex-message.js +323 -0
  29. package/dist/line/core/service.js +0 -1
  30. package/dist/mcp/handlers/contacts.js +12 -6
  31. package/dist/mcp/handlers/conversations.js +54 -0
  32. package/dist/mcp/handlers/send.js +8 -11
  33. package/dist/mcp/media.js +101 -2
  34. package/dist/mcp/names.js +78 -6
  35. package/dist/search/default-embedder.js +12 -3
  36. package/dist/version.js +1 -1
  37. package/dist/work-item/adapter.js +1 -0
  38. package/dist/work-item/destination-contracts.js +1 -0
  39. package/dist/work-item/event-context.js +73 -0
  40. package/dist/work-item/gold-contract.js +101 -0
  41. package/dist/work-item/gold-coverage.js +96 -0
  42. package/dist/work-item/gold-derivation.js +67 -0
  43. package/dist/work-item/gold-split.js +143 -0
  44. package/dist/work-item/index.js +23 -0
  45. package/dist/work-item/line-adapter.js +111 -0
  46. package/dist/work-item/llama-cpp-work-event-adapter.js +239 -0
  47. package/dist/work-item/local-provider.js +38 -0
  48. package/dist/work-item/local-work-event-provider.js +56 -0
  49. package/dist/work-item/onnx-work-event-adapter.js +223 -0
  50. package/dist/work-item/provider.js +21 -0
  51. package/dist/work-item/registry.js +19 -0
  52. package/dist/work-item/service.js +85 -0
  53. package/dist/work-item/store.js +97 -0
  54. package/dist/work-item/types.js +79 -0
  55. package/dist/work-item/work-event-graph-engine.js +63 -0
  56. package/dist/work-item/work-event-inference.js +267 -0
  57. package/dist/work-item/work-event-matcher.js +105 -0
  58. package/dist/work-item/work-event-model-manifest.js +39 -0
  59. package/dist/work-item/work-event-store.js +125 -0
  60. package/dist/work-item/work-event.js +123 -0
  61. package/package.json +24 -3
  62. package/run.mjs +162 -9
@@ -20,11 +20,14 @@ import { createCliLogger } from '../../util/log.js';
20
20
  import { LINE_APP_CONFIG } from '../core/config.js';
21
21
  import { encodeCallMessage } from '../core/thrift/index.js';
22
22
  import { createAuthClient } from './auth-service/client.js';
23
+ import { createBuddyClient } from './buddy-service/client.js';
23
24
  import { createE2EEClient } from './e2ee-service/client.js';
24
25
  import { parseOperation } from './parsers.js';
25
26
  import { createShopClient } from './shop-service/client.js';
27
+ import { createSquareClient } from './square-service/client.js';
26
28
  import { createSyncClient } from './sync-service/client.js';
27
29
  import { createTalkCommandClient } from './talk-service/command-client.js';
30
+ import { decodeBinaryResponseMessage, encodeBinaryCallMessage, postBinary, } from './talk-service/message-id-query.js';
28
31
  import { createTalkQueryClient } from './talk-service/query-client.js';
29
32
  import { sendRequest } from './transport.js';
30
33
  const TALK_EXCEPTION_CODE_NAMES = {
@@ -130,7 +133,15 @@ export class LineClient extends EventEmitter {
130
133
  restoreE2EEKeyBackup;
131
134
  logoutZ;
132
135
  getOwnedStickerPackages;
136
+ getJoinedSquareChats;
137
+ getSquareChatMessages;
138
+ getSquareChatStatus;
139
+ getSquareChatLatestMessage;
140
+ markSquareChatRead;
141
+ sendSquareMessage;
142
+ getBuddyRichMenu;
133
143
  updateChatName;
144
+ updateContactNotificationDisabled;
134
145
  inviteIntoChat;
135
146
  deleteOtherFromChat;
136
147
  deleteSelfFromChat;
@@ -155,7 +166,7 @@ export class LineClient extends EventEmitter {
155
166
  this.seq = 1;
156
167
  this.logger = config.logger || createCliLogger('LINE');
157
168
  this.startupFlowLogger = config.startupFlowLogger || null;
158
- Object.assign(this, createAuthClient(this), createTalkQueryClient(this), createTalkCommandClient(this), createSyncClient(this), createE2EEClient(this), createShopClient(this));
169
+ Object.assign(this, createAuthClient(this), createBuddyClient(this), createTalkQueryClient(this), createTalkCommandClient(this), createSyncClient(this), createE2EEClient(this), createShopClient(this), createSquareClient(this));
159
170
  }
160
171
  /**
161
172
  * Send a TCompact Thrift call to an arbitrary LINE service endpoint path.
@@ -199,6 +210,22 @@ export class LineClient extends EventEmitter {
199
210
  async sendTalk(method, args) {
200
211
  return this.sendCompact(LINE_APP_CONFIG.talkPath, method, args);
201
212
  }
213
+ async sendBinary(path, method, args) {
214
+ const body = encodeBinaryCallMessage(method, this.seq++, args);
215
+ const response = await postBinary(this.host, path, body, this.authToken);
216
+ if (response.nextToken) {
217
+ this.authToken = response.nextToken;
218
+ this.emit('tokenRotated', response.nextToken);
219
+ }
220
+ if (!response.body.length) {
221
+ throw new Error(`${method}(${path}) returned an empty response`);
222
+ }
223
+ const decoded = decodeBinaryResponseMessage(response.body);
224
+ if (decoded.fields?.[1]) {
225
+ throw new Error(`${method}(${path}) failed: ${decoded.fields[1]?.[2] || JSON.stringify(decoded.fields[1])}`);
226
+ }
227
+ return decoded;
228
+ }
202
229
  /**
203
230
  * Parse a raw Thrift operation struct into a domain object.
204
231
  *
@@ -27,7 +27,8 @@ export async function downloadLineMessageData(client, options) {
27
27
  throw new Error('LINE legacy media download requires a message id');
28
28
  }
29
29
  const suffix = options.preview ? '/preview' : '';
30
- const requestUrl = `https://${OBS_HOST}/r/talk/m/${encodeURIComponent(messageId)}${suffix}`;
30
+ const namespace = options.square ? 'g2' : 'talk';
31
+ const requestUrl = `https://${OBS_HOST}/r/${namespace}/m/${encodeURIComponent(messageId)}${suffix}`;
31
32
  const response = await fetch(requestUrl, {
32
33
  method: 'GET',
33
34
  headers: {
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Read-only LINE OpenChat (SquareService) capability.
3
+ *
4
+ * OpenChat is a separate service from TalkService: joined chats use `t...`
5
+ * MIDs and never appear in TalkService message boxes. Keep the protocol
6
+ * boundary explicit so callers cannot accidentally treat Square messages as
7
+ * ordinary group messages.
8
+ */
9
+ import { LINE_APP_CONFIG } from '../../core/config.js';
10
+ import { i32Field, i64Field, mapField, setField, stringField, structField, } from '../../core/thrift/index.js';
11
+ function mapEntries(value) {
12
+ if (value instanceof Map)
13
+ return Array.from(value.entries());
14
+ if (value && typeof value === 'object')
15
+ return Object.entries(value);
16
+ return [];
17
+ }
18
+ /** LINE currently emits `m...` SquareChat MIDs; older schemas used `t...`. */
19
+ export function isSquareChatMid(value) {
20
+ return value.startsWith('m') || value.startsWith('t');
21
+ }
22
+ function normalizeJoinedChat(chat, status, member) {
23
+ const id = String(chat?.[1] || '');
24
+ if (!isSquareChatMid(id))
25
+ return null;
26
+ const lastSquareMessage = status?.[3];
27
+ return {
28
+ id,
29
+ squareMid: chat?.[2] ? String(chat[2]) : null,
30
+ memberMid: member?.[1] ? String(member[1]) : null,
31
+ name: String(chat?.[4] || 'LINE 社群'),
32
+ pictureHash: chat?.[5] ? String(chat[5]) : null,
33
+ unreadCount: Number(status?.[5]?.[2] || 0),
34
+ lastMessage: lastSquareMessage?.[1] || null,
35
+ lastSenderName: status?.[4] ? String(status[4]) : null,
36
+ isMuted: member?.[5] === false,
37
+ };
38
+ }
39
+ function eventMessage(event) {
40
+ const payload = event?.[4];
41
+ const received = payload?.[1];
42
+ const sent = payload?.[2];
43
+ const mutated = payload?.[41];
44
+ const notification = payload?.[30];
45
+ const envelope = received || sent || mutated || notification;
46
+ const squareMessage = envelope?.[2];
47
+ const message = squareMessage?.[1];
48
+ if (!message)
49
+ return null;
50
+ const chatId = String(envelope?.[1] || message?.[2] || '');
51
+ const id = String(message?.[4] || '');
52
+ if (!chatId || !id)
53
+ return null;
54
+ const fromMid = String(message?.[1] || '');
55
+ return {
56
+ id,
57
+ chatId,
58
+ fromMid,
59
+ fromName: fromMid ? `成員 ${fromMid.slice(-4)}` : '匿名成員',
60
+ fromPictureUrl: null,
61
+ isSelf: Boolean(sent),
62
+ text: typeof message?.[10] === 'string' ? message[10] : '',
63
+ createdTime: Number(message?.[6] || message?.[5] || event?.[2] || 0),
64
+ contentType: Number(message?.[15] || 0),
65
+ contentMetadata: message?.[18] && typeof message[18] === 'object' ? message[18] : {},
66
+ threadMid: squareMessage?.[6]?.[1]
67
+ ? String(squareMessage[6][1])
68
+ : envelope?.[7]
69
+ ? String(envelope[7])
70
+ : null,
71
+ isThreadRoot: Boolean(squareMessage?.[6]?.[2]),
72
+ };
73
+ }
74
+ export function createSquareClient(runtime) {
75
+ const memberProfiles = new Map();
76
+ async function resolveMember(squareMemberMid) {
77
+ const cached = memberProfiles.get(squareMemberMid);
78
+ if (cached)
79
+ return cached;
80
+ try {
81
+ const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'getSquareMember', [structField(1, [stringField(1, squareMemberMid)])]);
82
+ const member = result.fields?.[0]?.[1];
83
+ const displayName = String(member?.[3] || '').trim();
84
+ const pictureHash = String(member?.[4] || '').trim();
85
+ if (displayName) {
86
+ const profile = {
87
+ displayName,
88
+ pictureUrl: pictureHash
89
+ ? `https://obs.line-scdn.net/${pictureHash.replace(/^\/+/, '')}`
90
+ : null,
91
+ };
92
+ memberProfiles.set(squareMemberMid, profile);
93
+ return profile;
94
+ }
95
+ }
96
+ catch (error) {
97
+ if (process.env.YOMI_DEBUG_SQUARE_MEMBER === '1') {
98
+ console.error('[square] getSquareMember failed', error instanceof Error ? error.message : String(error));
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ async function resolveMembers(squareMemberMids) {
104
+ const unresolved = squareMemberMids.filter((mid) => !memberProfiles.has(mid));
105
+ if (unresolved.length === 0)
106
+ return;
107
+ try {
108
+ const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'getSquareMembers', [structField(1, [setField(2, 11, unresolved)])]);
109
+ const raw = result.fields?.[0]?.[1];
110
+ const candidates = raw && typeof raw?.[1] === 'string'
111
+ ? [raw]
112
+ : mapEntries(raw).map(([, member]) => member);
113
+ for (const member of candidates) {
114
+ const mid = String(member?.[1] || '');
115
+ const displayName = String(member?.[3] || '').trim();
116
+ const pictureHash = String(member?.[4] || '').trim();
117
+ if (!mid || !displayName)
118
+ continue;
119
+ memberProfiles.set(mid, {
120
+ displayName,
121
+ pictureUrl: pictureHash
122
+ ? `https://obs.line-scdn.net/${pictureHash.replace(/^\/+/, '')}`
123
+ : null,
124
+ });
125
+ }
126
+ }
127
+ catch (error) {
128
+ if (process.env.YOMI_DEBUG_SQUARE_MEMBER === '1') {
129
+ console.error('[square] getSquareMembers failed', error instanceof Error ? error.message : String(error));
130
+ }
131
+ }
132
+ }
133
+ return {
134
+ async getJoinedSquareChats(limit = 100) {
135
+ const chats = [];
136
+ let continuationToken;
137
+ const seenTokens = [];
138
+ try {
139
+ for (;;) {
140
+ const request = [i32Field(3, Math.max(1, Math.min(limit, 100)))];
141
+ if (continuationToken)
142
+ request.unshift(stringField(2, continuationToken));
143
+ const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'getJoinedSquareChats', [structField(1, request)]);
144
+ const response = result.fields?.[0];
145
+ const statuses = mapEntries(response?.[3]);
146
+ const members = mapEntries(response?.[2]);
147
+ for (const raw of Array.isArray(response?.[1]) ? response[1] : []) {
148
+ const id = String(raw?.[1] || '');
149
+ const chat = normalizeJoinedChat(raw, statuses.find(([key]) => key === id)?.[1], members.find(([key]) => key === id)?.[1]);
150
+ if (chat)
151
+ chats.push(chat);
152
+ }
153
+ const next = response?.[4] ? String(response[4]) : '';
154
+ if (!next || seenTokens.includes(next) || chats.length >= limit)
155
+ break;
156
+ seenTokens.push(next);
157
+ continuationToken = next;
158
+ }
159
+ }
160
+ catch (error) {
161
+ // LINE's Desktop identity currently answers getJoinedSquareChats with
162
+ // `501 NO XLT`. A bootstrap fetchMyEvents(subscriptionId=0) returns
163
+ // the authoritative CREATE_SQUARE_CHAT_MEMBER snapshots instead.
164
+ chats.length = 0;
165
+ let eventContinuation;
166
+ const seenEventTokens = [];
167
+ for (;;) {
168
+ const request = [i64Field(1, 0), i32Field(3, 100)];
169
+ if (eventContinuation)
170
+ request.push(stringField(4, eventContinuation));
171
+ const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'fetchMyEvents', [structField(1, request)]);
172
+ const response = result.fields?.[0];
173
+ for (const event of Array.isArray(response?.[2]) ? response[2] : []) {
174
+ const createdMembership = event?.[4]?.[17];
175
+ const chat = normalizeJoinedChat(createdMembership?.[1], createdMembership?.[2], createdMembership?.[3]);
176
+ if (chat)
177
+ chats.push(chat);
178
+ }
179
+ const next = response?.[4] ? String(response[4]) : '';
180
+ if (!next || seenEventTokens.includes(next) || chats.length >= limit)
181
+ break;
182
+ seenEventTokens.push(next);
183
+ eventContinuation = next;
184
+ }
185
+ if (chats.length === 0)
186
+ throw error;
187
+ }
188
+ return chats
189
+ .filter((chat, index) => chats.findIndex((item) => item.id === chat.id) === index)
190
+ .slice(0, limit);
191
+ },
192
+ async getSquareChatMessages(chatId, limit = 40, threadMid) {
193
+ if (!isSquareChatMid(chatId))
194
+ throw new Error('Invalid OpenChat MID');
195
+ const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'fetchSquareChatEvents', [
196
+ structField(1, [
197
+ i64Field(1, 0),
198
+ stringField(2, chatId),
199
+ i32Field(4, Math.max(1, Math.min(limit, 100))),
200
+ i32Field(5, 1),
201
+ i32Field(6, 1),
202
+ i32Field(8, 1),
203
+ ...(threadMid ? [stringField(9, threadMid)] : []),
204
+ ]),
205
+ ]);
206
+ const events = result.fields?.[0]?.[2];
207
+ const messages = [];
208
+ for (const event of Array.isArray(events) ? events : []) {
209
+ const message = eventMessage(event);
210
+ if (!message || message.chatId !== chatId)
211
+ continue;
212
+ const existingIndex = messages.findIndex((item) => item.id === message.id);
213
+ if (existingIndex >= 0)
214
+ messages[existingIndex] = message;
215
+ else
216
+ messages.push(message);
217
+ }
218
+ const senderMids = [
219
+ ...new Set(messages.map((message) => message.fromMid).filter(Boolean)),
220
+ ];
221
+ await resolveMembers(senderMids);
222
+ await Promise.all(senderMids.map(async (mid) => {
223
+ const profile = await resolveMember(mid);
224
+ if (!profile)
225
+ return;
226
+ for (const message of messages) {
227
+ if (message.fromMid !== mid)
228
+ continue;
229
+ message.fromName = profile.displayName;
230
+ message.fromPictureUrl = profile.pictureUrl;
231
+ }
232
+ }));
233
+ return messages
234
+ .sort((a, b) => a.createdTime - b.createdTime)
235
+ .slice(-limit);
236
+ },
237
+ async getSquareChatStatus(chatId) {
238
+ if (!isSquareChatMid(chatId))
239
+ throw new Error('Invalid OpenChat MID');
240
+ const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'getSquareChatStatus', [structField(1, [stringField(2, chatId)])]);
241
+ return result.fields?.[0]?.[1] || null;
242
+ },
243
+ async getSquareChatLatestMessage(chatId) {
244
+ if (!isSquareChatMid(chatId))
245
+ throw new Error('Invalid OpenChat MID');
246
+ const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'getSquareChatStatus', [structField(1, [stringField(2, chatId)])]);
247
+ const status = result.fields?.[0]?.[1];
248
+ const raw = status?.[3]?.[1];
249
+ if (!raw?.[4])
250
+ return null;
251
+ const fromMid = String(raw?.[1] || '');
252
+ const profile = fromMid ? await resolveMember(fromMid) : null;
253
+ return {
254
+ id: String(raw[4]),
255
+ chatId,
256
+ fromMid,
257
+ fromName: profile?.displayName || String(status?.[4] || '') || '匿名成員',
258
+ fromPictureUrl: profile?.pictureUrl || null,
259
+ isSelf: false,
260
+ text: typeof raw?.[10] === 'string' ? raw[10] : '',
261
+ createdTime: Number(raw?.[6] || raw?.[5] || 0),
262
+ contentType: Number(raw?.[15] || 0),
263
+ contentMetadata: raw?.[18] && typeof raw[18] === 'object' ? raw[18] : {},
264
+ threadMid: null,
265
+ isThreadRoot: false,
266
+ };
267
+ },
268
+ async markSquareChatRead(chatId, messageId) {
269
+ if (!isSquareChatMid(chatId))
270
+ throw new Error('Invalid OpenChat MID');
271
+ if (!messageId)
272
+ throw new Error('OpenChat message id required');
273
+ await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'markAsRead', [
274
+ structField(1, [stringField(2, chatId), stringField(4, messageId)]),
275
+ ]);
276
+ },
277
+ async sendSquareMessage(chatId, text, contentMetadata = {}) {
278
+ if (!isSquareChatMid(chatId))
279
+ throw new Error('Invalid OpenChat MID');
280
+ const messageFields = [
281
+ stringField(2, chatId),
282
+ stringField(10, text),
283
+ i32Field(15, 0),
284
+ ];
285
+ if (Object.keys(contentMetadata).length > 0) {
286
+ messageFields.push(mapField(18, 11, 11, contentMetadata));
287
+ }
288
+ const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'sendMessage', [
289
+ structField(1, [
290
+ i32Field(1, runtime.seq++),
291
+ stringField(2, chatId),
292
+ structField(3, [structField(1, messageFields), i64Field(4, 4)]),
293
+ ]),
294
+ ]);
295
+ return result.fields?.[0]?.[1] || null;
296
+ },
297
+ };
298
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { Buffer } from 'node:buffer';
7
7
  import { parseMessage } from '../parsers.js';
8
- import { buildAcceptChatInvitationRequest, buildCancelReactionRequest, buildContactMidActionRequest, buildCreateChatRequest, buildDeleteOtherFromChatRequest, buildDeleteSelfFromChatRequest, buildFindAndAddContactsByMidRequest, buildInviteIntoChatRequest, buildReactRequest, buildSendChatCheckedRequest, buildSendMessageRequest, buildUnsendMessageRequest, buildUpdateChatNameRequest, normalizeSendMessageOptions, } from './requests.js';
8
+ import { buildAcceptChatInvitationRequest, buildCancelReactionRequest, buildContactMidActionRequest, buildCreateChatRequest, buildDeleteOtherFromChatRequest, buildDeleteSelfFromChatRequest, buildFindAndAddContactsByMidRequest, buildInviteIntoChatRequest, buildReactRequest, buildSendChatCheckedRequest, buildSendMessageRequest, buildUnsendMessageRequest, buildUpdateChatNameRequest, buildUpdateContactNotificationRequest, normalizeSendMessageOptions, } from './requests.js';
9
9
  /**
10
10
  * Create the TalkService command capability bound to one LINE client runtime.
11
11
  *
@@ -60,6 +60,14 @@ export function createTalkCommandClient(runtime) {
60
60
  }
61
61
  return true;
62
62
  },
63
+ /** Mute or unmute notifications for a 1:1/official-account contact. */
64
+ async updateContactNotificationDisabled(mid, disabled) {
65
+ const result = await runtime.sendTalk('updateContactSetting', buildUpdateContactNotificationRequest(mid, disabled));
66
+ if (result.error) {
67
+ throw new Error(`updateContactSetting failed: ${result.error}`);
68
+ }
69
+ return true;
70
+ },
63
71
  /**
64
72
  * Rename a group/chat via TalkService updateChat. Side effect visible to
65
73
  * every member. Throws on LINE error.
@@ -32,6 +32,12 @@ const writeStruct = (fields) => {
32
32
  ]);
33
33
  };
34
34
  writeValue = (type, value) => {
35
+ if (type === 2) {
36
+ return Buffer.from([value ? 1 : 0]);
37
+ }
38
+ if (type === 8) {
39
+ return writeI32(Number(value));
40
+ }
35
41
  if (type === 10) {
36
42
  return writeI64(value);
37
43
  }
@@ -49,9 +55,17 @@ writeValue = (type, value) => {
49
55
  ...items.map((item) => writeValue(elementType, item)),
50
56
  ]);
51
57
  }
58
+ if (type === 14) {
59
+ const [elementType, items] = value;
60
+ return Buffer.concat([
61
+ Buffer.from([elementType]),
62
+ writeI32(items.length),
63
+ ...items.map((item) => writeValue(elementType, item)),
64
+ ]);
65
+ }
52
66
  throw new Error(`Unsupported TBinary write type: ${type}`);
53
67
  };
54
- const encodeBinaryCallMessage = (method, seqId, fields) => {
68
+ export const encodeBinaryCallMessage = (method, seqId, fields) => {
55
69
  return Buffer.concat([
56
70
  writeI32(0x80010001),
57
71
  writeString(method),
@@ -145,9 +159,19 @@ readValue = (buffer, state, type) => {
145
159
  }
146
160
  return items;
147
161
  }
162
+ if (type === 14) {
163
+ const elementType = buffer[state.position];
164
+ state.position += 1;
165
+ const length = readI32(buffer, state);
166
+ const items = [];
167
+ for (let index = 0; index < length; index += 1) {
168
+ items.push(readValue(buffer, state, elementType));
169
+ }
170
+ return items;
171
+ }
148
172
  throw new Error(`Unsupported TBinary read type: ${type}`);
149
173
  };
150
- const decodeBinaryResponseMessage = (buffer) => {
174
+ export const decodeBinaryResponseMessage = (buffer) => {
151
175
  const state = { position: 0 };
152
176
  readU32(buffer, state);
153
177
  const method = readString(buffer, state);
@@ -155,7 +179,7 @@ const decodeBinaryResponseMessage = (buffer) => {
155
179
  const fields = readStruct(buffer, state);
156
180
  return { fields, method, seqId };
157
181
  };
158
- const postBinary = (host, path, body, token) => {
182
+ export const postBinary = (host, path, body, token) => {
159
183
  return new Promise((resolve, reject) => {
160
184
  const request = https.request({
161
185
  headers: {
@@ -190,6 +190,15 @@ export function buildSendChatCheckedRequest(chatMid, lastMessageId, sessionId =
190
190
  byteField(4, sessionId),
191
191
  ];
192
192
  }
193
+ /** Build updateContactSetting for LINE's per-contact notification mute. */
194
+ export function buildUpdateContactNotificationRequest(mid, disabled) {
195
+ return [
196
+ i32Field(1, 0),
197
+ stringField(2, mid),
198
+ i32Field(3, 1),
199
+ stringField(4, disabled ? 'true' : 'false'),
200
+ ];
201
+ }
193
202
  /**
194
203
  * Build the updateChat request fields to rename a group/chat.
195
204
  *
@@ -5,7 +5,7 @@ import { createCliLogger } from '../../util/log.js';
5
5
  import { recoverBootstrapKeys } from './e2ee/recovery/bootstrap-keys.js';
6
6
  import { STATE } from './service.js';
7
7
  const TERMINAL_ERROR_REGEX = /LOGGED_OUT|DIVESTED/i;
8
- const AUTH_ERROR_REGEX = /expired|unauthorized|authentication|invalid.*token/i;
8
+ const AUTH_ERROR_REGEX = /expired|unauthorized|authentication|invalid.*token|access token refresh required/i;
9
9
  /**
10
10
  * Resolve the active LINE logger for one runtime.
11
11
  *
@@ -27,6 +27,8 @@ export function interpretChatEvent(message, names) {
27
27
  : '有人邀請成員加入群組';
28
28
  case 'C_MJ':
29
29
  return mids.length >= 1 ? `${name(0)} 加入群組` : '有成員加入群組';
30
+ case 'C_ML':
31
+ return mids.length >= 1 ? `${name(0)} 離開群組` : '有成員離開群組';
30
32
  default:
31
33
  return key ? `[聊天室活動:${key}]` : '[聊天室活動]';
32
34
  }
@@ -10,6 +10,7 @@
10
10
  * on a backfill-adapter module that was not extracted). See
11
11
  * yomi/README.md for the full extraction rationale.
12
12
  */
13
+ import { isSquareChatMid } from '../client/square-service/client.js';
13
14
  import { createMessageCommandService } from './message-command-service.js';
14
15
  import { createMessageQueryService } from './message-query-service.js';
15
16
  /**
@@ -38,6 +39,9 @@ export function createChatRuntimeService(service) {
38
39
  * @returns LINE sendMessage result (includes the sent message id).
39
40
  */
40
41
  async sendMessage(to, text, contentMetadata, reply) {
42
+ if (isSquareChatMid(to)) {
43
+ return service.client.sendSquareMessage(to, text, contentMetadata);
44
+ }
41
45
  return createMessageCommandService(() => service.client, service.e2eeManager).sendMessage(to, {
42
46
  e2ee: true,
43
47
  text,
@@ -168,13 +172,24 @@ export function createChatRuntimeService(service) {
168
172
  async markChatRead(chatId, messageId) {
169
173
  let lastMessageId = messageId ?? null;
170
174
  if (!lastMessageId) {
171
- const recent = await service.getRecentMessages(chatId, 1);
172
- lastMessageId = recent?.[0]?.id ? String(recent[0].id) : null;
175
+ if (isSquareChatMid(chatId)) {
176
+ const recent = await service.client.getSquareChatMessages(chatId, 100);
177
+ lastMessageId = recent.at(-1)?.id ? String(recent.at(-1).id) : null;
178
+ }
179
+ else {
180
+ const recent = await service.getRecentMessages(chatId, 1);
181
+ lastMessageId = recent?.[0]?.id ? String(recent[0].id) : null;
182
+ }
173
183
  }
174
184
  if (!lastMessageId) {
175
185
  return { marked: false, reason: 'no message to mark read' };
176
186
  }
177
- await service.client.sendChatChecked(chatId, lastMessageId, 0);
187
+ if (isSquareChatMid(chatId)) {
188
+ await service.client.markSquareChatRead(chatId, lastMessageId);
189
+ }
190
+ else {
191
+ await service.client.sendChatChecked(chatId, lastMessageId, 0);
192
+ }
178
193
  return { marked: true, chatId, lastMessageId };
179
194
  },
180
195
  /**
@@ -50,6 +50,7 @@ export const LINE_APP_CONFIG = {
50
50
  host: 'ga2.line.naver.jp',
51
51
  // Service paths
52
52
  talkPath: '/S4',
53
+ squarePath: '/SQ1',
53
54
  syncPath: '/SYNC4',
54
55
  authPath: '/AS4',
55
56
  revokePath: '/RS4',