@rikaidev/yomi 0.5.0 → 0.6.1
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 +25 -12
- 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/integrator.js +21 -7
- 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 +25 -4
- package/run.mjs +186 -9
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export const CURRENT_SUMMARY_ENGINE_VERSION = 1;
|
|
3
|
+
const EPISODE_GAP_MS = 45 * 60 * 1000; // 45 minutes
|
|
4
|
+
const ROLLUP_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
5
|
+
function computeDigest(content) {
|
|
6
|
+
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
7
|
+
}
|
|
8
|
+
function inferDialogueActs(events) {
|
|
9
|
+
const acts = new Set();
|
|
10
|
+
for (const event of events) {
|
|
11
|
+
const text = event.text ?? '';
|
|
12
|
+
const isQuestion = /[??]/.test(text);
|
|
13
|
+
if (isQuestion) {
|
|
14
|
+
acts.add('ask');
|
|
15
|
+
}
|
|
16
|
+
if (['image', 'video', 'file'].includes(event.contentType)) {
|
|
17
|
+
acts.add('deliver');
|
|
18
|
+
}
|
|
19
|
+
if (event.mentionsSelf || (event.metadata.mentionMids?.length ?? 0) > 0) {
|
|
20
|
+
acts.add('directive');
|
|
21
|
+
}
|
|
22
|
+
if (event.contentType === 'sticker') {
|
|
23
|
+
acts.add('acknowledge');
|
|
24
|
+
}
|
|
25
|
+
if (text.length > 0 && !isQuestion) {
|
|
26
|
+
acts.add('inform');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return [...acts];
|
|
30
|
+
}
|
|
31
|
+
export function buildEpisodeSummaries(events, engineVersion = CURRENT_SUMMARY_ENGINE_VERSION) {
|
|
32
|
+
if (events.length === 0)
|
|
33
|
+
return [];
|
|
34
|
+
const byChat = new Map();
|
|
35
|
+
for (const event of events) {
|
|
36
|
+
const list = byChat.get(event.chatId) ?? [];
|
|
37
|
+
list.push(event);
|
|
38
|
+
byChat.set(event.chatId, list);
|
|
39
|
+
}
|
|
40
|
+
const summaries = [];
|
|
41
|
+
for (const [chatId, chatEvents] of byChat) {
|
|
42
|
+
const sorted = [...chatEvents].sort((a, b) => a.sentAt - b.sentAt);
|
|
43
|
+
let currentEpisode = [];
|
|
44
|
+
const flush = () => {
|
|
45
|
+
if (currentEpisode.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
const minSentAt = currentEpisode[0].sentAt;
|
|
48
|
+
const maxSentAt = currentEpisode[currentEpisode.length - 1].sentAt;
|
|
49
|
+
const messageIds = currentEpisode.map((e) => e.messageId);
|
|
50
|
+
const participantNames = [
|
|
51
|
+
...new Set(currentEpisode
|
|
52
|
+
.map((e) => e.senderName)
|
|
53
|
+
.filter((n) => Boolean(n))),
|
|
54
|
+
];
|
|
55
|
+
const ownerHints = [
|
|
56
|
+
...new Set(currentEpisode.flatMap((e) => e.metadata.mentionNames ?? [])),
|
|
57
|
+
];
|
|
58
|
+
const hasMentionsSelf = currentEpisode.some((e) => e.mentionsSelf);
|
|
59
|
+
const hasQuestions = currentEpisode.some((e) => /[??]/.test(e.text ?? ''));
|
|
60
|
+
const hasMedia = currentEpisode.some((e) => ['image', 'video', 'file', 'audio'].includes(e.contentType));
|
|
61
|
+
const dialogueActs = inferDialogueActs(currentEpisode);
|
|
62
|
+
const hasSelfQuestion = currentEpisode.some((e) => e.isSelf && /[??]/.test(e.text ?? ''));
|
|
63
|
+
const selfQuestionIdx = currentEpisode.findIndex((e) => e.isSelf && /[??]/.test(e.text ?? ''));
|
|
64
|
+
const hasReplyAfterSelfQuestion = selfQuestionIdx >= 0 &&
|
|
65
|
+
currentEpisode.slice(selfQuestionIdx + 1).some((e) => !e.isSelf);
|
|
66
|
+
const responseState = hasReplyAfterSelfQuestion
|
|
67
|
+
? 'received'
|
|
68
|
+
: hasSelfQuestion
|
|
69
|
+
? 'waiting'
|
|
70
|
+
: 'observing';
|
|
71
|
+
const digest = computeDigest(`${chatId}:${minSentAt}:${messageIds.join(',')}`);
|
|
72
|
+
const episodeId = `ep-${chatId.slice(0, 8)}-${minSentAt}`;
|
|
73
|
+
summaries.push({
|
|
74
|
+
episodeId,
|
|
75
|
+
chatId,
|
|
76
|
+
minSentAt,
|
|
77
|
+
maxSentAt,
|
|
78
|
+
messageIds,
|
|
79
|
+
participantNames,
|
|
80
|
+
dialogueActs,
|
|
81
|
+
ownerHints,
|
|
82
|
+
responseState,
|
|
83
|
+
hasMentionsSelf,
|
|
84
|
+
hasQuestions,
|
|
85
|
+
hasMedia,
|
|
86
|
+
candidateCount: hasMentionsSelf || hasSelfQuestion ? 1 : 0,
|
|
87
|
+
digest,
|
|
88
|
+
engineVersion,
|
|
89
|
+
});
|
|
90
|
+
currentEpisode = [];
|
|
91
|
+
};
|
|
92
|
+
for (const event of sorted) {
|
|
93
|
+
if (currentEpisode.length > 0 &&
|
|
94
|
+
event.sentAt - currentEpisode[currentEpisode.length - 1].sentAt >
|
|
95
|
+
EPISODE_GAP_MS) {
|
|
96
|
+
flush();
|
|
97
|
+
}
|
|
98
|
+
currentEpisode.push(event);
|
|
99
|
+
}
|
|
100
|
+
flush();
|
|
101
|
+
}
|
|
102
|
+
return summaries.sort((a, b) => a.minSentAt - b.minSentAt);
|
|
103
|
+
}
|
|
104
|
+
export function buildRollupSummary(windowStart, windowEnd, episodes, engineVersion = CURRENT_SUMMARY_ENGINE_VERSION) {
|
|
105
|
+
const matching = episodes.filter((ep) => ep.maxSentAt >= windowStart && ep.minSentAt <= windowEnd);
|
|
106
|
+
const activeChatIds = [...new Set(matching.map((ep) => ep.chatId))];
|
|
107
|
+
const episodeIds = matching.map((ep) => ep.episodeId);
|
|
108
|
+
const totalEvents = matching.reduce((sum, ep) => sum + ep.messageIds.length, 0);
|
|
109
|
+
const unresolvedFrontierCount = matching.filter((ep) => ep.responseState === 'waiting' || ep.hasMentionsSelf).length;
|
|
110
|
+
const topics = matching
|
|
111
|
+
.filter((ep) => ep.dialogueActs.length > 0)
|
|
112
|
+
.map((ep) => `${ep.chatId}:${ep.dialogueActs.join('+')}`)
|
|
113
|
+
.slice(0, 10);
|
|
114
|
+
const rollupId = `rollup-${Math.floor(windowStart / ROLLUP_INTERVAL_MS)}`;
|
|
115
|
+
const digest = computeDigest(`${rollupId}:${episodeIds.join(',')}:${totalEvents}`);
|
|
116
|
+
return {
|
|
117
|
+
rollupId,
|
|
118
|
+
windowStart,
|
|
119
|
+
windowEnd,
|
|
120
|
+
activeChatIds,
|
|
121
|
+
episodeIds,
|
|
122
|
+
totalEvents,
|
|
123
|
+
unresolvedFrontierCount,
|
|
124
|
+
topics,
|
|
125
|
+
digest,
|
|
126
|
+
engineVersion,
|
|
127
|
+
};
|
|
128
|
+
}
|
package/dist/attention/policy.js
CHANGED
|
@@ -49,23 +49,29 @@ export async function scoreAttentionBatch(inputs, embedder = getDefaultEmbedder(
|
|
|
49
49
|
const messageVectors = vectors.slice(anchors.length);
|
|
50
50
|
return inputs.map((input, inputIndex) => {
|
|
51
51
|
if (input.isMuted)
|
|
52
|
-
return { score: -100, tier: 'filtered', reason: '
|
|
52
|
+
return { score: -100, tier: 'filtered', reason: 'muted' };
|
|
53
53
|
if (input.explicitPriority)
|
|
54
|
-
return { score: 70, tier: 'now', reason: '
|
|
54
|
+
return { score: 70, tier: 'now', reason: 'manual_priority' };
|
|
55
55
|
if (input.hasMention)
|
|
56
|
-
return { score: 65, tier: 'now', reason: '
|
|
56
|
+
return { score: 65, tier: 'now', reason: 'direct_mention' };
|
|
57
|
+
if (input.isOfficial &&
|
|
58
|
+
/^\[yomi:(?:image|rich|sticker|video|audio|file|extended_image)\]$/.test(input.text.trim())) {
|
|
59
|
+
return { score: 0, tier: 'filtered', reason: 'ordinary' };
|
|
60
|
+
}
|
|
57
61
|
const similarities = new Map();
|
|
58
62
|
anchors.forEach((anchor, anchorIndex) => {
|
|
59
63
|
const similarity = dot(messageVectors[inputIndex], anchorVectors[anchorIndex]);
|
|
60
64
|
similarities.set(anchor.intent, Math.max(similarities.get(anchor.intent) ?? -Infinity, similarity));
|
|
61
65
|
});
|
|
62
66
|
const ranked = [...intents].sort((a, b) => (similarities.get(b) ?? 0) - (similarities.get(a) ?? 0));
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const intent = ranked[0];
|
|
68
|
+
if (input.isOfficial && intent !== 'transaction') {
|
|
69
|
+
return {
|
|
70
|
+
score: -40,
|
|
71
|
+
tier: 'filtered',
|
|
72
|
+
reason: intent === 'promotion' ? 'promotion' : 'ordinary',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
69
75
|
const scoreByIntent = {
|
|
70
76
|
reply: 55,
|
|
71
77
|
deadline: 60,
|
|
@@ -74,7 +80,7 @@ export async function scoreAttentionBatch(inputs, embedder = getDefaultEmbedder(
|
|
|
74
80
|
ordinary: 0,
|
|
75
81
|
promotion: -40,
|
|
76
82
|
};
|
|
77
|
-
const score = scoreByIntent[intent]
|
|
83
|
+
const score = scoreByIntent[intent];
|
|
78
84
|
const tier = score >= 55
|
|
79
85
|
? 'now'
|
|
80
86
|
: score >= 35
|
|
@@ -83,12 +89,12 @@ export async function scoreAttentionBatch(inputs, embedder = getDefaultEmbedder(
|
|
|
83
89
|
? 'know'
|
|
84
90
|
: 'filtered';
|
|
85
91
|
const reasons = {
|
|
86
|
-
reply: '
|
|
87
|
-
deadline: '
|
|
88
|
-
transaction: '
|
|
89
|
-
important: '
|
|
90
|
-
ordinary: '
|
|
91
|
-
promotion: '
|
|
92
|
+
reply: 'reply_expected',
|
|
93
|
+
deadline: 'deadline',
|
|
94
|
+
transaction: 'transaction',
|
|
95
|
+
important: 'important',
|
|
96
|
+
ordinary: 'ordinary',
|
|
97
|
+
promotion: 'promotion',
|
|
92
98
|
};
|
|
93
99
|
return { score, tier, reason: reasons[intent] };
|
|
94
100
|
});
|
|
@@ -86,7 +86,7 @@ class VolatileCacheStore {
|
|
|
86
86
|
cache = new Map();
|
|
87
87
|
loaded = false;
|
|
88
88
|
dirty = false;
|
|
89
|
-
|
|
89
|
+
flushPromise = null;
|
|
90
90
|
constructor(filePath) {
|
|
91
91
|
this.filePath = filePath;
|
|
92
92
|
}
|
|
@@ -139,11 +139,10 @@ class VolatileCacheStore {
|
|
|
139
139
|
/** Coalesce bursts of writes into a single atomic flush cycle. */
|
|
140
140
|
scheduleFlush() {
|
|
141
141
|
this.dirty = true;
|
|
142
|
-
if (this.
|
|
142
|
+
if (this.flushPromise) {
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
|
-
this.
|
|
146
|
-
void (async () => {
|
|
145
|
+
this.flushPromise = (async () => {
|
|
147
146
|
const fs = await import('node:fs/promises');
|
|
148
147
|
while (this.dirty) {
|
|
149
148
|
this.dirty = false;
|
|
@@ -169,8 +168,13 @@ class VolatileCacheStore {
|
|
|
169
168
|
// Best-effort cache persistence; in-memory copy still serves reads.
|
|
170
169
|
}
|
|
171
170
|
}
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
})().finally(() => {
|
|
172
|
+
this.flushPromise = null;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async flush() {
|
|
176
|
+
while (this.flushPromise)
|
|
177
|
+
await this.flushPromise;
|
|
174
178
|
}
|
|
175
179
|
}
|
|
176
180
|
/**
|
|
@@ -196,11 +200,12 @@ export class CredentialStore {
|
|
|
196
200
|
this.filePath = fallbackFilePath;
|
|
197
201
|
this.cache = new Map();
|
|
198
202
|
this.loaded = false;
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
203
|
+
// The user-isolated credential file (0600 mode) is fast, reliable, and avoids
|
|
204
|
+
// triggering disruptive macOS SecurityAgent GUI prompts. Mutating the Keychain
|
|
205
|
+
// via `security` CLI is opt-in (via YOMI_USE_KEYCHAIN=1) rather than default.
|
|
202
206
|
this.secureStorageEnabled =
|
|
203
207
|
process.platform === 'darwin' &&
|
|
208
|
+
process.env.YOMI_USE_KEYCHAIN === '1' &&
|
|
204
209
|
process.env.YOMI_NO_KEYCHAIN !== '1' &&
|
|
205
210
|
process.env.YOMI_READ_ONLY_SESSION !== '1';
|
|
206
211
|
this.keychainService = this.secureStorageEnabled
|
|
@@ -229,8 +234,12 @@ export class CredentialStore {
|
|
|
229
234
|
}
|
|
230
235
|
}
|
|
231
236
|
catch { }
|
|
232
|
-
|
|
233
|
-
|
|
237
|
+
const keychain = this.keychainService ??
|
|
238
|
+
(process.platform === 'darwin' && process.env.YOMI_NO_KEYCHAIN !== '1'
|
|
239
|
+
? getKeychainService()
|
|
240
|
+
: null);
|
|
241
|
+
if (keychain) {
|
|
242
|
+
const result = await keychain.getCredential(this.keychainAccount);
|
|
234
243
|
if (result.success && result.password) {
|
|
235
244
|
try {
|
|
236
245
|
const fs = await import('node:fs/promises');
|
|
@@ -243,7 +252,6 @@ export class CredentialStore {
|
|
|
243
252
|
catch { }
|
|
244
253
|
return result.password;
|
|
245
254
|
}
|
|
246
|
-
return null;
|
|
247
255
|
}
|
|
248
256
|
return null;
|
|
249
257
|
}
|
|
@@ -361,6 +369,11 @@ export class CredentialStore {
|
|
|
361
369
|
return persisted;
|
|
362
370
|
});
|
|
363
371
|
}
|
|
372
|
+
/** Await durable and volatile write queues, primarily for orderly shutdown. */
|
|
373
|
+
async flush() {
|
|
374
|
+
await this.persistQueue;
|
|
375
|
+
await this.volatile.flush();
|
|
376
|
+
}
|
|
364
377
|
/**
|
|
365
378
|
* Delete a credential and persist.
|
|
366
379
|
* @param key - Credential key to delete.
|