dsh-line-bot 0.2.2 → 0.4.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 +18 -0
- package/README.en.md +23 -0
- package/README.ja.md +23 -0
- package/README.ko.md +23 -0
- package/README.md +23 -0
- package/README.zh-CN.md +23 -0
- package/bin/cli.mjs +53 -14
- package/client.js +231 -15
- package/lib/agent-runner.mjs +14 -3
- package/lib/config.mjs +21 -0
- package/lib/host.mjs +261 -12
- package/lib/memory.mjs +209 -0
- package/lib/tieba-sync.mjs +19 -11
- package/lib/toolkit.mjs +42 -0
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -13,11 +13,13 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
// 二級選單:卡片區塊的快速跳轉。
|
|
14
14
|
const NAV = [
|
|
15
15
|
{ key: 'overview', label: '狀態總覽' },
|
|
16
|
-
{ key: 'balance', label: 'DeepSeek 餘額' },
|
|
17
16
|
{ key: 'line', label: 'LINE 憑證' },
|
|
18
17
|
{ key: 'tunnel', label: 'Cloudflare' },
|
|
18
|
+
{ key: 'model', label: '模型與來源' },
|
|
19
19
|
{ key: 'persona', label: '回覆人設' },
|
|
20
20
|
{ key: 'sync', label: '貼吧同步' },
|
|
21
|
+
{ key: 'memory', label: '個人記憶' },
|
|
22
|
+
{ key: 'balance', label: 'DeepSeek 餘額' },
|
|
21
23
|
{ key: 'nudge', label: '主動傳訊' },
|
|
22
24
|
{ key: 'users', label: '使用者' },
|
|
23
25
|
{ key: 'advanced', label: '進階/日誌' },
|
|
@@ -186,10 +188,10 @@ window.__ModuleLoader__.load({
|
|
|
186
188
|
return h('button', { type: 'button', style, onClick: disabled ? undefined : onClick, disabled: !!disabled, title: title || '' }, children)
|
|
187
189
|
}
|
|
188
190
|
|
|
189
|
-
function Card({ id, title, desc, right, children }) {
|
|
191
|
+
function Card({ id, title, desc, right, children, order }) {
|
|
190
192
|
return h(
|
|
191
193
|
'section',
|
|
192
|
-
{ id, 'data-section': id || undefined, style: { ...S.card, scrollMarginTop: 104 } },
|
|
194
|
+
{ id, 'data-section': id || undefined, style: { ...S.card, scrollMarginTop: 104, ...(order ? { order } : {}) } },
|
|
193
195
|
h('div', { style: S.cardHead }, h('div', null, h('div', { style: S.cardTitle }, title), desc ? h('div', { style: S.cardDesc }, desc) : null), right || null),
|
|
194
196
|
h('div', { style: S.cardBody }, children),
|
|
195
197
|
)
|
|
@@ -250,6 +252,28 @@ window.__ModuleLoader__.load({
|
|
|
250
252
|
)
|
|
251
253
|
}
|
|
252
254
|
|
|
255
|
+
|
|
256
|
+
function UserChips({ users, value, onChange, emptyText }) {
|
|
257
|
+
const ids = Array.isArray(value) ? value : []
|
|
258
|
+
const list = Array.isArray(users) ? users : []
|
|
259
|
+
if (list.length === 0) {
|
|
260
|
+
return h('div', { style: S.note }, emptyText || '目前還沒有使用者跟 bot 聊過;有人傳訊息後就會出現在這裡。')
|
|
261
|
+
}
|
|
262
|
+
return h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6 } },
|
|
263
|
+
list.map((user) => {
|
|
264
|
+
const selected = ids.includes(user.userId)
|
|
265
|
+
const label = (user.displayName || '未命名') + '(' + String(user.userId).slice(-6) + ')'
|
|
266
|
+
return h('button', {
|
|
267
|
+
key: user.userId,
|
|
268
|
+
type: 'button',
|
|
269
|
+
onClick: () => onChange(selected ? ids.filter((id) => id !== user.userId) : [...ids, user.userId]),
|
|
270
|
+
title: user.userId,
|
|
271
|
+
style: { ...S.btn, padding: '4px 10px', borderRadius: 999, fontSize: 12, background: selected ? 'var(--dsw-alias-state-business-primary, #4b7bec)' : 'transparent', color: selected ? '#fff' : 'var(--dsw-alias-label-primary)', border: '1px solid ' + (selected ? 'transparent' : 'var(--dsw-alias-border-l2)') },
|
|
272
|
+
}, label)
|
|
273
|
+
}),
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
253
277
|
function Badge({ ok, children }) {
|
|
254
278
|
const color = ok === true ? S.ok.color : ok === false ? S.bad.color : S.warn.color
|
|
255
279
|
const dot = ok === true ? S.ok.color : ok === false ? S.bad.color : S.warn.color
|
|
@@ -292,7 +316,12 @@ window.__ModuleLoader__.load({
|
|
|
292
316
|
'persona-status': '重新讀取人設',
|
|
293
317
|
'persona-mode': '切換人設模式',
|
|
294
318
|
'tieba-preview': '預覽貼吧同步差異',
|
|
319
|
+
'model-catalog': '讀取模型清單',
|
|
295
320
|
'balance-refresh': '查詢 DeepSeek 餘額',
|
|
321
|
+
'memory-list': '讀取個人記憶',
|
|
322
|
+
'memory-delete': '刪除記憶',
|
|
323
|
+
'memory-prune': '清理到期記憶',
|
|
324
|
+
'memory-review': '請角色整理記憶',
|
|
296
325
|
'balance-ask': '餘額提醒',
|
|
297
326
|
'tieba-sync': '同步貼吧學習',
|
|
298
327
|
'local-persona-list': '讀取自訂人設',
|
|
@@ -317,6 +346,8 @@ window.__ModuleLoader__.load({
|
|
|
317
346
|
const [active, setActive] = useState('overview')
|
|
318
347
|
const [syncPreview, setSyncPreview] = useState(null)
|
|
319
348
|
const [balancePreview, setBalancePreview] = useState(null)
|
|
349
|
+
const [memoryData, setMemoryData] = useState(null)
|
|
350
|
+
const [modelCatalog, setModelCatalog] = useState(null)
|
|
320
351
|
const [localEdit, setLocalEdit] = useState({ id: '', name: '', text: '' })
|
|
321
352
|
|
|
322
353
|
const dirty = useMemo(() => {
|
|
@@ -436,6 +467,68 @@ window.__ModuleLoader__.load({
|
|
|
436
467
|
await act('balance-ask', { dryRun: false, force: true })
|
|
437
468
|
}
|
|
438
469
|
|
|
470
|
+
async function loadMemory({ quiet = false } = {}) {
|
|
471
|
+
if (!quiet) setBusy('memory-list')
|
|
472
|
+
try {
|
|
473
|
+
if (dirty && !quiet) await persist(false)
|
|
474
|
+
const value = await jpost('/action', { action: 'memory-list', payload: {} })
|
|
475
|
+
if (value?.status) {
|
|
476
|
+
setStatus(value.status)
|
|
477
|
+
setDraft(stripSecrets(value.status.config))
|
|
478
|
+
}
|
|
479
|
+
setMemoryData(value?.result || null)
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (!quiet) setToast({ kind: 'error', text: '讀取記憶失敗:' + (err.message || err) })
|
|
482
|
+
} finally {
|
|
483
|
+
if (!quiet) setBusy('')
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function removeMemory(userId, entry) {
|
|
488
|
+
setBusy('memory-delete')
|
|
489
|
+
try {
|
|
490
|
+
const value = await jpost('/action', { action: 'memory-delete', payload: { userId, id: entry.id } })
|
|
491
|
+
if (value?.status) {
|
|
492
|
+
setStatus(value.status)
|
|
493
|
+
setDraft(stripSecrets(value.status.config))
|
|
494
|
+
}
|
|
495
|
+
await loadMemory({ quiet: true })
|
|
496
|
+
setToast({ kind: 'ok', text: '已刪掉一筆記憶。' })
|
|
497
|
+
} catch (err) {
|
|
498
|
+
setToast({ kind: 'error', text: '刪除記憶失敗:' + (err.message || err) })
|
|
499
|
+
} finally {
|
|
500
|
+
setBusy('')
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function runMemory(action) {
|
|
505
|
+
await act(action)
|
|
506
|
+
await loadMemory({ quiet: true })
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function loadModelCatalog({ quiet = false, force = false } = {}) {
|
|
510
|
+
if (!quiet) setBusy('model-catalog')
|
|
511
|
+
try {
|
|
512
|
+
if (dirty && !quiet) await persist(false)
|
|
513
|
+
const value = await jpost('/action', { action: 'model-catalog', payload: force ? { force: true } : {} })
|
|
514
|
+
if (value?.status) {
|
|
515
|
+
setStatus(value.status)
|
|
516
|
+
setDraft(stripSecrets(value.status.config))
|
|
517
|
+
}
|
|
518
|
+
setModelCatalog(value?.result || null)
|
|
519
|
+
} catch (err) {
|
|
520
|
+
if (!quiet) setToast({ kind: 'error', text: '讀取模型清單失敗:' + (err.message || err) })
|
|
521
|
+
} finally {
|
|
522
|
+
if (!quiet) setBusy('')
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
useEffect(() => {
|
|
527
|
+
loadMemory({ quiet: true })
|
|
528
|
+
loadModelCatalog({ quiet: true })
|
|
529
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
530
|
+
}, [])
|
|
531
|
+
|
|
439
532
|
async function previewSync() {
|
|
440
533
|
setBusy('tieba-preview')
|
|
441
534
|
setToast(null)
|
|
@@ -566,6 +659,11 @@ window.__ModuleLoader__.load({
|
|
|
566
659
|
const balanceStatus = status.balance || {}
|
|
567
660
|
const balanceSymbol = balanceStatus.cny ? '¥' : balanceStatus.usd ? '$' : '¥'
|
|
568
661
|
const balanceTotal = balanceStatus.cny ? balanceStatus.cny.total : balanceStatus.usd ? balanceStatus.usd.total : null
|
|
662
|
+
const memoryCfg = cfg.memory || {}
|
|
663
|
+
const memoryStatus = status.memory || {}
|
|
664
|
+
const agentStatus = status.agent || {}
|
|
665
|
+
const modelProviders = Array.isArray(modelCatalog?.providers) ? modelCatalog.providers : []
|
|
666
|
+
const memoryUsers = memoryData?.users || []
|
|
569
667
|
const syncStats = status.sync || {}
|
|
570
668
|
const runtime = status
|
|
571
669
|
const persona = status.persona || {}
|
|
@@ -625,7 +723,7 @@ window.__ModuleLoader__.load({
|
|
|
625
723
|
// 1. 狀態總覽
|
|
626
724
|
h(
|
|
627
725
|
Card,
|
|
628
|
-
{ id: sectionDomId('overview'), title: '狀態總覽', desc: '綠燈=已就緒。第一次使用建議依序:填入 LINE 憑證 → 啟動 webhook → 啟動隧道。', right: h(Btn, { tone: 'primary', onClick: () => act('setup'), disabled: !!busy }, busy === 'setup' ? '上線中…' : '一鍵上線') },
|
|
726
|
+
{ id: sectionDomId('overview'), order: 1, title: '狀態總覽', desc: '綠燈=已就緒。第一次使用建議依序:填入 LINE 憑證 → 啟動 webhook → 啟動隧道。', right: h(Btn, { tone: 'primary', onClick: () => act('setup'), disabled: !!busy }, busy === 'setup' ? '上線中…' : '一鍵上線') },
|
|
629
727
|
h(
|
|
630
728
|
'div',
|
|
631
729
|
{ style: S.row },
|
|
@@ -652,11 +750,20 @@ window.__ModuleLoader__.load({
|
|
|
652
750
|
h(
|
|
653
751
|
Card,
|
|
654
752
|
{
|
|
655
|
-
id: sectionDomId('balance'),
|
|
753
|
+
id: sectionDomId('balance'), order: 8,
|
|
656
754
|
title: 'DeepSeek 餘額',
|
|
657
|
-
desc: '讓角色知道 API
|
|
658
|
-
right: h(Btn, { onClick: () => act('balance-refresh'), disabled: !!busy }, busy === 'balance-refresh' ? '查詢中…' : '重新查詢'),
|
|
755
|
+
desc: '讓角色知道 API 錢包剩多少;低於門檻時由角色自己想一句話提醒你儲值(會避開主動傳訊的靜音時段)。只有模型來源是 deepseek-account 時才適用。',
|
|
756
|
+
right: h(Btn, { onClick: () => act('balance-refresh'), disabled: !!busy || balanceStatus.applicable === false }, busy === 'balance-refresh' ? '查詢中…' : '重新查詢'),
|
|
659
757
|
},
|
|
758
|
+
balanceStatus.applicable === false
|
|
759
|
+
? h('div', { style: S.warnBox }, `目前模型來源是「${agentStatus.provider || '未設定'}」,不是 DeepSeek 官方帳號,所以餘額查詢與主動提醒已停用。把上面的「模型與接口來源」切回 deepseek-account 就會自動恢復。`)
|
|
760
|
+
: null,
|
|
761
|
+
h(
|
|
762
|
+
'fieldset',
|
|
763
|
+
{
|
|
764
|
+
disabled: balanceStatus.applicable === false,
|
|
765
|
+
style: { border: 0, margin: 0, padding: 0, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 12, ...(balanceStatus.applicable === false ? { opacity: 0.45 } : {}) },
|
|
766
|
+
},
|
|
660
767
|
h(
|
|
661
768
|
'div',
|
|
662
769
|
{ style: S.row },
|
|
@@ -684,6 +791,7 @@ window.__ModuleLoader__.load({
|
|
|
684
791
|
? ('查詢失敗:' + balanceStatus.error)
|
|
685
792
|
: '按「重新查詢」讀取目前的 DeepSeek API 餘額。',
|
|
686
793
|
),
|
|
794
|
+
h(Field, { label: '餘額提醒對象', hint: '留空=沿用主動傳訊的指定對象;只會提醒這裡選的人' }, h(UserChips, { users: status.users, value: balanceCfg.targetUserIds, onChange: (ids) => update({ balance: { targetUserIds: ids } }) })),
|
|
687
795
|
h(
|
|
688
796
|
'div',
|
|
689
797
|
{ style: S.grid },
|
|
@@ -703,12 +811,60 @@ window.__ModuleLoader__.load({
|
|
|
703
811
|
h(Btn, { tone: 'primary', onClick: askBalance, disabled: !!busy }, '立即提醒一次'),
|
|
704
812
|
),
|
|
705
813
|
balancePreview && balancePreview.text ? h('div', { style: S.panelBox }, h('div', { style: S.fieldTitle }, '角色會傳這則:'), h('pre', { style: S.pre }, balancePreview.text)) : null,
|
|
814
|
+
),
|
|
815
|
+
),
|
|
816
|
+
|
|
817
|
+
// 1.6 個人記憶
|
|
818
|
+
h(
|
|
819
|
+
Card,
|
|
820
|
+
{
|
|
821
|
+
id: sectionDomId('memory'), order: 7,
|
|
822
|
+
title: '個人記憶',
|
|
823
|
+
desc: '每位使用者分開。角色覺得重要的事會自己記下來;有期限的到期自動刪,每天也會請角色自己整理一次。',
|
|
824
|
+
right: h(Btn, { onClick: () => loadMemory(), disabled: !!busy }, busy === 'memory-list' ? '讀取中…' : '重新整理'),
|
|
825
|
+
},
|
|
826
|
+
h('div', { style: S.row },
|
|
827
|
+
h(Badge, { ok: memoryStatus.enabled !== false }, '共 ' + (memoryStatus.total || 0) + ' 筆'),
|
|
828
|
+
h(Badge, { ok: memoryCfg.reviewDaily !== false }, '每日整理:' + (memoryCfg.reviewDaily !== false ? (Number(memoryCfg.reviewHour) || 0) + ' 點' : '關閉')),
|
|
829
|
+
h(Badge, { ok: true }, '到期自動刪'),
|
|
830
|
+
),
|
|
831
|
+
h(KV, { rows: [
|
|
832
|
+
['上次清理', memoryStatus.lastPruneAt ? fmtTime(memoryStatus.lastPruneAt) : '還沒清過'],
|
|
833
|
+
['上次角色整理', memoryStatus.lastReviewAt ? fmtTime(memoryStatus.lastReviewAt) : '還沒整理過'],
|
|
834
|
+
['記憶檔', memoryStatus.file || ''],
|
|
835
|
+
] }),
|
|
836
|
+
h('div', { style: S.grid },
|
|
837
|
+
h(Field, { label: '每天整理時間(0–23 點)', hint: '由角色自己判斷哪幾筆該忘掉' }, h(Num, { value: memoryCfg.reviewHour, min: 0, max: 23, step: 1, onChange: (value) => update({ memory: { reviewHour: value } }) })),
|
|
838
|
+
h(Field, { label: '每輪最多帶入幾筆', hint: '其餘仍留在檔案裡' }, h(Num, { value: memoryCfg.injectMaxEntries, min: 1, max: 80, step: 1, onChange: (value) => update({ memory: { injectMaxEntries: value } }) })),
|
|
839
|
+
h(Field, { label: '每人上限(筆)', hint: '超過時刪最舊的' }, h(Num, { value: memoryCfg.maxPerUser, min: 10, max: 500, step: 10, onChange: (value) => update({ memory: { maxPerUser: value } }) })),
|
|
840
|
+
h(Field, { label: '帶入字數上限', hint: '避免佔用太多 prompt' }, h(Num, { value: memoryCfg.injectMaxChars, min: 200, max: 8000, step: 200, onChange: (value) => update({ memory: { injectMaxChars: value } }) })),
|
|
841
|
+
),
|
|
842
|
+
h('div', { style: S.row },
|
|
843
|
+
h(Toggle, { checked: memoryCfg.enabled !== false, onChange: (value) => update({ memory: { enabled: value } }), label: '開啟個人記憶', hint: '關掉後角色不再記新的事情,也不會讀到舊的' }),
|
|
844
|
+
h(Toggle, { checked: memoryCfg.reviewDaily !== false, onChange: (value) => update({ memory: { reviewDaily: value } }), label: '每天請角色整理一次', hint: '清掉過期或不再成立的記憶' }),
|
|
845
|
+
),
|
|
846
|
+
h('div', { style: S.row },
|
|
847
|
+
h(Btn, { onClick: () => runMemory('memory-prune'), disabled: !!busy }, busy === 'memory-prune' ? '清理中…' : '清理到期記憶'),
|
|
848
|
+
h(Btn, { onClick: () => runMemory('memory-review'), disabled: !!busy }, busy === 'memory-review' ? '整理中…' : '現在請角色整理'),
|
|
849
|
+
),
|
|
850
|
+
memoryData
|
|
851
|
+
? (memoryUsers.length === 0
|
|
852
|
+
? h('div', { style: S.note }, '目前沒有任何記憶。角色在對話中覺得重要時,會自己記下來。')
|
|
853
|
+
: memoryUsers.map((row) => h('div', { key: row.userId, style: S.panelBox },
|
|
854
|
+
h('div', { style: S.fieldTitle }, (row.displayName || row.userId) + '(' + row.entries.length + ' 筆)'),
|
|
855
|
+
row.entries.map((entry) => h('div', { key: entry.id, style: { display: 'flex', gap: 8, alignItems: 'flex-start', padding: '6px 0', borderTop: '1px solid var(--dsw-alias-border-l2)' } },
|
|
856
|
+
h('span', { style: { flex: '0 0 auto', fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', paddingTop: 2 } }, ({ fact: '事實', event: '事件', preference: '喜好', inside: '默契' })[entry.kind] || entry.kind),
|
|
857
|
+
h('span', { style: { flex: 1, fontSize: 13, lineHeight: 1.5 } }, entry.text, entry.expiresText ? h('span', { style: { color: 'var(--dsw-alias-label-tertiary)' } }, '(到 ' + String(entry.expiresText).slice(0, 10) + ')') : null),
|
|
858
|
+
h(Btn, { tone: 'danger', onClick: () => removeMemory(row.userId, entry), disabled: !!busy }, '刪除'),
|
|
859
|
+
)),
|
|
860
|
+
)))
|
|
861
|
+
: h('div', { style: S.note }, '按「重新整理」讀取目前記住的內容。'),
|
|
706
862
|
),
|
|
707
863
|
|
|
708
864
|
// 2. LINE 憑證
|
|
709
865
|
h(
|
|
710
866
|
Card,
|
|
711
|
-
{ id: sectionDomId('line'), title: 'LINE 憑證', desc: '在 LINE Developers Console 的 Messaging API channel 取得。貼上後可按「驗證」確認。' },
|
|
867
|
+
{ id: sectionDomId('line'), order: 2, title: 'LINE 憑證', desc: '在 LINE Developers Console 的 Messaging API channel 取得。貼上後可按「驗證」確認。' },
|
|
712
868
|
h(
|
|
713
869
|
'div',
|
|
714
870
|
{ style: S.note },
|
|
@@ -754,7 +910,7 @@ window.__ModuleLoader__.load({
|
|
|
754
910
|
// 3. Cloudflare Tunnel
|
|
755
911
|
h(
|
|
756
912
|
Card,
|
|
757
|
-
{ id: sectionDomId('tunnel'), title: 'Cloudflare Tunnel', desc: '把 127.0.0.1 的 webhook 公開到 https,LINE 才收得到。Quick tunnel 免 token、網址每次不同;API Token 模式可固定網域。' },
|
|
913
|
+
{ id: sectionDomId('tunnel'), order: 3, title: 'Cloudflare Tunnel', desc: '把 127.0.0.1 的 webhook 公開到 https,LINE 才收得到。Quick tunnel 免 token、網址每次不同;API Token 模式可固定網域。' },
|
|
758
914
|
h('div', { style: S.grid },
|
|
759
915
|
h(Field, { label: '模式' },
|
|
760
916
|
h('select', {
|
|
@@ -792,10 +948,70 @@ window.__ModuleLoader__.load({
|
|
|
792
948
|
h('div', { style: S.hint }, 'Quick tunnel 每次重啟網址都會變;若 LINE 的 webhook 沒自動更新,按上面「把隧道網址套到 webhook」。API Token 模式需要先在 Cloudflare 建立 API Token(權限:Account → Cloudflare Tunnel → Edit;Zone → DNS → Edit)。若驗證後面板沒自動帶入帳號/Zone,代表 token 少了 Account Settings:Read/Zone:Read,請手動填 Account ID 與 Zone ID(或幫 token 補上讀取權限)。'),
|
|
793
949
|
),
|
|
794
950
|
|
|
951
|
+
// 3.5 模型與接口來源
|
|
952
|
+
h(
|
|
953
|
+
Card,
|
|
954
|
+
{
|
|
955
|
+
id: sectionDomId('model'),
|
|
956
|
+
order: 4,
|
|
957
|
+
title: '模型與接口來源',
|
|
958
|
+
desc: 'LINE 回覆要用的 provider/模型。留空=跟隨 DSH 目前的預設;選非 DeepSeek 的來源時,下面的餘額功能會自動停用。',
|
|
959
|
+
right: h(Btn, { onClick: () => loadModelCatalog({ force: true }), disabled: !!busy }, busy === 'model-catalog' ? '讀取中…' : '重新讀取清單'),
|
|
960
|
+
},
|
|
961
|
+
h(KV, {
|
|
962
|
+
rows: [
|
|
963
|
+
['目前使用', `${agentStatus.provider || '—'} / ${agentStatus.model || '—'}${agentStatus.source === 'custom' ? '(面板指定)' : '(跟隨 DSH 預設)'}`],
|
|
964
|
+
['DSH 預設', `${agentStatus.defaultProvider || '—'} / ${agentStatus.defaultModel || '—'}`],
|
|
965
|
+
['餘額功能', agentStatus.provider === 'deepseek-account' ? '可用(DeepSeek 官方帳號)' : '已停用(不是 deepseek-account)'],
|
|
966
|
+
],
|
|
967
|
+
}),
|
|
968
|
+
h('div', { style: S.grid },
|
|
969
|
+
h(Field, { label: '模型接口來源(provider)', hint: '清單來自這個 DSH profile 已註冊的 provider' },
|
|
970
|
+
h('select', {
|
|
971
|
+
style: S.input,
|
|
972
|
+
value: draft.agent.provider || '',
|
|
973
|
+
onChange: (event) => {
|
|
974
|
+
const provider = event.target.value
|
|
975
|
+
const entry = modelProviders.find((item) => item.id === provider)
|
|
976
|
+
const first = entry?.models?.[0]?.id || ''
|
|
977
|
+
update({ agent: { provider, model: provider ? first : '' } })
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
h('option', { value: '' }, `跟隨 DSH 預設${agentStatus.defaultProvider ? `(${agentStatus.defaultProvider} / ${agentStatus.defaultModel})` : ''}`),
|
|
981
|
+
modelProviders.map((item) => h('option', { key: item.id, value: item.id }, `${item.name}(${item.id})${item.models?.length ? '' : ' · 無模型資料'}`)),
|
|
982
|
+
draft.agent.provider && !modelProviders.some((item) => item.id === draft.agent.provider)
|
|
983
|
+
? h('option', { value: draft.agent.provider }, draft.agent.provider)
|
|
984
|
+
: null,
|
|
985
|
+
)),
|
|
986
|
+
h(Field, { label: '模型', hint: '可直接輸入模型 id,或從清單選' },
|
|
987
|
+
h('input', {
|
|
988
|
+
style: S.input,
|
|
989
|
+
list: 'line-bot-model-options',
|
|
990
|
+
value: draft.agent.model || '',
|
|
991
|
+
placeholder: '留空=跟隨預設;例:deepseek-flash',
|
|
992
|
+
onChange: (event) => update({ agent: { model: event.target.value } }),
|
|
993
|
+
}),
|
|
994
|
+
h('datalist', { id: 'line-bot-model-options' },
|
|
995
|
+
((modelProviders.find((item) => item.id === (draft.agent.provider || agentStatus.defaultProvider)) || {}).models || []).map((item) => h('option', { key: item.id, value: item.id }, item.name))),
|
|
996
|
+
),
|
|
997
|
+
h(Field, { label: '推理強度(選填)', hint: '留空=跟隨 provider 預設' },
|
|
998
|
+
h(Text, { value: draft.agent.reasoningEffort || '', onChange: (value) => update({ agent: { reasoningEffort: value } }), placeholder: 'max / high / medium' })),
|
|
999
|
+
),
|
|
1000
|
+
h('div', { style: S.note }, modelCatalog?.error
|
|
1001
|
+
? `讀取模型清單失敗:${modelCatalog.error}`
|
|
1002
|
+
: modelCatalog
|
|
1003
|
+
? `共 ${modelProviders.length} 個來源。清單只列已註冊的 provider;實際能不能用仍取決於該來源的憑證。`
|
|
1004
|
+
: '正在讀取模型清單…'),
|
|
1005
|
+
h('div', { style: S.row },
|
|
1006
|
+
h(Btn, { onClick: () => loadModelCatalog({ force: true }), disabled: !!busy }, busy === 'model-catalog' ? '讀取中…' : '重新讀取模型清單'),
|
|
1007
|
+
h(Btn, { onClick: () => act('agent-test', { text: '請用一句話確認你現在用的模型來源。' }), disabled: !!busy }, busy === 'agent-test' ? '測試中…' : '測試 Agent 回覆'),
|
|
1008
|
+
),
|
|
1009
|
+
),
|
|
1010
|
+
|
|
795
1011
|
// 4. 人設
|
|
796
1012
|
h(
|
|
797
1013
|
Card,
|
|
798
|
-
{ id: sectionDomId('persona'), title: '回覆人設', desc: 'LINE 的回覆會套用這裡選的人設。來源有兩種:大肥魚插件(有安裝時)、插件自己的自訂人設(不依賴任何外掛)。' },
|
|
1014
|
+
{ id: sectionDomId('persona'), order: 5, title: '回覆人設', desc: 'LINE 的回覆會套用這裡選的人設。來源有兩種:大肥魚插件(有安裝時)、插件自己的自訂人設(不依賴任何外掛)。' },
|
|
799
1015
|
persona.warning ? h('div', { style: S.warnBox }, persona.warning) : null,
|
|
800
1016
|
persona.error ? h('div', { style: S.warnBox }, `讀不到大肥魚插件:${persona.error}`) : null,
|
|
801
1017
|
h('div', { style: S.grid },
|
|
@@ -856,7 +1072,7 @@ window.__ModuleLoader__.load({
|
|
|
856
1072
|
// 4b. 貼吧學習同步
|
|
857
1073
|
h(
|
|
858
1074
|
Card,
|
|
859
|
-
{ id: sectionDomId('sync'), title: '貼吧學習同步', desc: '把「刷貼吧」每天演化學到的語氣樣本,寫回上方人設的「## 貼吧學來的口氣」段落,並重載大肥魚插件讓它立即生效。' },
|
|
1075
|
+
{ id: sectionDomId('sync'), order: 6, title: '貼吧學習同步', desc: '把「刷貼吧」每天演化學到的語氣樣本,寫回上方人設的「## 貼吧學來的口氣」段落,並重載大肥魚插件讓它立即生效。' },
|
|
860
1076
|
h(Toggle, { checked: syncCfg.enabled !== false, onChange: (value) => update({ sync: { enabled: value } }), label: '開啟自動同步', hint: '每小時檢查一次;貼吧人設版本有變才寫入(寫入前會備份 personas.json)' }),
|
|
861
1077
|
h('div', { style: S.grid },
|
|
862
1078
|
h(Field, { label: '同步到哪個人設' },
|
|
@@ -886,7 +1102,7 @@ window.__ModuleLoader__.load({
|
|
|
886
1102
|
// 5. 主動傳訊
|
|
887
1103
|
h(
|
|
888
1104
|
Card,
|
|
889
|
-
{ id: sectionDomId('nudge'), title: '主動傳訊(大肥魚自己找人聊天)', desc: '由插件內建排程器執行:每隔幾分鐘擲骰子,符合條件就請 Agent 主動傳一句;不適合時 Agent 可回 SKIP。' },
|
|
1105
|
+
{ id: sectionDomId('nudge'), order: 9, title: '主動傳訊(大肥魚自己找人聊天)', desc: '由插件內建排程器執行:每隔幾分鐘擲骰子,符合條件就請 Agent 主動傳一句;不適合時 Agent 可回 SKIP。' },
|
|
890
1106
|
h(Toggle, { checked: !!nudgeCfg.enabled, onChange: (value) => update({ nudge: { enabled: value } }), label: '開啟主動傳訊' }),
|
|
891
1107
|
h('div', { style: S.grid },
|
|
892
1108
|
h(Field, { label: '每次擲骰機率(0~1)', hint: '例:0.18 = 每輪 18% 機率開口' }, h(Num, { value: nudgeCfg.chance, min: 0, max: 1, step: 0.01, onChange: (value) => update({ nudge: { chance: value } }) })),
|
|
@@ -898,7 +1114,7 @@ window.__ModuleLoader__.load({
|
|
|
898
1114
|
h(Field, { label: '安靜開始(HH:MM)' }, h(Text, { value: nudgeCfg.quietStart, onChange: (value) => update({ nudge: { quietStart: value } }), placeholder: '00:30' })),
|
|
899
1115
|
h(Field, { label: '安靜結束(HH:MM)' }, h(Text, { value: nudgeCfg.quietEnd, onChange: (value) => update({ nudge: { quietEnd: value } }), placeholder: '09:30' })),
|
|
900
1116
|
),
|
|
901
|
-
h(Field, { label: '
|
|
1117
|
+
h(Field, { label: '指定對象', hint: '留空=所有已聊過的使用者中挑最久沒互動的;直接點選要傳給誰' }, h(UserChips, { users: status.users, value: nudgeCfg.targetUserIds, onChange: (ids) => update({ nudge: { targetUserIds: ids } }) })),
|
|
902
1118
|
h(Field, { label: '主動傳訊提示詞(給 Agent 的指示)', hint: '留空=用內建預設(自然、簡短、不提系統、不重複、不適合就 SKIP)' }, h(Area, { value: nudgeCfg.prompt, onChange: (value) => update({ nudge: { prompt: value } }), rows: 4 })),
|
|
903
1119
|
h('div', { style: S.row },
|
|
904
1120
|
h(Btn, { onClick: () => act('nudge-test', { force: true }), disabled: !!busy, tone: 'primary' }, busy === 'nudge-test' ? '試跑中…' : '立即試傳一則'),
|
|
@@ -919,7 +1135,7 @@ window.__ModuleLoader__.load({
|
|
|
919
1135
|
// 6. 使用者
|
|
920
1136
|
h(
|
|
921
1137
|
Card,
|
|
922
|
-
{ id: sectionDomId('users'), title: '使用者', desc: '與 bot 互動過的 LINE 使用者。每個人有自己的 DSH session 與工作資料夾(記憶、檔案不互通),人設也可以個別指定。' },
|
|
1138
|
+
{ id: sectionDomId('users'), order: 10, title: '使用者', desc: '與 bot 互動過的 LINE 使用者。每個人有自己的 DSH session 與工作資料夾(記憶、檔案不互通),人設也可以個別指定。' },
|
|
923
1139
|
(status.users || []).length === 0
|
|
924
1140
|
? h('div', { style: S.hint }, '還沒有人傳訊息給 bot。')
|
|
925
1141
|
: h('table', { style: S.table },
|
|
@@ -955,7 +1171,7 @@ window.__ModuleLoader__.load({
|
|
|
955
1171
|
// 7. 進階設定
|
|
956
1172
|
h(
|
|
957
1173
|
Card,
|
|
958
|
-
{ id: sectionDomId('advanced'), title: '進階設定', desc: '通常不用改;出問題時可從這裡調整。' },
|
|
1174
|
+
{ id: sectionDomId('advanced'), order: 11, title: '進階設定', desc: '通常不用改;出問題時可從這裡調整。' },
|
|
959
1175
|
h('div', { style: S.grid },
|
|
960
1176
|
h(Field, { label: 'Webhook 連接埠', hint: '改完按儲存;伺服器會自動重啟。' }, h(Num, { value: server.port, min: 1024, max: 65535, onChange: (value) => update({ server: { port: value } }) })),
|
|
961
1177
|
h(Field, { label: '每位使用者獨立工作資料夾', hint: '開啟=每個 LINE 使用者有自己的 cwd(檔案、AGENTS.md、工作區記憶互不相通)' },
|
package/lib/agent-runner.mjs
CHANGED
|
@@ -80,7 +80,7 @@ export function createAgentRunner({ ctx, logger, getConfig }) {
|
|
|
80
80
|
|
|
81
81
|
// 把「插件自己的人設」與「貼吧動態」掛進這個 agent 的 system prompt。
|
|
82
82
|
// 自訂人設只有選了才注入;大肥魚的人設走它自己的 session 指定。
|
|
83
|
-
function agentSetup({ personaText, contextText }) {
|
|
83
|
+
function agentSetup({ personaText, contextText, tools = [] }) {
|
|
84
84
|
return (agentCtx) => {
|
|
85
85
|
try {
|
|
86
86
|
const prompt = agentCtx?.systemPrompt
|
|
@@ -120,6 +120,17 @@ export function createAgentRunner({ ctx, logger, getConfig }) {
|
|
|
120
120
|
} catch (error) {
|
|
121
121
|
logger?.warn?.(`注入人設/貼吧動態失敗:${error?.message || error}`)
|
|
122
122
|
}
|
|
123
|
+
if (Array.isArray(tools) && tools.length > 0) {
|
|
124
|
+
try {
|
|
125
|
+
const registry = agentCtx.get?.('tools') || agentCtx.tools
|
|
126
|
+
if (!registry?.register) throw new Error('這個 profile 沒有 tools 服務')
|
|
127
|
+
for (const spec of tools) {
|
|
128
|
+
agentCtx.effect(() => registry.register(spec), `line-bot: tool ${spec.name}`)
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logger?.warn?.(`掛載記憶工具失敗:${error?.message || error}`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
123
134
|
}
|
|
124
135
|
}
|
|
125
136
|
|
|
@@ -161,7 +172,7 @@ export function createAgentRunner({ ctx, logger, getConfig }) {
|
|
|
161
172
|
}
|
|
162
173
|
}
|
|
163
174
|
|
|
164
|
-
async function turn({ sessionId, text, persona, personaClient, personaText, contextText, cwd: cwdOverride }) {
|
|
175
|
+
async function turn({ sessionId, text, persona, personaClient, personaText, contextText, tools = [], cwd: cwdOverride }) {
|
|
165
176
|
const cfg = getConfig()
|
|
166
177
|
const timeoutMs = cfg.agent.timeoutMs || 180000
|
|
167
178
|
const { options } = agentOptions()
|
|
@@ -175,7 +186,7 @@ export function createAgentRunner({ ctx, logger, getConfig }) {
|
|
|
175
186
|
let offEnd = null
|
|
176
187
|
|
|
177
188
|
try {
|
|
178
|
-
const setup = personaText || contextText ? agentSetup({ personaText, contextText }) : undefined
|
|
189
|
+
const setup = personaText || contextText || tools.length > 0 ? agentSetup({ personaText, contextText, tools }) : undefined
|
|
179
190
|
const opened = await openAgent(sessionId, options, cwd, setup)
|
|
180
191
|
agent = opened.agent
|
|
181
192
|
handle = opened.handle
|
package/lib/config.mjs
CHANGED
|
@@ -99,6 +99,17 @@ export const DEFAULT_CONFIG = {
|
|
|
99
99
|
threshold: 10,
|
|
100
100
|
ask: true,
|
|
101
101
|
cooldownHours: 12,
|
|
102
|
+
targetUserIds: [],
|
|
103
|
+
},
|
|
104
|
+
memory: {
|
|
105
|
+
// 每位使用者各自的個人記憶:agent 覺得重要就自己記,有期限的到期自動刪。
|
|
106
|
+
enabled: true,
|
|
107
|
+
maxPerUser: 120,
|
|
108
|
+
injectMaxEntries: 20,
|
|
109
|
+
injectMaxChars: 2000,
|
|
110
|
+
// 背景每天檢查一次,請角色自己把過期/失效的記憶刪掉。
|
|
111
|
+
reviewDaily: true,
|
|
112
|
+
reviewHour: 4,
|
|
102
113
|
},
|
|
103
114
|
log: {
|
|
104
115
|
maxLines: 400,
|
|
@@ -211,6 +222,15 @@ export function sanitizePatch(current, patch) {
|
|
|
211
222
|
balance.ask = Boolean(balance.ask)
|
|
212
223
|
balance.threshold = clampFloat(balance.threshold, 0, 100000, 10)
|
|
213
224
|
balance.cooldownHours = clampInt(balance.cooldownHours, 1, 168, 12)
|
|
225
|
+
balance.targetUserIds = safeArray(balance.targetUserIds)
|
|
226
|
+
|
|
227
|
+
const memory = next.memory || (next.memory = {})
|
|
228
|
+
memory.enabled = Boolean(memory.enabled)
|
|
229
|
+
memory.reviewDaily = Boolean(memory.reviewDaily)
|
|
230
|
+
memory.maxPerUser = clampInt(memory.maxPerUser, 10, 500, 120)
|
|
231
|
+
memory.injectMaxEntries = clampInt(memory.injectMaxEntries, 1, 80, 20)
|
|
232
|
+
memory.injectMaxChars = clampInt(memory.injectMaxChars, 200, 8000, 2000)
|
|
233
|
+
memory.reviewHour = clampInt(memory.reviewHour, 0, 23, 4)
|
|
214
234
|
|
|
215
235
|
const nudge = next.nudge
|
|
216
236
|
nudge.enabled = Boolean(nudge.enabled)
|
|
@@ -291,6 +311,7 @@ export function normalizeState(raw) {
|
|
|
291
311
|
users: isPlainObject(state.users) ? state.users : {},
|
|
292
312
|
nudge: isPlainObject(state.nudge) ? state.nudge : {},
|
|
293
313
|
balance: isPlainObject(state.balance) ? state.balance : {},
|
|
314
|
+
memory: isPlainObject(state.memory) ? state.memory : {},
|
|
294
315
|
sync: isPlainObject(state.sync) ? state.sync : {},
|
|
295
316
|
webhook: isPlainObject(state.webhook) ? state.webhook : null,
|
|
296
317
|
lastEventAt: Number(state.lastEventAt) || 0,
|