dsh-line-bot 0.6.0 → 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 +11 -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 +100 -16
- package/lib/memory.mjs +18 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
- 群組內每位成員有自己的記憶點(同一個人到不同群組也分開),回覆時會注入該成員的記憶。
|
|
6
|
+
- 群組回覆會參考最近 4~6 則別人的訊息;沒被點名而擲骰不回時,訊息仍會留給下一輪參考。
|
|
7
|
+
- 群組「清空記憶」會同時清掉該群組所有成員的記憶。
|
|
8
|
+
|
|
9
|
+
## 0.6.1
|
|
10
|
+
|
|
11
|
+
- 修正模型用 `<meme#18>` 這種角度括號標記時,圖片沒送出、反而把標記當文字顯示的問題;現在支援 `<meme#N>`、`[[meme:#N]]`、`[[meme:N]]` 與 `[表情: …]`,並會清掉殘留的括號。
|
|
12
|
+
- 群組對話不再注入貼吧作息提示(一對一聊天與主動傳訊仍保留)。
|
|
13
|
+
|
|
3
14
|
## 0.6.0
|
|
4
15
|
|
|
5
16
|
- 支援群組與多人聊天室:被 @ 或引用(回覆某則訊息)一定回,其餘訊息依「群組回覆機率」擲骰(預設 15%)。
|
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,8 +317,10 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
function agentContextText(
|
|
321
|
-
|
|
320
|
+
function agentContextText(ownerKey, kind = 'user', extra = '') {
|
|
321
|
+
// 群組不注入貼吧作息,避免在群組裡突然講起刷貼吧的事。
|
|
322
|
+
const tieba = kind === 'user' ? tiebaContextText() : ''
|
|
323
|
+
return [modelContextText(), tieba, balanceContextText(), extra, memoryContextText(ownerKey), memeContextText() || stickerContextText()].filter(Boolean).join('\n\n')
|
|
322
324
|
}
|
|
323
325
|
|
|
324
326
|
async function refreshBalance() {
|
|
@@ -833,6 +835,51 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
833
835
|
}
|
|
834
836
|
}
|
|
835
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
|
+
|
|
836
883
|
function isAllowed(userId, cfg) {
|
|
837
884
|
const list = cfg.line.allowedUserIds || []
|
|
838
885
|
return list.length === 0 || list.includes(userId)
|
|
@@ -933,7 +980,9 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
933
980
|
|
|
934
981
|
const STICKER_MARKER = /\[\[\s*(?:sticker|貼圖|贴图|表情包)\s*:\s*([^\]\n]{1,60})\s*\]\]/gi
|
|
935
982
|
const MEME_MARKER = /\[\[\s*meme\s*:\s*([^\]\n]{1,80})\s*\]\]/gi
|
|
936
|
-
const
|
|
983
|
+
const MEME_HASH = /\[\[\s*meme\s*#\s*(\d{1,3})\s*\]\]/gi
|
|
984
|
+
const MEME_ANGLE = /<\s*meme\s*#\s*(\d{1,3})\s*>/gi
|
|
985
|
+
const MEME_BRACKET = /\[表情\s*[::#]\s*([^\]\n]{1,80})\]/g
|
|
937
986
|
|
|
938
987
|
function splitStickerMarkers(text) {
|
|
939
988
|
const names = []
|
|
@@ -944,6 +993,14 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
944
993
|
if (value) names.push(value)
|
|
945
994
|
return ''
|
|
946
995
|
})
|
|
996
|
+
.replace(MEME_ANGLE, (_match, index) => {
|
|
997
|
+
memeNames.push(`#${index}`)
|
|
998
|
+
return ''
|
|
999
|
+
})
|
|
1000
|
+
.replace(MEME_HASH, (_match, index) => {
|
|
1001
|
+
memeNames.push(`#${index}`)
|
|
1002
|
+
return ''
|
|
1003
|
+
})
|
|
947
1004
|
.replace(MEME_MARKER, (_match, name) => {
|
|
948
1005
|
const value = String(name || '').trim()
|
|
949
1006
|
if (value) memeNames.push(value)
|
|
@@ -957,6 +1014,8 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
957
1014
|
})
|
|
958
1015
|
}
|
|
959
1016
|
cleaned = cleaned
|
|
1017
|
+
.replace(/^\s*[<>]\s*$/gm, '')
|
|
1018
|
+
.replace(/[ \t]*[<>][ \t]*$/gm, '')
|
|
960
1019
|
.replace(/[ \t]+\n/g, '\n')
|
|
961
1020
|
.replace(/\n{3,}/g, '\n\n')
|
|
962
1021
|
.trim()
|
|
@@ -973,6 +1032,7 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
973
1032
|
if (!memes.available()) return ''
|
|
974
1033
|
if (memes.packs().length === 0) return ''
|
|
975
1034
|
const sample = memes.sample({ limit: 20 })
|
|
1035
|
+
runtime.memeChoices = sample
|
|
976
1036
|
const lines = (sample || [])
|
|
977
1037
|
.map((item) => String(item.caption || '').trim())
|
|
978
1038
|
.filter(Boolean)
|
|
@@ -1144,7 +1204,7 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
1144
1204
|
const base = parsed.memes.length > 0 || parsed.names.length > 0 ? await publicBaseReady() : ''
|
|
1145
1205
|
const imageUrls = []
|
|
1146
1206
|
if (cfg.stickers?.enabled !== false && base && parsed.memes.length > 0 && memes.available()) {
|
|
1147
|
-
const sample = memes.sample({ limit: 20 })
|
|
1207
|
+
const sample = runtime.memeChoices?.length ? runtime.memeChoices : memes.sample({ limit: 20 })
|
|
1148
1208
|
for (const desc of parsed.memes.slice(0, max)) {
|
|
1149
1209
|
const indexMatch = /^#?(\d{1,3})$/.exec(String(desc).trim())
|
|
1150
1210
|
const index = indexMatch ? Number(indexMatch[1]) - 1 : -1
|
|
@@ -1263,17 +1323,25 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
1263
1323
|
if (!text) return
|
|
1264
1324
|
|
|
1265
1325
|
let groupReason = ''
|
|
1326
|
+
let groupRecent = ''
|
|
1327
|
+
let senderName = ''
|
|
1266
1328
|
if (sourceKind !== 'user') {
|
|
1329
|
+
senderName = await ensurePerson(userId)
|
|
1330
|
+
const bufferText = messageType === 'sticker' ? '(傳了一個 LINE 貼圖)' : clip(text, 300)
|
|
1267
1331
|
const gate = groupGate(event.message, cfg)
|
|
1268
1332
|
if (!gate.reply) {
|
|
1333
|
+
pushRecent(chatId, { userId, name: senderName || userId, text: bufferText, at: Date.now() })
|
|
1269
1334
|
logger.info(`群組訊息(${gate.reason}):這次不回`)
|
|
1270
1335
|
return
|
|
1271
1336
|
}
|
|
1337
|
+
groupRecent = groupRecentText(chatId)
|
|
1338
|
+
pushRecent(chatId, { userId, name: senderName || userId, text: bufferText, at: Date.now() })
|
|
1272
1339
|
groupReason = gate.reason
|
|
1273
1340
|
logger.info(`群組訊息:回覆(${gate.reason})`)
|
|
1274
1341
|
}
|
|
1275
1342
|
|
|
1276
1343
|
const conversationKey = sourceKind === 'user' ? userId : chatId
|
|
1344
|
+
const memoryKey = sourceKind === 'user' ? userId : `${chatId}:${userId}`
|
|
1277
1345
|
const user = sourceKind === 'user' ? ensureUser(userId) : ensureGroup(chatId, sourceKind)
|
|
1278
1346
|
const bucketName = sourceKind === 'user' ? 'users' : 'groups'
|
|
1279
1347
|
store.updateState((state) => {
|
|
@@ -1316,8 +1384,8 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
1316
1384
|
persona: applied.persona,
|
|
1317
1385
|
personaText: applied.personaText,
|
|
1318
1386
|
personaClient: persona(),
|
|
1319
|
-
contextText: agentContextText(
|
|
1320
|
-
tools: memoryToolsFor(
|
|
1387
|
+
contextText: agentContextText(memoryKey, sourceKind, groupRecent),
|
|
1388
|
+
tools: memoryToolsFor(memoryKey),
|
|
1321
1389
|
images: messageImages,
|
|
1322
1390
|
cwd: ensureUserWorkspace(conversationKey) || cfg.agent.cwd || undefined,
|
|
1323
1391
|
})
|
|
@@ -1840,21 +1908,36 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
1840
1908
|
case 'memory-list': {
|
|
1841
1909
|
const only = String(payload.userId || '').trim()
|
|
1842
1910
|
const state = store.getState()
|
|
1843
|
-
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 {
|
|
1844
1921
|
userId: id,
|
|
1845
1922
|
id,
|
|
1846
|
-
kind,
|
|
1847
|
-
displayName
|
|
1923
|
+
kind: rowKind,
|
|
1924
|
+
displayName,
|
|
1848
1925
|
entries: memory.list(id).map((entry) => ({
|
|
1849
1926
|
...entry,
|
|
1850
1927
|
createdText: new Date(entry.createdAt).toISOString(),
|
|
1851
1928
|
expiresText: entry.expiresAt ? new Date(entry.expiresAt).toISOString() : '',
|
|
1852
1929
|
})),
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
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)
|
|
1858
1941
|
return { users: rows, total: rows.reduce((sum, row) => sum + row.entries.length, 0) }
|
|
1859
1942
|
}
|
|
1860
1943
|
case 'memory-add': {
|
|
@@ -1974,12 +2057,13 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
1974
2057
|
logger.warn(`清空工作資料夾失敗:${errorText(error)}`)
|
|
1975
2058
|
}
|
|
1976
2059
|
}
|
|
2060
|
+
const cleared = memory.forgetPrefix(`${groupId}:`)
|
|
1977
2061
|
store.updateState((state) => {
|
|
1978
2062
|
const next = { ...state, groups: { ...state.groups } }
|
|
1979
2063
|
delete next.groups[groupId]
|
|
1980
2064
|
return next
|
|
1981
2065
|
})
|
|
1982
|
-
return { groupId, reset: true }
|
|
2066
|
+
return { groupId, reset: true, memory: cleared }
|
|
1983
2067
|
}
|
|
1984
2068
|
case 'user-reset': {
|
|
1985
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
|
}
|