dsh-line-bot 0.6.1 → 0.7.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/CHANGELOG.md +6 -0
- package/README.en.md +1 -0
- package/README.ja.md +1 -0
- package/README.ko.md +1 -0
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/lib/config.mjs +1 -0
- package/lib/host.mjs +83 -14
- package/lib/memory.mjs +18 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
- 群組內每位成員有自己的記憶點(同一個人到不同群組也分開),回覆時會注入該成員的記憶。
|
|
6
|
+
- 群組回覆會參考最近 4~6 則別人的訊息;沒被點名而擲骰不回時,訊息仍會留給下一輪參考。
|
|
7
|
+
- 群組「清空記憶」會同時清掉該群組所有成員的記憶。
|
|
8
|
+
|
|
3
9
|
## 0.6.1
|
|
4
10
|
|
|
5
11
|
- 修正模型用 `<meme#18>` 這種角度括號標記時,圖片沒送出、反而把標記當文字顯示的問題;現在支援 `<meme#N>`、`[[meme:#N]]`、`[[meme:N]]` 與 `[表情: …]`,並會清掉殘留的括號。
|
package/README.en.md
CHANGED
|
@@ -142,6 +142,7 @@ When the bot is added to a group (or a multi-user room):
|
|
|
142
142
|
- **Being @-mentioned always triggers a reply**, and **quoting any message always triggers a reply**; any other message is answered only when a dice roll against the *group reply chance* (default 15%) succeeds, otherwise it stays quiet.
|
|
143
143
|
- Each group gets its own **DSH session, workspace folder, persona and personal memory**, separate from direct chats and other groups.
|
|
144
144
|
- The `persona` command works in groups too; the panel's **Groups** card lets you tune the reply chance, toggles, per-group persona and clear a group's memory.
|
|
145
|
+
- Each member has **their own memory points** inside a group; replies also look at the last 4–6 messages from others so the bot can pick up the thread.
|
|
145
146
|
- Proactive messages and DeepSeek balance reminders still target direct chats only.
|
|
146
147
|
|
|
147
148
|
## Per-user memory and personas
|
package/README.ja.md
CHANGED
|
@@ -142,6 +142,7 @@ Bot がグループ(または複数人のルーム)に追加されたとき
|
|
|
142
142
|
- **@ で呼ばれたら必ず返信**、**引用(返信)されたら必ず返信**します。それ以外は「グループ返信確率」(既定 15%)の抽選に当たったときだけ返信します。
|
|
143
143
|
- グループごとに **DSH セッション・ワークスペース・ペルソナ・個人メモリ** を持ち、1 対 1 のチャットや他のグループとは分かれています。
|
|
144
144
|
- グループでも `人設` コマンドが使えます。パネルの「グループ」カードで返信確率・スイッチ・グループごとのペルソナ・メモリの消去を設定できます。
|
|
145
|
+
- グループ内でも**メンバーごとに別々の記憶**を持ちます。返信時は直近 4~6 件の他人のメッセージも参考にして話を拾います。
|
|
145
146
|
- 自発メッセージと DeepSeek 残高の通知は 1 対 1 の相手にだけ送られます。
|
|
146
147
|
|
|
147
148
|
## ユーザーごとのメモリとペルソナ
|
package/README.ko.md
CHANGED
|
@@ -142,6 +142,7 @@ Bot이 그룹(또는 다인 룸)에 추가되면:
|
|
|
142
142
|
- **@로 호출되면 항상 답장**하고, **인용(답장)되면 항상 답장**합니다. 그 외에는 "그룹 답장 확률"(기본 15%) 주사위를 굴려 당첨될 때만 답장합니다.
|
|
143
143
|
- 그룹마다 **DSH 세션, 작업 폴더, 페르소나, 개인 기억**이 따로 있으며 1:1 채팅이나 다른 그룹과 분리됩니다.
|
|
144
144
|
- 그룹에서도 `人設` 명령을 쓸 수 있습니다. 패널의 "그룹" 카드에서 답장 확률, 스위치, 그룹별 페르소나, 기억 지우기를 설정할 수 있습니다.
|
|
145
|
+
- 그룹 안에서도 **멤버마다 별도의 기억**을 가집니다. 답장할 때 최근 4~6개의 다른 사람 메시지도 참고해 대화를 이어갑니다.
|
|
145
146
|
- 자발적 메시지와 DeepSeek 잔액 알림은 여전히 1:1 대상에게만 갑니다.
|
|
146
147
|
|
|
147
148
|
## 사용자별 기억과 페르소나
|
package/README.md
CHANGED
|
@@ -142,6 +142,7 @@ Bot 被加進群組(或多人聊天室)後:
|
|
|
142
142
|
- **被 @ 一定回**、**被引用(回覆某則訊息)一定回**;其他訊息依「群組回覆機率」擲骰(預設 15%),沒中就安靜。
|
|
143
143
|
- 每個群組有自己的 **DSH session、工作資料夾、人設與個人記憶**,和一對一聊天、其他群組都分開。
|
|
144
144
|
- 群組裡也能用 `人設` 指令;面板「群組」卡可以調回覆機率、開關、個別群組的人設與清空記憶。
|
|
145
|
+
- 每個成員在群組裡有**自己的記憶點**;回覆時也會參考最近 4~6 則別人的訊息,接得上話題。
|
|
145
146
|
- 主動傳訊與 DeepSeek 餘額提醒仍然只找一對一的對象。
|
|
146
147
|
|
|
147
148
|
## 每位使用者各自的記憶與人設
|
package/README.zh-CN.md
CHANGED
|
@@ -142,6 +142,7 @@ Bot 被加入群组(或多人聊天室)后:
|
|
|
142
142
|
- **被 @ 一定回**、**被引用(回复某条消息)一定回**;其他消息按「群组回复概率」掷骰(默认 15%),没中就安静。
|
|
143
143
|
- 每个群组有自己的 **DSH session、工作文件夹、人设与个人记忆**,和一对一聊天、其他群组分开。
|
|
144
144
|
- 群组里也能用 `人设` 指令;面板「群组」卡可调回复概率、开关、个别群组的人设与清空记忆。
|
|
145
|
+
- 每个成员在群组里有**自己的记忆点**;回复时也会参考最近 4~6 条别人的消息,接得上话题。
|
|
145
146
|
- 主动传讯与 DeepSeek 余额提醒仍然只找一对一的对象。
|
|
146
147
|
|
|
147
148
|
## 每位用户各自的记忆与人设
|
package/lib/config.mjs
CHANGED
|
@@ -330,6 +330,7 @@ export function normalizeState(raw) {
|
|
|
330
330
|
return {
|
|
331
331
|
users: isPlainObject(state.users) ? state.users : {},
|
|
332
332
|
groups: isPlainObject(state.groups) ? state.groups : {},
|
|
333
|
+
people: isPlainObject(state.people) ? state.people : {},
|
|
333
334
|
nudge: isPlainObject(state.nudge) ? state.nudge : {},
|
|
334
335
|
balance: isPlainObject(state.balance) ? state.balance : {},
|
|
335
336
|
memory: isPlainObject(state.memory) ? state.memory : {},
|
package/lib/host.mjs
CHANGED
|
@@ -41,7 +41,7 @@ const { createMemeSource } = memeSourceMod
|
|
|
41
41
|
const { createStickerImages } = stickerImageMod
|
|
42
42
|
const { chunkText, clip, inQuietWindow, jsonFail, jsonOk, maskSecret, readJsonBody, safeArray, sleep } = utilMod
|
|
43
43
|
|
|
44
|
-
export const VERSION = '0.
|
|
44
|
+
export const VERSION = '0.7.0'
|
|
45
45
|
|
|
46
46
|
// 環境變數提供的 Cloudflare API Token(方便用 shell 注入、不用把 token 寫進設定檔)。
|
|
47
47
|
// 設定檔裡的 token 優先;兩者都沒有時才用環境變數。
|
|
@@ -317,10 +317,10 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
function agentContextText(
|
|
320
|
+
function agentContextText(ownerKey, kind = 'user', extra = '') {
|
|
321
321
|
// 群組不注入貼吧作息,避免在群組裡突然講起刷貼吧的事。
|
|
322
322
|
const tieba = kind === 'user' ? tiebaContextText() : ''
|
|
323
|
-
return [modelContextText(), tieba, balanceContextText(), memoryContextText(
|
|
323
|
+
return [modelContextText(), tieba, balanceContextText(), extra, memoryContextText(ownerKey), memeContextText() || stickerContextText()].filter(Boolean).join('\n\n')
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
async function refreshBalance() {
|
|
@@ -835,6 +835,51 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
835
835
|
}
|
|
836
836
|
}
|
|
837
837
|
|
|
838
|
+
function personName(userId) {
|
|
839
|
+
const state = store.getState()
|
|
840
|
+
return state.users?.[userId]?.displayName || state.people?.[userId]?.displayName || ''
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function ensurePerson(userId) {
|
|
844
|
+
const name = personName(userId)
|
|
845
|
+
if (name) return name
|
|
846
|
+
try {
|
|
847
|
+
const profile = await withToken((token) => lineApi.getProfile(token, userId))
|
|
848
|
+
if (profile?.displayName) {
|
|
849
|
+
store.updateState((state) => ({
|
|
850
|
+
...state,
|
|
851
|
+
people: {
|
|
852
|
+
...(state.people || {}),
|
|
853
|
+
[userId]: { displayName: profile.displayName, pictureUrl: profile.pictureUrl || '', at: Date.now() },
|
|
854
|
+
},
|
|
855
|
+
}))
|
|
856
|
+
return profile.displayName
|
|
857
|
+
}
|
|
858
|
+
} catch (error) {
|
|
859
|
+
logger.warn(`讀取 LINE 成員資料失敗(${userId}):${errorText(error)}`)
|
|
860
|
+
}
|
|
861
|
+
return ''
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function groupRecentText(groupId) {
|
|
865
|
+
const rows = (store.getState().groups?.[groupId]?.recent || []).slice(-6)
|
|
866
|
+
if (rows.length === 0) return ''
|
|
867
|
+
return [
|
|
868
|
+
'【群組最近的訊息】',
|
|
869
|
+
...rows.map((row) => `${row.name || row.userId}:${row.text}`),
|
|
870
|
+
'(上面是群組裡最近幾則別人的訊息;現在輪到你回應,可以自然接話,但不要逐條回顧。)',
|
|
871
|
+
].join('\n')
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function pushRecent(groupId, entry) {
|
|
875
|
+
store.updateState((state) => {
|
|
876
|
+
const group = state.groups?.[groupId]
|
|
877
|
+
if (!group) return state
|
|
878
|
+
const recent = [...(Array.isArray(group.recent) ? group.recent : []), entry].slice(-12)
|
|
879
|
+
return { ...state, groups: { ...state.groups, [groupId]: { ...group, recent } } }
|
|
880
|
+
})
|
|
881
|
+
}
|
|
882
|
+
|
|
838
883
|
function isAllowed(userId, cfg) {
|
|
839
884
|
const list = cfg.line.allowedUserIds || []
|
|
840
885
|
return list.length === 0 || list.includes(userId)
|
|
@@ -1278,17 +1323,25 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
1278
1323
|
if (!text) return
|
|
1279
1324
|
|
|
1280
1325
|
let groupReason = ''
|
|
1326
|
+
let groupRecent = ''
|
|
1327
|
+
let senderName = ''
|
|
1281
1328
|
if (sourceKind !== 'user') {
|
|
1329
|
+
senderName = await ensurePerson(userId)
|
|
1330
|
+
const bufferText = messageType === 'sticker' ? '(傳了一個 LINE 貼圖)' : clip(text, 300)
|
|
1282
1331
|
const gate = groupGate(event.message, cfg)
|
|
1283
1332
|
if (!gate.reply) {
|
|
1333
|
+
pushRecent(chatId, { userId, name: senderName || userId, text: bufferText, at: Date.now() })
|
|
1284
1334
|
logger.info(`群組訊息(${gate.reason}):這次不回`)
|
|
1285
1335
|
return
|
|
1286
1336
|
}
|
|
1337
|
+
groupRecent = groupRecentText(chatId)
|
|
1338
|
+
pushRecent(chatId, { userId, name: senderName || userId, text: bufferText, at: Date.now() })
|
|
1287
1339
|
groupReason = gate.reason
|
|
1288
1340
|
logger.info(`群組訊息:回覆(${gate.reason})`)
|
|
1289
1341
|
}
|
|
1290
1342
|
|
|
1291
1343
|
const conversationKey = sourceKind === 'user' ? userId : chatId
|
|
1344
|
+
const memoryKey = sourceKind === 'user' ? userId : `${chatId}:${userId}`
|
|
1292
1345
|
const user = sourceKind === 'user' ? ensureUser(userId) : ensureGroup(chatId, sourceKind)
|
|
1293
1346
|
const bucketName = sourceKind === 'user' ? 'users' : 'groups'
|
|
1294
1347
|
store.updateState((state) => {
|
|
@@ -1331,8 +1384,8 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
1331
1384
|
persona: applied.persona,
|
|
1332
1385
|
personaText: applied.personaText,
|
|
1333
1386
|
personaClient: persona(),
|
|
1334
|
-
contextText: agentContextText(
|
|
1335
|
-
tools: memoryToolsFor(
|
|
1387
|
+
contextText: agentContextText(memoryKey, sourceKind, groupRecent),
|
|
1388
|
+
tools: memoryToolsFor(memoryKey),
|
|
1336
1389
|
images: messageImages,
|
|
1337
1390
|
cwd: ensureUserWorkspace(conversationKey) || cfg.agent.cwd || undefined,
|
|
1338
1391
|
})
|
|
@@ -1855,21 +1908,36 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
1855
1908
|
case 'memory-list': {
|
|
1856
1909
|
const only = String(payload.userId || '').trim()
|
|
1857
1910
|
const state = store.getState()
|
|
1858
|
-
const mapEntry = (kind) => ([id, item]) =>
|
|
1911
|
+
const mapEntry = (kind) => ([id, item]) => {
|
|
1912
|
+
let displayName = item?.displayName || ''
|
|
1913
|
+
let rowKind = kind
|
|
1914
|
+
if (id.includes(':')) {
|
|
1915
|
+
const [groupId, memberId] = id.split(':')
|
|
1916
|
+
const group = state.groups?.[groupId]
|
|
1917
|
+
displayName = `${group?.displayName || groupId}/${personName(memberId) || memberId}`
|
|
1918
|
+
rowKind = 'group-member'
|
|
1919
|
+
}
|
|
1920
|
+
return {
|
|
1859
1921
|
userId: id,
|
|
1860
1922
|
id,
|
|
1861
|
-
kind,
|
|
1862
|
-
displayName
|
|
1923
|
+
kind: rowKind,
|
|
1924
|
+
displayName,
|
|
1863
1925
|
entries: memory.list(id).map((entry) => ({
|
|
1864
1926
|
...entry,
|
|
1865
1927
|
createdText: new Date(entry.createdAt).toISOString(),
|
|
1866
1928
|
expiresText: entry.expiresAt ? new Date(entry.expiresAt).toISOString() : '',
|
|
1867
1929
|
})),
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
const rows = memory.keys()
|
|
1933
|
+
.filter((id) => !only || id === only || id.startsWith(`${only}:`))
|
|
1934
|
+
.map((id) => {
|
|
1935
|
+
if (id.includes(':')) return mapEntry('group-member')([id, null])
|
|
1936
|
+
if (state.users?.[id]) return mapEntry('user')([id, state.users[id]])
|
|
1937
|
+
if (state.groups?.[id]) return mapEntry('group')([id, state.groups[id]])
|
|
1938
|
+
return mapEntry('unknown')([id, null])
|
|
1939
|
+
})
|
|
1940
|
+
.filter((row) => row.entries.length > 0)
|
|
1873
1941
|
return { users: rows, total: rows.reduce((sum, row) => sum + row.entries.length, 0) }
|
|
1874
1942
|
}
|
|
1875
1943
|
case 'memory-add': {
|
|
@@ -1989,12 +2057,13 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
1989
2057
|
logger.warn(`清空工作資料夾失敗:${errorText(error)}`)
|
|
1990
2058
|
}
|
|
1991
2059
|
}
|
|
2060
|
+
const cleared = memory.forgetPrefix(`${groupId}:`)
|
|
1992
2061
|
store.updateState((state) => {
|
|
1993
2062
|
const next = { ...state, groups: { ...state.groups } }
|
|
1994
2063
|
delete next.groups[groupId]
|
|
1995
2064
|
return next
|
|
1996
2065
|
})
|
|
1997
|
-
return { groupId, reset: true }
|
|
2066
|
+
return { groupId, reset: true, memory: cleared }
|
|
1998
2067
|
}
|
|
1999
2068
|
case 'user-reset': {
|
|
2000
2069
|
const userId = String(payload.userId || '')
|
package/lib/memory.mjs
CHANGED
|
@@ -205,5 +205,22 @@ export function createMemoryStore({ dir, logger, getConfig }) {
|
|
|
205
205
|
]
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
function keys() {
|
|
209
|
+
return Object.keys(store.users).filter((key) => (store.users[key]?.entries?.length || 0) > 0)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function forgetPrefix(prefix) {
|
|
213
|
+
const key = String(prefix || '')
|
|
214
|
+
if (!key) return { removed: 0, keys: [] }
|
|
215
|
+
const keys = Object.keys(store.users).filter((item) => item.startsWith(key))
|
|
216
|
+
let removed = 0
|
|
217
|
+
for (const item of keys) {
|
|
218
|
+
removed += store.users[item]?.entries?.length || 0
|
|
219
|
+
delete store.users[item]
|
|
220
|
+
}
|
|
221
|
+
if (keys.length > 0) save()
|
|
222
|
+
return { removed, keys }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { file, list, add, forget, prune, stats, formatForContext, toolsFor, forgetPrefix, keys }
|
|
209
226
|
}
|