dsh-line-bot 0.9.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.1
4
+ - 貼吧學習同步的目標下拉現在也包含「插件自訂人設」(`local:`);選自訂人設時會寫入 `<DSH_HOME>/line-bot/persona-overlays/<id>.md` 覆蓋層,每輪注入。
5
+ - 讀不到大肥魚 HTTP bridge 時,改成直接讀取 `<DSH_HOME>/storages/dsh-persona-dafeiyu/personas.json` 並列出內建人設,不再讓同步目標變空。
6
+ - 修正本機自訂人設同步完成後回報的模式(應為 overlay)。
7
+ - 貼吧梗只從梗類貼吧匯入(一般回覆碎片不再被當成梗);內建種子擴充到 24 則。
8
+ - 明確要求角色在主人說「不要圖」時不要加表情包標記。
9
+
3
10
  ## 0.9.0
4
11
  - 玩梗:新增文字梗庫(內建種子、貼吧高讚金句、wanjiqi-meme 彈幕梗),面板可個別開關來源、調整每輪注入數量、新增/刪除梗;每輪只餵幾條給角色,並限制一次回覆最多用一個。
5
12
  - 貼吧學習可寫入內建人設:大肥魚、阿茶、听澜等內建人設不能改原文,改成寫入 `<DSH_HOME>/line-bot/persona-overlays/<id>.md` 覆蓋層,每輪注入、不用重載;自訂人設仍直接改本文。
package/client.js CHANGED
@@ -759,7 +759,7 @@ window.__ModuleLoader__.load({
759
759
  const selectedPersona = personaCatalog.find((item) => item.id === personaCfg.id) || null
760
760
  const selectedName = selectedPersona?.name || personaCfg.id || '大肥魚'
761
761
  const selectedIsLocal = String(personaCfg.id || '').startsWith('local:')
762
- const syncPersonaOptions = personaOptions.slice()
762
+ const syncPersonaOptions = personaCatalog.slice()
763
763
  const syncTarget = syncCfg.targetPersonaId || (syncPersonaOptions[0] && syncPersonaOptions[0].id) || ''
764
764
  const bot = status.line?.bot || null
765
765
  const webhook = status.line?.webhook || null
@@ -1077,7 +1077,7 @@ window.__ModuleLoader__.load({
1077
1077
  onChange: (event) => update({ agent: { model: event.target.value } }),
1078
1078
  }),
1079
1079
  h('datalist', { id: 'line-bot-model-options' },
1080
- ((modelProviders.find((item) => item.id === (draft.agent.provider || agentStatus.defaultProvider)) || {}).models || []).map((item) => h('option', { key: item.id, value: item.id }, item.name))),
1080
+ ((modelProviders.find((item) => item.id === (draft.agent.provider || agentStatus.defaultProvider)) || {}).models || []).map((item) => h('option', { key: item.id, value: item.id }, item.name + (String(item.id).startsWith('local:') ? '(插件自訂)' : (item.builtin ? '(內建)' : ''))))),
1081
1081
  ),
1082
1082
  h(Field, { label: '推理強度(選填)', hint: '留空=跟隨 provider 預設' },
1083
1083
  h(Text, { value: draft.agent.reasoningEffort || '', onChange: (value) => update({ agent: { reasoningEffort: value } }), placeholder: 'max / high / medium' })),
