amiru 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +244 -2
- package/cordis.patch.yml +16 -0
- package/icon.svg +13 -0
- package/index.js +191 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +53 -4
- package/src/config.js +64 -0
- package/src/memory.js +122 -0
- package/src/persona.js +161 -0
- package/src/proactive.js +101 -0
- package/src/prompt.js +163 -0
- package/src/qq.js +87 -0
- package/src/session-events.js +35 -0
- package/src/store.js +251 -0
package/src/store.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs'
|
|
10
|
+
import { dirname, join } from 'node:path'
|
|
11
|
+
|
|
12
|
+
function readJson(path, fallback) {
|
|
13
|
+
try {
|
|
14
|
+
if (!existsSync(path)) return fallback
|
|
15
|
+
const text = readFileSync(path, 'utf8').trim()
|
|
16
|
+
if (!text) return fallback
|
|
17
|
+
return JSON.parse(text)
|
|
18
|
+
} catch {
|
|
19
|
+
return fallback
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeJsonAtomic(path, value) {
|
|
24
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
25
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`
|
|
26
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
|
|
27
|
+
renameSync(tmp, path)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function textOfMessage(message) {
|
|
31
|
+
const content = message?.content
|
|
32
|
+
if (!Array.isArray(content)) return ''
|
|
33
|
+
return content
|
|
34
|
+
.filter((block) => block && block.type === 'text' && typeof block.text === 'string')
|
|
35
|
+
.map((block) => block.text)
|
|
36
|
+
.join('\n')
|
|
37
|
+
.trim()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function truncate(text, max) {
|
|
41
|
+
const s = String(text ?? '').replace(/\s+/g, ' ').trim()
|
|
42
|
+
return s.length > max ? `${s.slice(0, max - 1)}…` : s
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function bindingIdFor(accountId, chatKey) {
|
|
46
|
+
return createHash('sha256').update(`${accountId}|${chatKey}`).digest('hex').slice(0, 24)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class AmiruStore {
|
|
50
|
+
constructor(dataDir) {
|
|
51
|
+
this.dir = dataDir
|
|
52
|
+
this.bindingsPath = join(dataDir, 'bindings.json')
|
|
53
|
+
this.memoriesPath = join(dataDir, 'memories.json')
|
|
54
|
+
this.messagesPath = join(dataDir, 'messages.jsonl')
|
|
55
|
+
this.bindings = readJson(this.bindingsPath, {})
|
|
56
|
+
this.memories = readJson(this.memoriesPath, {})
|
|
57
|
+
this.messages = this.#loadMessages()
|
|
58
|
+
mkdirSync(dataDir, { recursive: true })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#loadMessages() {
|
|
62
|
+
if (!existsSync(this.messagesPath)) return []
|
|
63
|
+
try {
|
|
64
|
+
const lines = readFileSync(this.messagesPath, 'utf8').split('\n')
|
|
65
|
+
const out = []
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
if (!line.trim()) continue
|
|
68
|
+
try {
|
|
69
|
+
out.push(JSON.parse(line))
|
|
70
|
+
} catch {
|
|
71
|
+
// One torn line must not make the rest of the diary unreadable.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out.slice(-5000)
|
|
75
|
+
} catch {
|
|
76
|
+
return []
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#saveBindings() {
|
|
81
|
+
writeJsonAtomic(this.bindingsPath, this.bindings)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#saveMemories() {
|
|
85
|
+
writeJsonAtomic(this.memoriesPath, this.memories)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getBinding(bindingId) {
|
|
89
|
+
return this.bindings[bindingId]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getBindingBySession(sessionId) {
|
|
93
|
+
return Object.values(this.bindings).find((b) => b.sessionId === sessionId)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
listBindings() {
|
|
97
|
+
return Object.values(this.bindings)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ensureBinding({ accountId, chatKey, scope, peerId, sessionId, appId, personaId }) {
|
|
101
|
+
const bindingId = bindingIdFor(accountId, chatKey)
|
|
102
|
+
let binding = this.bindings[bindingId]
|
|
103
|
+
const now = Date.now()
|
|
104
|
+
if (!binding) {
|
|
105
|
+
binding = {
|
|
106
|
+
bindingId,
|
|
107
|
+
accountId,
|
|
108
|
+
appId,
|
|
109
|
+
chatKey,
|
|
110
|
+
scope,
|
|
111
|
+
peerId,
|
|
112
|
+
sessionId,
|
|
113
|
+
personaId: personaId || 'default',
|
|
114
|
+
createdAt: now,
|
|
115
|
+
lastUserAt: 0,
|
|
116
|
+
lastAssistantAt: 0,
|
|
117
|
+
lastProactiveAt: 0,
|
|
118
|
+
proactiveDay: '',
|
|
119
|
+
proactiveCount: 0,
|
|
120
|
+
unansweredProactive: 0,
|
|
121
|
+
userMessageCount: 0,
|
|
122
|
+
assistantMessageCount: 0,
|
|
123
|
+
proactiveEnabled: true,
|
|
124
|
+
optedOut: false,
|
|
125
|
+
}
|
|
126
|
+
} else if (binding.sessionId !== sessionId) {
|
|
127
|
+
binding.sessionId = sessionId
|
|
128
|
+
}
|
|
129
|
+
this.bindings[bindingId] = binding
|
|
130
|
+
this.#saveBindings()
|
|
131
|
+
return binding
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
updateBinding(bindingId, patch) {
|
|
135
|
+
const binding = this.bindings[bindingId]
|
|
136
|
+
if (!binding) return undefined
|
|
137
|
+
Object.assign(binding, patch)
|
|
138
|
+
this.#saveBindings()
|
|
139
|
+
return binding
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setPersona(bindingId, personaId) {
|
|
143
|
+
return this.updateBinding(bindingId, { personaId })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
appendMessage({ bindingId, sessionId, role, message, time = Date.now() }) {
|
|
147
|
+
const text = textOfMessage(message)
|
|
148
|
+
if (!text) return null
|
|
149
|
+
const row = {
|
|
150
|
+
id: randomUUID(),
|
|
151
|
+
bindingId,
|
|
152
|
+
sessionId,
|
|
153
|
+
role,
|
|
154
|
+
text,
|
|
155
|
+
time,
|
|
156
|
+
}
|
|
157
|
+
appendFileSync(this.messagesPath, `${JSON.stringify(row)}\n`, 'utf8')
|
|
158
|
+
this.messages.push(row)
|
|
159
|
+
if (this.messages.length > 5000) this.messages.splice(0, this.messages.length - 5000)
|
|
160
|
+
return row
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
recentMessages(bindingId, limit = 10, { excludeSessionId } = {}) {
|
|
164
|
+
const rows = this.messages.filter(
|
|
165
|
+
(m) => m.bindingId === bindingId && (!excludeSessionId || m.sessionId !== excludeSessionId),
|
|
166
|
+
)
|
|
167
|
+
return rows.slice(-limit)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
previousSessionMessages(bindingId, currentSessionId, limit = 10) {
|
|
171
|
+
const rows = this.messages.filter(
|
|
172
|
+
(m) => m.bindingId === bindingId && m.sessionId !== currentSessionId,
|
|
173
|
+
)
|
|
174
|
+
return rows.slice(-limit)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
addMemory(bindingId, { text, kind = 'note', importance = 3 }) {
|
|
178
|
+
const value = String(text ?? '').trim()
|
|
179
|
+
if (!value) return null
|
|
180
|
+
const item = {
|
|
181
|
+
id: randomUUID(),
|
|
182
|
+
bindingId,
|
|
183
|
+
text: value.slice(0, 1000),
|
|
184
|
+
kind: String(kind || 'note').slice(0, 40),
|
|
185
|
+
importance: Math.max(1, Math.min(5, Number(importance) || 3)),
|
|
186
|
+
time: Date.now(),
|
|
187
|
+
}
|
|
188
|
+
const list = this.memories[bindingId] ?? []
|
|
189
|
+
list.push(item)
|
|
190
|
+
this.memories[bindingId] = list.slice(-200)
|
|
191
|
+
this.#saveMemories()
|
|
192
|
+
return item
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
listMemories(bindingId, limit = 8) {
|
|
196
|
+
const list = this.memories[bindingId] ?? []
|
|
197
|
+
return [...list]
|
|
198
|
+
.sort((a, b) => (b.importance - a.importance) || (b.time - a.time))
|
|
199
|
+
.slice(0, limit)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
recordUserMessage(bindingId, time = Date.now()) {
|
|
203
|
+
const b = this.bindings[bindingId]
|
|
204
|
+
if (!b) return
|
|
205
|
+
b.lastUserAt = time
|
|
206
|
+
b.userMessageCount = (b.userMessageCount || 0) + 1
|
|
207
|
+
b.unansweredProactive = 0
|
|
208
|
+
this.#saveBindings()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
recordAssistantMessage(bindingId, time = Date.now()) {
|
|
212
|
+
const b = this.bindings[bindingId]
|
|
213
|
+
if (!b) return
|
|
214
|
+
b.lastAssistantAt = time
|
|
215
|
+
b.assistantMessageCount = (b.assistantMessageCount || 0) + 1
|
|
216
|
+
this.#saveBindings()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
recordProactive(bindingId, day, time = Date.now()) {
|
|
220
|
+
const b = this.bindings[bindingId]
|
|
221
|
+
if (!b) return
|
|
222
|
+
if (b.proactiveDay !== day) {
|
|
223
|
+
b.proactiveDay = day
|
|
224
|
+
b.proactiveCount = 0
|
|
225
|
+
}
|
|
226
|
+
b.lastProactiveAt = time
|
|
227
|
+
b.proactiveCount = (b.proactiveCount || 0) + 1
|
|
228
|
+
b.unansweredProactive = (b.unansweredProactive || 0) + 1
|
|
229
|
+
this.#saveBindings()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
stats(bindingId) {
|
|
233
|
+
const b = this.bindings[bindingId]
|
|
234
|
+
if (!b) return null
|
|
235
|
+
const messages = this.messages.filter((m) => m.bindingId === bindingId)
|
|
236
|
+
return {
|
|
237
|
+
binding: b,
|
|
238
|
+
messages: messages.length,
|
|
239
|
+
memories: (this.memories[bindingId] ?? []).length,
|
|
240
|
+
lastMessage: messages.at(-1) ?? null,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function extractMessageText(message) {
|
|
246
|
+
return textOfMessage(message)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function shorten(text, max = 160) {
|
|
250
|
+
return truncate(text, max)
|
|
251
|
+
}
|