@@ -1160,7 +1160,7 @@ window.__ModuleLoader__.load({
1160
1160
  { id: sectionDomId('sync'), order: 6, title: '貼吧學習同步', desc: '把「刷貼吧」每天演化學到的語氣樣本,寫進上方人設。自訂人設會直接改人設本文;內建人設(大肥魚、阿茶、听澜)不能改原文,會寫進插件自己的覆蓋層,每輪注入、不用重載。' },
1161
1161
  h(Toggle, { checked: syncCfg.enabled !== false, onChange: (value) => update({ sync: { enabled: value } }), label: '開啟自動同步', hint: '每小時檢查一次;貼吧人設版本有變才寫入(寫入前會備份 personas.json)' }),
1162
1162
  h('div', { style: S.grid },
1163
- h(Field, { label: '同步到哪個人設', hint: '內建人設使用覆蓋層;自訂人設直接改本文' },
1163
+ h(Field, { label: '同步到哪個人設', hint: '內建人設與插件自訂人設使用覆蓋層;大肥魚的自訂人設直接改本文' },
1164
1164
  h('select', { value: syncTarget, onChange: (event) => update({ sync: { targetPersonaId: event.target.value } }), style: S.input },
1165
1165
  syncPersonaOptions.length
1166
1166
  ? syncPersonaOptions.map((item) => h('option', { key: item.id, value: item.id }, item.name))
package/lib/host.mjs CHANGED
@@ -28,7 +28,7 @@ const stickersMod = await import(withVersion('./stickers.mjs'))
28
28
  const memeSourceMod = await import(withVersion('./meme-source.mjs'))
29
29
  const stickerImageMod = await import(withVersion('./sticker-image.mjs'))
30
30
  const utilMod = await import(withVersion('./util.mjs'))
31
- const { createConfigStore, publicConfig, dataDir } = configMod
31
+ const { createConfigStore, publicConfig, dataDir, dshHome } = configMod
32
32
  const { createPersonaClient } = personaMod
33
33
  const { createAgentRunner } = runnerMod
34
34
  const { createWebhookServer } = webhookMod
@@ -43,9 +43,9 @@ const { createJokeStore } = jokesMod
43
43
  const { createStickerStore } = stickersMod
44
44
  const { createMemeSource } = memeSourceMod
45
45
  const { createStickerImages } = stickerImageMod
46
- const { chunkText, clip, inQuietWindow, jsonFail, jsonOk, maskSecret, readJsonBody, safeArray, sleep } = utilMod
46
+ const { chunkText, clip, inQuietWindow, jsonFail, jsonOk, maskSecret, readJsonBody, safeArray, sleep, readJsonSafe } = utilMod
47
47
 
48
- export const VERSION = '0.9.0'
48
+ export const VERSION = '0.9.1'
49
49
 
50
50
  // 環境變數提供的 Cloudflare API Token(方便用 shell 注入、不用把 token 寫進設定檔)。
51
51
  // 設定檔裡的 token 優先;兩者都沒有時才用環境變數。
@@ -161,17 +161,17 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
161
161
  } catch (error) {
162
162
  logger.warn(`請大肥魚讓開失敗(自訂人設仍會注入):${errorText(error)}`)
163
163
  }
164
- return { persona: null, personaText: resolved.text, source: 'local', name: resolved.name }
164
+ return { persona: null, personaText: resolved.text, source: 'local', name: resolved.name, selection: resolved.id }
165
165
  }
166
166
  if (resolved.kind === 'dafeiyu') {
167
- return { persona: resolved.id, personaText: null, source: 'dafeiyu', name: '' }
167
+ return { persona: resolved.id, personaText: null, source: 'dafeiyu', name: '', selection: resolved.id }
168
168
  }
169
- return { persona: null, personaText: null, source: '', name: '' }
169
+ return { persona: null, personaText: null, source: '', name: '', selection: '' }
170
170
  }
171
171
 
172
172
  // 內建人設(大肥魚等)不能改原文,貼吧同步會寫在覆蓋層;每輪注入在 persona 之後。
173
173
  function personaOverlayText(applied) {
174
- const id = applied?.persona
174
+ const id = applied?.selection || applied?.persona
175
175
  if (!id) return ''
176
176
  try {
177
177
  return readPersonaOverlay(id)
@@ -1675,6 +1675,33 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
1675
1675
 
1676
1676
  // ── 人設摘要(給面板) ───────────────────────────────────────────────────
1677
1677
  let personaCache = { at: 0, value: null }
1678
+ function readDafeiyuStoreFallback() {
1679
+ const builtins = [
1680
+ { id: 'dafeiyu', name: '大肥魚', chars: 635 },
1681
+ { id: 'acha', name: '阿茶', chars: 979 },
1682
+ { id: 'tinglan', name: '听澜', chars: 1738 },
1683
+ ].map((item) => ({ ...item, builtin: true, enabled: true, description: '', source: 'dafeiyu' }))
1684
+ const list = builtins
1685
+ try {
1686
+ const file = path.join(dshHome(), 'storages', 'dsh-persona-dafeiyu', 'personas.json')
1687
+ const raw = readJsonSafe(file, null)
1688
+ for (const item of raw?.personas || []) {
1689
+ const id = String(item?.id || '').trim()
1690
+ if (!id) continue
1691
+ list.push({
1692
+ id,
1693
+ name: String(item.name || id),
1694
+ builtin: false,
1695
+ enabled: true,
1696
+ chars: String(item.text || '').length,
1697
+ description: '直接從 personas.json 讀取',
1698
+ source: 'dafeiyu',
1699
+ })
1700
+ }
1701
+ } catch { /* 讀不到就只列內建 */ }
1702
+ return list
1703
+ }
1704
+
1678
1705
  async function getPersonaSummary({ force = false } = {}) {
1679
1706
  const cfg = store.get()
1680
1707
  if (!force && personaCache.value && Date.now() - personaCache.at < 5000) return personaCache.value
@@ -1722,7 +1749,11 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
1722
1749
  ]
1723
1750
  } catch (error) {
1724
1751
  summary.error = errorText(error)
1725
- summary.catalog = [...local]
1752
+ const fallback = readDafeiyuStoreFallback()
1753
+ summary.list = fallback
1754
+ summary.mode = 'unknown'
1755
+ summary.catalog = [...fallback, ...local]
1756
+ summary.warning = '讀不到大肥魚外掛的 HTTP bridge,已直接讀取 personas.json;清單可能較舊,寫入後請重啟大肥魚外掛。'
1726
1757
  }
1727
1758
  const selected = summary.catalog.find((item) => item.id === cfg.persona.id)
1728
1759
  summary.selectedName = selected?.name || ''
@@ -1730,7 +1761,7 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
1730
1761
  summary.warning = selectedSource === 'local'
1731
1762
  ? `找不到自訂人設「${cfg.persona.id}」,LINE 會改用預設人設。`
1732
1763
  : `找不到人設「${cfg.persona.id}」,LINE 會退回大肥魚的全域人設。`
1733
- } else if (selectedSource === 'dafeiyu' && cfg.persona.enforce && summary.mode !== 'session') {
1764
+ } else if (!summary.error && selectedSource === 'dafeiyu' && cfg.persona.enforce && summary.mode !== 'session') {
1734
1765
  summary.warning = `大肥魚目前是「${summary.mode || '未知'}」模式;LINE 的逐 session 人設指定要切到 session 模式才會生效。`
1735
1766
  }
1736
1767
  personaCache = { at: Date.now(), value: summary }
@@ -223,8 +223,9 @@ export function createTiebaSync({ ctx, store, logger, getCatalog }) {
223
223
  const list = await catalog()
224
224
  const info = list.find((item) => item.id === targetId)
225
225
  const builtin = Boolean(info?.builtin) || ['dafeiyu', 'acha', 'tinglan'].includes(targetId)
226
+ const overlayMode = builtin || String(targetId).startsWith('local:') || info?.source === 'local'
226
227
  const section = buildSection(tieba)
227
- if (builtin) {
228
+ if (overlayMode) {
228
229
  const oldText = readPersonaOverlay(targetId)
229
230
  return {
230
231
  ok: true,
@@ -269,6 +270,7 @@ export function createTiebaSync({ ctx, store, logger, getCatalog }) {
269
270
  const list = await catalog()
270
271
  const info = list.find((item) => item.id === targetId)
271
272
  const builtin = Boolean(info?.builtin) || ['dafeiyu', 'acha', 'tinglan'].includes(targetId)
273
+ const overlayMode = builtin || String(targetId).startsWith('local:') || info?.source === 'local'
272
274
  const section = buildSection(tieba)
273
275
  const at = Date.now()
274
276
  const stateSync = store.getState().sync || {}
@@ -277,7 +279,7 @@ export function createTiebaSync({ ctx, store, logger, getCatalog }) {
277
279
  let reload = null
278
280
  let sectionText = section
279
281
 
280
- if (builtin) {
282
+ if (overlayMode) {
281
283
  const oldText = readPersonaOverlay(targetId)
282
284
  const nextText = applySection(oldText, section).trimEnd() + '\n'
283
285
  changed = sansStamp(oldText) !== sansStamp(section)
@@ -291,7 +293,7 @@ export function createTiebaSync({ ctx, store, logger, getCatalog }) {
291
293
  fs.writeFileSync(backupPath, oldText)
292
294
  }
293
295
  overlayWrite(targetId, nextText)
294
- logger.info(`已把刷貼吧學到的口氣寫進內建人設「${info?.name || targetId}」的覆蓋層`)
296
+ logger.info(`已把刷貼吧學到的口氣寫進「${info?.name || targetId}」的覆蓋層`)
295
297
  }
296
298
  sectionText = nextText
297
299
  } else {
@@ -326,7 +328,7 @@ export function createTiebaSync({ ctx, store, logger, getCatalog }) {
326
328
  at,
327
329
  version: tieba.version,
328
330
  targetId,
329
- mode: builtin ? 'overlay' : 'persona',
331
+ mode: overlayMode ? 'overlay' : 'persona',
330
332
  samples: tieba.samples.length,
331
333
  changed,
332
334
  backupPath,
@@ -335,7 +337,7 @@ export function createTiebaSync({ ctx, store, logger, getCatalog }) {
335
337
  },
336
338
  }))
337
339
 
338
- if (!builtin && changed && cfg.reloadPersona !== false) {
340
+ if (!overlayMode && changed && cfg.reloadPersona !== false) {
339
341
  try {
340
342
  reload = await reloadPersonaPlugin(ctx)
341
343
  if (!reload.ok) logger.warn(`重載大肥魚外掛失敗:${reload.error}`)
@@ -350,7 +352,7 @@ export function createTiebaSync({ ctx, store, logger, getCatalog }) {
350
352
  version: tieba.version,
351
353
  targetId,
352
354
  targetName: info?.name || targetId,
353
- mode: builtin ? 'overlay' : 'persona',
355
+ mode: overlayMode ? 'overlay' : 'persona',
354
356
  changed,
355
357
  samples: tieba.samples.length,
356
358
  backupPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-line-bot",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "把 LINE 官方帳號(Messaging API)接進 DeepSeek Harness 的插件:內建 Cloudflare Tunnel、人設選擇(大肥魚或插件自訂)、主動傳訊與完整設定面板。",
5
5
  "type": "module",
6
6
  "main": "index.js",