dsh-line-bot 0.3.0 → 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 +11 -0
- package/README.en.md +13 -2
- package/README.ja.md +13 -2
- package/README.ko.md +13 -2
- package/README.md +13 -2
- package/README.zh-CN.md +13 -2
- package/bin/cli.mjs +53 -14
- package/client.js +133 -17
- package/lib/config.mjs +2 -0
- package/lib/host.mjs +85 -7
- package/lib/tieba-sync.mjs +19 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- 新增「模型與接口來源」:可選這個 DSH profile 已註冊的 provider 與模型、推理強度;留空則跟隨 DSH 預設。
|
|
6
|
+
- DeepSeek 餘額只適用 `deepseek-account`:切成其他 provider 時,餘額卡會反白停用、不再抓餘額,也不會主動提醒儲值。
|
|
7
|
+
- 面板順序調整:LINE 憑證移到第 2、Cloudflare Tunnel 第 3,模型與來源第 4。
|
|
8
|
+
|
|
9
|
+
## 0.3.1
|
|
10
|
+
|
|
11
|
+
- 修好 Windows 安裝:`npx dsh-line-bot install` 現在會找到 DeepSeek Harness 內建的 pnpm(`%LOCALAPPDATA%\Programs\…`、`%PROGRAMFILES%`),也會正確透過 `shell` 執行 `dsh.cmd` / `pnpm.cmd`。
|
|
12
|
+
- 找不到 pnpm 時的錯誤訊息補上 Windows 的建議(`npm install -g pnpm` 或改用面板安裝)。
|
|
13
|
+
|
|
3
14
|
## 0.3.0
|
|
4
15
|
|
|
5
16
|
- 新增「個人記憶」:每位使用者一份長期記憶(`<DSH_HOME>/line-bot/memories.json`),角色可在對話中自己呼叫 `line_memory_add` 記下重要資訊、用 `line_memory_forget` 忘掉、用 `line_memory_list` 確認。
|
package/README.en.md
CHANGED
|
@@ -71,11 +71,13 @@ At the top of the panel is a **sticky save bar**: "Save changes / Refresh" stays
|
|
|
71
71
|
| Section | Contents |
|
|
72
72
|
| --- | --- |
|
|
73
73
|
| Overview | Green-light status for webhook server / tunnel / LINE / persona / proactive messages, public URL, one-click bring-up and start/stop |
|
|
74
|
-
| DeepSeek balance | Current balance, per-turn context, low-balance proactive reminder, threshold and cooldown |
|
|
75
|
-
| Personal memory | Per-user long-term memory, expiry cleanup, daily self-review, manual delete |
|
|
76
74
|
| LINE credentials | Channel ID, Channel Secret, Access Token, auto-issue, loading animation, bot info, webhook apply/test, allow-list |
|
|
77
75
|
| Cloudflare Tunnel | quick / API token modes, token, hostname, tunnel name, Account/Zone ID, download cloudflared, start/stop, auto-configure webhook, bring your own public URL |
|
|
76
|
+
| Model & provider | Pick the provider/model and reasoning effort for LINE replies; disables DeepSeek balance when the provider is not deepseek-account |
|
|
78
77
|
| Reply persona | Choose a persona (dafeiyu or a plugin custom one), add/edit/delete custom personas, copy the current persona into a custom one, enforce, allow users to switch personas, switch dafeiyu to session mode, test an Agent reply |
|
|
78
|
+
| Tieba learning sync | Writes the tone samples learned while browsing Tieba back into the persona's "Tieba tone" section, preview diff, sync now |
|
|
79
|
+
| Personal memory | Per-user long-term memory, expiry cleanup, daily self-review, manual delete |
|
|
80
|
+
| DeepSeek balance | Current balance, per-turn context, low-balance proactive reminder, threshold and cooldown (deepseek-account only) |
|
|
79
81
|
| Proactive messages | Toggle, probability, interval, daily/monthly caps, minimum gap, quiet hours, idle, target users, prompt, send a test now, statistics |
|
|
80
82
|
| Users | The DSH session each LINE user maps to, their own workspace folder, their current persona (editable), message count, last interaction, clear memory / reset session |
|
|
81
83
|
| Advanced | Port, Agent provider/model/effort/cwd/timeout, one workspace per user (and its root), welcome/error/empty replies, logs, reload host modules (dev) |
|
|
@@ -96,6 +98,15 @@ At the top of the panel is a **sticky save bar**: "Save changes / Refresh" stays
|
|
|
96
98
|
4. It takes the last assistant text from the session events and replies with LINE reply; if the reply token has expired it falls back to push.
|
|
97
99
|
5. The proactive-message scheduler checks once a minute (interval, quiet hours, caps, probability); when conditions are met it asks the Agent to say something on its own and pushes it.
|
|
98
100
|
|
|
101
|
+
## Model & provider
|
|
102
|
+
|
|
103
|
+
The "Model & provider" card chooses the provider/model used for LINE replies:
|
|
104
|
+
|
|
105
|
+
- Providers come from this DSH profile; models are fetched live and the first one is preselected when you pick a provider.
|
|
106
|
+
- "Follow DSH default" leaves the choice to `agentDefaultModel` (the panel shows the effective value).
|
|
107
|
+
- Reasoning effort can stay empty to use the provider default.
|
|
108
|
+
- DeepSeek balance and low-balance reminders only work when the provider is `deepseek-account`; other providers grey out that card, and switching back restores it.
|
|
109
|
+
|
|
99
110
|
## Persona sources
|
|
100
111
|
|
|
101
112
|
- **dafeiyu (optional)**: when `dsh-persona-dafeiyu` is installed, the panel lists its personas (built-in + your own); LINE selects them per session, without changing other GUI conversations. It must be in `session` mode; the panel tells you and offers a one-click switch.
|
package/README.ja.md
CHANGED
|
@@ -71,11 +71,13 @@ dsh plugin --profile desktop add dsh-line-bot
|
|
|
71
71
|
| セクション | 内容 |
|
|
72
72
|
| --- | --- |
|
|
73
73
|
| 概要 | webhook サーバー/トンネル/LINE/ペルソナ/自発メッセージの状態、公開 URL、ワンクリック起動と停止 |
|
|
74
|
-
| DeepSeek 残高 | 現在の残高、毎ターンの注入、残高が少ないときの主動リマインド、しきい値とクールダウン |
|
|
75
|
-
| 個人メモリ | ユーザーごとの長期記憶、期限切れの自動削除、毎日の自己整理、手動削除 |
|
|
76
74
|
| LINE 認証情報 | Channel ID、Channel Secret、Access Token、自動発行、ローディング表示、bot 情報、webhook の適用/テスト、許可リスト |
|
|
77
75
|
| Cloudflare Tunnel | quick/API トークンモード、トークン、hostname、トンネル名、Account/Zone ID、cloudflared のダウンロード、起動/停止、webhook 自動設定、独自の公開 URL |
|
|
76
|
+
| モデルとプロバイダー | LINE 返信で使う provider/モデルと推論強度を選択。deepseek-account 以外では DeepSeek 残高を自動で無効化 |
|
|
78
77
|
| 返信ペルソナ | ペルソナ選択(大肥魚またはプラグインのカスタム)、カスタムペルソナの追加・編集・削除、現在のペルソナをカスタムに複製、強制適用、ユーザーによる切り替え許可、大肥魚を session モードに切り替え、Agent 返信のテスト |
|
|
78
|
+
| 貼吧学習の同期 | 貼吧で学んだ口調をペルソナの「貼吧学来的口气」節に書き戻す、差分プレビュー、今すぐ同期 |
|
|
79
|
+
| 個人メモリ | ユーザーごとの長期記憶、期限切れの自動削除、毎日の自己整理、手動削除 |
|
|
80
|
+
| DeepSeek 残高 | 現在の残高、毎ターンの注入、残高が少ないときの主動リマインド、しきい値とクールダウン(deepseek-account のみ) |
|
|
79
81
|
| 自発メッセージ | オン/オフ、確率、間隔、1 日/1 か月の上限、最短間隔、静かな時間帯、idle、対象ユーザー、プロンプト、今すぐ試送、統計 |
|
|
80
82
|
| ユーザー | LINE ユーザーごとに対応する DSH セッション、専用ワークスペース、現在のペルソナ(直接変更可)、メッセージ数、最終やりとり、メモリ消去/セッション再作成 |
|
|
81
83
|
| 詳細設定 | ポート、Agent の provider/model/effort/cwd/タイムアウト、ユーザーごとのワークスペース(ルート含む)、ウェルカム/エラー/空返信、ログ、開発用 host リロード |
|
|
@@ -96,6 +98,15 @@ dsh plugin --profile desktop add dsh-line-bot
|
|
|
96
98
|
4. セッションイベントから最後の assistant テキストを取り出し、LINE の reply で返信します。reply token が切れていたら自動で push に切り替えます。
|
|
97
99
|
5. 自発メッセージのスケジューラーが 1 分ごとに確認し(間隔、静かな時間帯、上限、確率)、条件を満たせば Agent に一言話してもらって push します。
|
|
98
100
|
|
|
101
|
+
## モデルとプロバイダー
|
|
102
|
+
|
|
103
|
+
「モデルとプロバイダー」カードは LINE 返信で使う provider/モデルを決めます:
|
|
104
|
+
|
|
105
|
+
- provider はこの DSH profile に登録されているもの、モデルはライブで取得し、provider を選ぶと最初のモデルが入ります。
|
|
106
|
+
- 「DSH のデフォルトに従う」は `agentDefaultModel` の provider/model をそのまま使います(実際の値はパネルに表示)。
|
|
107
|
+
- 推論強度は空欄なら provider のデフォルト。
|
|
108
|
+
- provider が `deepseek-account` のときだけ DeepSeek 残高と残高リマインドが動きます。他の provider ではカードがグレーアウトして無効になり、戻すと復帰します。
|
|
109
|
+
|
|
99
110
|
## ペルソナのソース
|
|
100
111
|
|
|
101
112
|
- **大肥魚(任意)**:`dsh-persona-dafeiyu` がインストールされていると、パネルにそのペルソナ(内蔵+自作)が表示されます。LINE ではセッション単位で指定され、GUI の他の会話には影響しません。`session` モードである必要があり、パネルが案内してワンクリックで切り替えられます。
|
package/README.ko.md
CHANGED
|
@@ -71,11 +71,13 @@ dsh plugin --profile desktop add dsh-line-bot
|
|
|
71
71
|
| 섹션 | 내용 |
|
|
72
72
|
| --- | --- |
|
|
73
73
|
| 개요 | webhook 서버/터널/LINE/페르소나/능동 메시지 상태, 공개 URL, 원클릭 기동과 시작/중지 |
|
|
74
|
-
| DeepSeek 잔액 | 현재 잔액, 매 턴 주입, 잔액이 적을 때 캐릭터가 먼저 알림, 기준 금액과 쿨다운 |
|
|
75
|
-
| 개인 기억 | 사용자별 장기 기억, 만료 자동 삭제, 매일 자기 정리, 수동 삭제 |
|
|
76
74
|
| LINE 자격 증명 | Channel ID, Channel Secret, Access Token, 자동 발급, 로딩 표시, bot 정보, webhook 적용/테스트, 허용 목록 |
|
|
77
75
|
| Cloudflare Tunnel | quick/API 토큰 모드, 토큰, hostname, 터널 이름, Account/Zone ID, cloudflared 다운로드, 시작/중지, webhook 자동 설정, 직접 제공하는 공개 URL |
|
|
76
|
+
| 모델과 공급자 | LINE 답장에 쓸 provider/모델과 추론 강도 선택. deepseek-account가 아니면 DeepSeek 잔액 자동 비활성화 |
|
|
78
77
|
| 답장 페르소나 | 페르소나 선택(大肥魚 또는 플러그인 커스텀), 커스텀 페르소나 추가/편집/삭제, 현재 페르소나를 커스텀으로 복사, 강제 적용, 사용자 직접 전환 허용, 大肥魚를 session 모드로 전환, Agent 답장 테스트 |
|
|
78
|
+
| 貼吧 학습 동기화 | 貼吧에서 배운 말투를 페르소나의 「貼吧学来的口气」 섹션으로 기록, 차이 미리보기, 지금 동기화 |
|
|
79
|
+
| 개인 기억 | 사용자별 장기 기억, 만료 자동 삭제, 매일 자기 정리, 수동 삭제 |
|
|
80
|
+
| DeepSeek 잔액 | 현재 잔액, 매 턴 주입, 잔액이 적을 때 캐릭터가 먼저 알림, 기준 금액과 쿨다운(deepseek-account 전용) |
|
|
79
81
|
| 능동 메시지 | 켜기/끄기, 확률, 간격, 일일/월간 상한, 최소 간격, 조용한 시간대, idle, 대상 사용자, 프롬프트, 지금 시험 발송, 통계 |
|
|
80
82
|
| 사용자 | LINE 사용자별 DSH 세션, 전용 작업 폴더, 현재 페르소나(직접 변경 가능), 메시지 수, 마지막 상호작용, 기억 지우기/세션 재설정 |
|
|
81
83
|
| 고급 설정 | 포트, Agent provider/model/effort/cwd/타임아웃, 사용자별 작업 폴더(루트 포함), 환영/오류/빈 답장, 로그, 개발용 host 리로드 |
|
|
@@ -96,6 +98,15 @@ dsh plugin --profile desktop add dsh-line-bot
|
|
|
96
98
|
4. 세션 이벤트에서 마지막 assistant 텍스트를 꺼내 LINE reply로 답장합니다. reply token이 만료되었으면 자동으로 push로 전환합니다.
|
|
97
99
|
5. 능동 메시지 스케줄러가 1분마다 확인하고(간격, 조용한 시간대, 상한, 확률) 조건을 만족하면 Agent에게 한마디 하게 한 뒤 push합니다.
|
|
98
100
|
|
|
101
|
+
## 모델과 공급자
|
|
102
|
+
|
|
103
|
+
「모델과 공급자」카드는 LINE 답장에 쓸 provider/모델을 정합니다:
|
|
104
|
+
|
|
105
|
+
- provider는 이 DSH profile에 등록된 목록, 모델은 실시간으로 읽어 오며 provider를 고르면 첫 모델이 자동 입력됩니다.
|
|
106
|
+
- 「DSH 기본값 따르기」는 `agentDefaultModel`의 provider/모델을 그대로 씁니다(패널에 실제 값 표시).
|
|
107
|
+
- 추론 강도는 비워 두면 provider 기본값.
|
|
108
|
+
- provider가 `deepseek-account`일 때만 DeepSeek 잔액과 잔액 알림이 동작합니다. 다른 provider에서는 카드가 회색으로 비활성화되고, 돌아오면 복구됩니다.
|
|
109
|
+
|
|
99
110
|
## 페르소나 출처
|
|
100
111
|
|
|
101
112
|
- **大肥魚(선택)**: `dsh-persona-dafeiyu`가 설치되어 있으면 패널에 그 페르소나(내장+직접 만든 것)가 나옵니다. LINE에서는 세션 단위로 지정되며 GUI의 다른 대화는 바뀌지 않습니다. `session` 모드여야 하며, 패널이 안내하고 원클릭으로 전환할 수 있습니다.
|
package/README.md
CHANGED
|
@@ -71,11 +71,13 @@ dsh plugin --profile desktop add dsh-line-bot
|
|
|
71
71
|
| 區塊 | 內容 |
|
|
72
72
|
| --- | --- |
|
|
73
73
|
| 狀態總覽 | webhook server/隧道/LINE/人設/主動傳訊的綠燈狀態、公開網址、一鍵上線與啟停 |
|
|
74
|
-
| DeepSeek 餘額 | 目前餘額、每輪注入、低於門檻時由角色主動提醒、提醒門檻與冷卻 |
|
|
75
|
-
| 個人記憶 | 每位使用者分開的長期記憶、到期自動刪、每日請角色整理、手動刪除 |
|
|
76
74
|
| LINE 憑證 | Channel ID、Channel Secret、Access Token、自動簽發、loading 動畫、bot 資訊、webhook 套用/測試、白名單 |
|
|
77
75
|
| Cloudflare Tunnel | quick/API Token 模式、Token、hostname、隧道名稱、Account/Zone ID、下載 cloudflared、啟停、自動設定 webhook、自行提供的公開網址 |
|
|
76
|
+
| 模型與接口來源 | 選擇 LINE 回覆的 provider/模型與推理強度;非 deepseek-account 時自動停用 DeepSeek 餘額 |
|
|
78
77
|
| 回覆人設 | 選擇人設(大肥魚或插件自訂)、新增/編輯/刪除自訂人設、把目前人設複製成自訂、強制套用、允許使用者自己換人設、切換大肥魚 session 模式、測試 Agent 回覆 |
|
|
78
|
+
| 貼吧學習同步 | 把刷貼吧學到的語氣寫回人設的「貼吧學來的口氣」段、預覽差異、立即同步 |
|
|
79
|
+
| 個人記憶 | 每位使用者分開的長期記憶、到期自動刪、每日請角色整理、手動刪除 |
|
|
80
|
+
| DeepSeek 餘額 | 目前餘額、每輪注入、低於門檻時由角色主動提醒、提醒門檻與冷卻(僅 deepseek-account) |
|
|
79
81
|
| 主動傳訊 | 開關、機率、間隔、每日/每月上限、最短間隔、安靜時段、idle、指定對象、提示詞、立即試傳、統計 |
|
|
80
82
|
| 使用者 | 每位 LINE 使用者對應的 DSH session、專屬工作資料夾、目前人設(可直接改)、訊息數、上次互動、清空記憶/重設 session |
|
|
81
83
|
| 進階設定 | 連接埠、Agent provider/model/effort/cwd/逾時、一人一個工作資料夾(含根目錄)、歡迎/錯誤/空回覆、日誌、開發用 host 重新載入 |
|
|
@@ -96,6 +98,15 @@ dsh plugin --profile desktop add dsh-line-bot
|
|
|
96
98
|
4. 從 session 事件取出最後一段 assistant 文字,用 LINE reply 回覆;reply token 過期就自動改 push。
|
|
97
99
|
5. 主動傳訊排程器每分鐘檢查一次(間隔、安靜時段、上限、機率),符合條件就請 Agent 主動說一句,再 push 出去。
|
|
98
100
|
|
|
101
|
+
## 模型與接口來源
|
|
102
|
+
|
|
103
|
+
「模型與接口來源」卡決定 LINE 回覆要用哪個 provider/模型:
|
|
104
|
+
|
|
105
|
+
- 來源清單來自這個 DSH profile 已註冊的 provider;模型清單即時讀取,選 provider 後會自動帶入第一個模型。
|
|
106
|
+
- 「跟隨 DSH 預設」=不覆寫,使用 `agentDefaultModel` 目前的 provider/model(面板會顯示實際值)。
|
|
107
|
+
- 推理強度可留空跟隨 provider 預設。
|
|
108
|
+
- 只有 provider 是 `deepseek-account` 時,DeepSeek 餘額與低餘額提醒才會運作;切到其他來源時該卡會反白停用,切回即恢復。
|
|
109
|
+
|
|
99
110
|
## 人設來源
|
|
100
111
|
|
|
101
112
|
- **大肥魚(選用)**:有安裝 `dsh-persona-dafeiyu` 時,面板會列出它的人設(內建+你自訂的),LINE 會逐 session 指定,不會改到 GUI 其他對話。需要它處於 `session` 模式;面板會提示並可一鍵切換。
|
package/README.zh-CN.md
CHANGED
|
@@ -71,11 +71,13 @@ dsh plugin --profile desktop add dsh-line-bot
|
|
|
71
71
|
| 区块 | 内容 |
|
|
72
72
|
| --- | --- |
|
|
73
73
|
| 状态总览 | webhook server/隧道/LINE/人设/主动传讯的绿灯状态、公开网址、一键上线与启停 |
|
|
74
|
-
| DeepSeek 余额 | 当前余额、每轮注入、低于门槛时由角色主动提醒、提醒门槛与冷却 |
|
|
75
|
-
| 个人记忆 | 每位用户分开的长期记忆、到期自动删除、每日请角色整理、手动删除 |
|
|
76
74
|
| LINE 凭证 | Channel ID、Channel Secret、Access Token、自动签发、loading 动画、bot 信息、webhook 套用/测试、白名单 |
|
|
77
75
|
| Cloudflare Tunnel | quick/API Token 模式、Token、hostname、隧道名称、Account/Zone ID、下载 cloudflared、启停、自动设置 webhook、自行提供的公开网址 |
|
|
76
|
+
| 模型与接口来源 | 选择 LINE 回复的 provider/模型与推理强度;非 deepseek-account 时自动停用 DeepSeek 余额 |
|
|
78
77
|
| 回复人设 | 选择人设(大肥鱼或插件自定义)、新增/编辑/删除自定义人设、把目前人设复制成自定义、强制套用、允许用户自己换人设、切换大肥鱼 session 模式、测试 Agent 回复 |
|
|
78
|
+
| 贴吧学习同步 | 把刷贴吧学到的语气写回人设的「贴吧学来的口气」段、预览差异、立即同步 |
|
|
79
|
+
| 个人记忆 | 每位用户分开的长期记忆、到期自动删除、每日请角色整理、手动删除 |
|
|
80
|
+
| DeepSeek 余额 | 当前余额、每轮注入、低于门槛时由角色主动提醒、提醒门槛与冷却(仅 deepseek-account) |
|
|
79
81
|
| 主动传讯 | 开关、概率、间隔、每日/每月上限、最短间隔、安静时段、idle、指定对象、提示词、立即试传、统计 |
|
|
80
82
|
| 用户 | 每位 LINE 用户对应的 DSH session、专属工作文件夹、目前人设(可直接改)、消息数、上次互动、清空记忆/重设 session |
|
|
81
83
|
| 高级设置 | 端口、Agent provider/model/effort/cwd/超时、一人一个工作文件夹(含根目录)、欢迎/错误/空回复、日志、开发用 host 重新载入 |
|
|
@@ -96,6 +98,15 @@ dsh plugin --profile desktop add dsh-line-bot
|
|
|
96
98
|
4. 从 session 事件取出最后一段 assistant 文字,用 LINE reply 回复;reply token 过期就自动改 push。
|
|
97
99
|
5. 主动传讯排程器每分钟检查一次(间隔、安静时段、上限、概率),符合条件就请 Agent 主动说一句,再 push 出去。
|
|
98
100
|
|
|
101
|
+
## 模型与接口来源
|
|
102
|
+
|
|
103
|
+
「模型与接口来源」卡决定 LINE 回复要用哪个 provider/模型:
|
|
104
|
+
|
|
105
|
+
- 来源清单来自这个 DSH profile 已注册的 provider;模型清单即时读取,选 provider 后会自动带入第一个模型。
|
|
106
|
+
- 「跟随 DSH 默认」=不覆写,使用 `agentDefaultModel` 目前的 provider/model(面板会显示实际值)。
|
|
107
|
+
- 推理强度可留空跟随 provider 默认。
|
|
108
|
+
- 只有 provider 是 `deepseek-account` 时,DeepSeek 余额与低余额提醒才会运作;切到其他来源时该卡会反白停用,切回即恢复。
|
|
109
|
+
|
|
99
110
|
## 人设来源
|
|
100
111
|
|
|
101
112
|
- **大肥鱼(可选)**:有安装 `dsh-persona-dafeiyu` 时,面板会列出它的人设(内置+你自定义的),LINE 会逐 session 指定,不会改到 GUI 其他对话。需要它处于 `session` 模式;面板会提示并可一键切换。
|
package/bin/cli.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import fs from 'node:fs'
|
|
|
16
16
|
import os from 'node:os'
|
|
17
17
|
import path from 'node:path'
|
|
18
18
|
import { spawnSync } from 'node:child_process'
|
|
19
|
+
|
|
20
|
+
const IS_WIN = process.platform === 'win32'
|
|
19
21
|
import { fileURLToPath } from 'node:url'
|
|
20
22
|
|
|
21
23
|
const PKG = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
@@ -140,19 +142,54 @@ function withBundle(manifest, enabled) {
|
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
function windowsPnpmCandidates() {
|
|
146
|
+
const roots = [
|
|
147
|
+
process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Programs'),
|
|
148
|
+
process.env.PROGRAMFILES,
|
|
149
|
+
process.env['PROGRAMFILES(X86)'],
|
|
150
|
+
].filter(Boolean)
|
|
151
|
+
const suffix = ['resources', 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.cjs']
|
|
152
|
+
const found = []
|
|
153
|
+
for (const root of roots) {
|
|
154
|
+
found.push(path.join(root, 'DeepSeek Harness', ...suffix))
|
|
155
|
+
try {
|
|
156
|
+
for (const name of fs.readdirSync(root)) {
|
|
157
|
+
if (/deepseek/i.test(name)) found.push(path.join(root, name, ...suffix))
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
/* 目錄不存在就跳過 */
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return found
|
|
164
|
+
}
|
|
165
|
+
|
|
143
166
|
function findPnpm() {
|
|
144
167
|
const localPnpm = process.platform === 'darwin'
|
|
145
|
-
? '/Applications/DeepSeek Harness.app/Contents/Resources/runtime/primary-runtime/dependencies/pnpm/bin/pnpm.cjs'
|
|
146
|
-
: ''
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
168
|
+
? ['/Applications/DeepSeek Harness.app/Contents/Resources/runtime/primary-runtime/dependencies/pnpm/bin/pnpm.cjs']
|
|
169
|
+
: process.platform === 'win32'
|
|
170
|
+
? windowsPnpmCandidates()
|
|
171
|
+
: []
|
|
172
|
+
for (const candidate of localPnpm) {
|
|
173
|
+
if (fs.existsSync(candidate)) return { kind: 'pnpm', command: process.execPath, prefix: [candidate], shell: false }
|
|
174
|
+
}
|
|
175
|
+
const pnpm = spawnSync('pnpm', ['--version'], { stdio: 'ignore', shell: IS_WIN })
|
|
176
|
+
if (pnpm.status === 0) return { kind: 'pnpm', command: 'pnpm', prefix: [], shell: IS_WIN }
|
|
150
177
|
return null
|
|
151
178
|
}
|
|
152
179
|
|
|
153
180
|
function findDsh() {
|
|
154
|
-
const probe = spawnSync('dsh', ['--version'], { stdio: 'ignore' })
|
|
155
|
-
return probe.status === 0 ? 'dsh' :
|
|
181
|
+
const probe = spawnSync('dsh', ['--version'], { stdio: 'ignore', shell: IS_WIN })
|
|
182
|
+
return probe.status === 0 ? { command: 'dsh', shell: IS_WIN } : null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function quoteWin(value) {
|
|
186
|
+
return /[\s"]/.test(value) ? '"' + value.replace(/"/g, '\\"') + '"' : value
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function spawnTool(tool, args, options) {
|
|
190
|
+
if (!tool.shell) return spawnSync(tool.command, [...(tool.prefix || []), ...args], options)
|
|
191
|
+
const line = [tool.command, ...args].map(quoteWin).join(' ')
|
|
192
|
+
return spawnSync(line, { ...options, shell: true })
|
|
156
193
|
}
|
|
157
194
|
|
|
158
195
|
function runInstall(profile, versionSpec, { dryRun }) {
|
|
@@ -161,18 +198,20 @@ function runInstall(profile, versionSpec, { dryRun }) {
|
|
|
161
198
|
let result
|
|
162
199
|
if (dsh) {
|
|
163
200
|
const args = ['plugin', '--profile', profile, 'add', versionSpec]
|
|
164
|
-
out(`→
|
|
165
|
-
if (!dryRun) result =
|
|
201
|
+
out(`→ dsh ${args.join(' ')}`)
|
|
202
|
+
if (!dryRun) result = spawnTool(dsh, args, { cwd: dir, stdio: 'inherit' })
|
|
166
203
|
} else {
|
|
167
204
|
const pnpm = findPnpm()
|
|
168
205
|
if (!pnpm) {
|
|
169
206
|
fail(
|
|
170
|
-
|
|
207
|
+
IS_WIN
|
|
208
|
+
? '找不到 dsh 或 pnpm。請先安裝 DeepSeek Harness 桌面版(內含 pnpm),或先執行「npm install -g pnpm」再重試;也可以直接用面板「設定 → 外掛 → 安裝 bundle」。'
|
|
209
|
+
: '找不到 dsh 或 pnpm。請先安裝 DeepSeek Harness 桌面版(內含 pnpm),或改用面板「設定 → 外掛 → 安裝」。',
|
|
171
210
|
)
|
|
172
211
|
}
|
|
173
|
-
const args = [...pnpm.prefix, 'add', versionSpec, '--dir', dir]
|
|
212
|
+
const args = [...(pnpm.prefix || []), 'add', versionSpec, '--dir', dir]
|
|
174
213
|
out(`→ ${pnpm.command} ${args.join(' ')}`)
|
|
175
|
-
if (!dryRun) result =
|
|
214
|
+
if (!dryRun) result = spawnTool(pnpm, args, { cwd: dir, stdio: 'inherit' })
|
|
176
215
|
}
|
|
177
216
|
if (!dryRun && result && result.status !== 0) {
|
|
178
217
|
fail(`安裝套件失敗(pnpm 結束碼 ${result.status})。請確認網路與 ${dir} 的權限後重試。`)
|
|
@@ -232,9 +271,9 @@ function commandUninstall(options) {
|
|
|
232
271
|
const dsh = findDsh()
|
|
233
272
|
if (dsh) {
|
|
234
273
|
const args = ['plugin', '--profile', profile, 'remove', NAME]
|
|
235
|
-
out(`→
|
|
274
|
+
out(`→ dsh ${args.join(' ')}`)
|
|
236
275
|
if (!options.dryRun) {
|
|
237
|
-
const result =
|
|
276
|
+
const result = spawnTool(dsh, args, { cwd: dir, stdio: 'inherit' })
|
|
238
277
|
if (result.status !== 0) out('(pnpm 移除失敗,可能本來就沒安裝;bundle 清單已處理)')
|
|
239
278
|
}
|
|
240
279
|
} else {
|
package/client.js
CHANGED
|
@@ -13,12 +13,13 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
// 二級選單:卡片區塊的快速跳轉。
|
|
14
14
|
const NAV = [
|
|
15
15
|
{ key: 'overview', label: '狀態總覽' },
|
|
16
|
-
{ key: 'balance', label: 'DeepSeek 餘額' },
|
|
17
|
-
{ key: 'memory', label: '個人記憶' },
|
|
18
16
|
{ key: 'line', label: 'LINE 憑證' },
|
|
19
17
|
{ key: 'tunnel', label: 'Cloudflare' },
|
|
18
|
+
{ key: 'model', label: '模型與來源' },
|
|
20
19
|
{ key: 'persona', label: '回覆人設' },
|
|
21
20
|
{ key: 'sync', label: '貼吧同步' },
|
|
21
|
+
{ key: 'memory', label: '個人記憶' },
|
|
22
|
+
{ key: 'balance', label: 'DeepSeek 餘額' },
|
|
22
23
|
{ key: 'nudge', label: '主動傳訊' },
|
|
23
24
|
{ key: 'users', label: '使用者' },
|
|
24
25
|
{ key: 'advanced', label: '進階/日誌' },
|
|
@@ -187,10 +188,10 @@ window.__ModuleLoader__.load({
|
|
|
187
188
|
return h('button', { type: 'button', style, onClick: disabled ? undefined : onClick, disabled: !!disabled, title: title || '' }, children)
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
function Card({ id, title, desc, right, children }) {
|
|
191
|
+
function Card({ id, title, desc, right, children, order }) {
|
|
191
192
|
return h(
|
|
192
193
|
'section',
|
|
193
|
-
{ id, 'data-section': id || undefined, style: { ...S.card, scrollMarginTop: 104 } },
|
|
194
|
+
{ id, 'data-section': id || undefined, style: { ...S.card, scrollMarginTop: 104, ...(order ? { order } : {}) } },
|
|
194
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),
|
|
195
196
|
h('div', { style: S.cardBody }, children),
|
|
196
197
|
)
|
|
@@ -251,6 +252,28 @@ window.__ModuleLoader__.load({
|
|
|
251
252
|
)
|
|
252
253
|
}
|
|
253
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
|
+
|
|
254
277
|
function Badge({ ok, children }) {
|
|
255
278
|
const color = ok === true ? S.ok.color : ok === false ? S.bad.color : S.warn.color
|
|
256
279
|
const dot = ok === true ? S.ok.color : ok === false ? S.bad.color : S.warn.color
|
|
@@ -293,6 +316,7 @@ window.__ModuleLoader__.load({
|
|
|
293
316
|
'persona-status': '重新讀取人設',
|
|
294
317
|
'persona-mode': '切換人設模式',
|
|
295
318
|
'tieba-preview': '預覽貼吧同步差異',
|
|
319
|
+
'model-catalog': '讀取模型清單',
|
|
296
320
|
'balance-refresh': '查詢 DeepSeek 餘額',
|
|
297
321
|
'memory-list': '讀取個人記憶',
|
|
298
322
|
'memory-delete': '刪除記憶',
|
|
@@ -323,6 +347,7 @@ window.__ModuleLoader__.load({
|
|
|
323
347
|
const [syncPreview, setSyncPreview] = useState(null)
|
|
324
348
|
const [balancePreview, setBalancePreview] = useState(null)
|
|
325
349
|
const [memoryData, setMemoryData] = useState(null)
|
|
350
|
+
const [modelCatalog, setModelCatalog] = useState(null)
|
|
326
351
|
const [localEdit, setLocalEdit] = useState({ id: '', name: '', text: '' })
|
|
327
352
|
|
|
328
353
|
const dirty = useMemo(() => {
|
|
@@ -481,8 +506,26 @@ window.__ModuleLoader__.load({
|
|
|
481
506
|
await loadMemory({ quiet: true })
|
|
482
507
|
}
|
|
483
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
|
+
|
|
484
526
|
useEffect(() => {
|
|
485
527
|
loadMemory({ quiet: true })
|
|
528
|
+
loadModelCatalog({ quiet: true })
|
|
486
529
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
487
530
|
}, [])
|
|
488
531
|
|
|
@@ -618,6 +661,8 @@ window.__ModuleLoader__.load({
|
|
|
618
661
|
const balanceTotal = balanceStatus.cny ? balanceStatus.cny.total : balanceStatus.usd ? balanceStatus.usd.total : null
|
|
619
662
|
const memoryCfg = cfg.memory || {}
|
|
620
663
|
const memoryStatus = status.memory || {}
|
|
664
|
+
const agentStatus = status.agent || {}
|
|
665
|
+
const modelProviders = Array.isArray(modelCatalog?.providers) ? modelCatalog.providers : []
|
|
621
666
|
const memoryUsers = memoryData?.users || []
|
|
622
667
|
const syncStats = status.sync || {}
|
|
623
668
|
const runtime = status
|
|
@@ -678,7 +723,7 @@ window.__ModuleLoader__.load({
|
|
|
678
723
|
// 1. 狀態總覽
|
|
679
724
|
h(
|
|
680
725
|
Card,
|
|
681
|
-
{ 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' ? '上線中…' : '一鍵上線') },
|
|
682
727
|
h(
|
|
683
728
|
'div',
|
|
684
729
|
{ style: S.row },
|
|
@@ -705,11 +750,20 @@ window.__ModuleLoader__.load({
|
|
|
705
750
|
h(
|
|
706
751
|
Card,
|
|
707
752
|
{
|
|
708
|
-
id: sectionDomId('balance'),
|
|
753
|
+
id: sectionDomId('balance'), order: 8,
|
|
709
754
|
title: 'DeepSeek 餘額',
|
|
710
|
-
desc: '讓角色知道 API
|
|
711
|
-
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' ? '查詢中…' : '重新查詢'),
|
|
712
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
|
+
},
|
|
713
767
|
h(
|
|
714
768
|
'div',
|
|
715
769
|
{ style: S.row },
|
|
@@ -737,6 +791,7 @@ window.__ModuleLoader__.load({
|
|
|
737
791
|
? ('查詢失敗:' + balanceStatus.error)
|
|
738
792
|
: '按「重新查詢」讀取目前的 DeepSeek API 餘額。',
|
|
739
793
|
),
|
|
794
|
+
h(Field, { label: '餘額提醒對象', hint: '留空=沿用主動傳訊的指定對象;只會提醒這裡選的人' }, h(UserChips, { users: status.users, value: balanceCfg.targetUserIds, onChange: (ids) => update({ balance: { targetUserIds: ids } }) })),
|
|
740
795
|
h(
|
|
741
796
|
'div',
|
|
742
797
|
{ style: S.grid },
|
|
@@ -756,13 +811,14 @@ window.__ModuleLoader__.load({
|
|
|
756
811
|
h(Btn, { tone: 'primary', onClick: askBalance, disabled: !!busy }, '立即提醒一次'),
|
|
757
812
|
),
|
|
758
813
|
balancePreview && balancePreview.text ? h('div', { style: S.panelBox }, h('div', { style: S.fieldTitle }, '角色會傳這則:'), h('pre', { style: S.pre }, balancePreview.text)) : null,
|
|
814
|
+
),
|
|
759
815
|
),
|
|
760
816
|
|
|
761
817
|
// 1.6 個人記憶
|
|
762
818
|
h(
|
|
763
819
|
Card,
|
|
764
820
|
{
|
|
765
|
-
id: sectionDomId('memory'),
|
|
821
|
+
id: sectionDomId('memory'), order: 7,
|
|
766
822
|
title: '個人記憶',
|
|
767
823
|
desc: '每位使用者分開。角色覺得重要的事會自己記下來;有期限的到期自動刪,每天也會請角色自己整理一次。',
|
|
768
824
|
right: h(Btn, { onClick: () => loadMemory(), disabled: !!busy }, busy === 'memory-list' ? '讀取中…' : '重新整理'),
|
|
@@ -808,7 +864,7 @@ window.__ModuleLoader__.load({
|
|
|
808
864
|
// 2. LINE 憑證
|
|
809
865
|
h(
|
|
810
866
|
Card,
|
|
811
|
-
{ 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 取得。貼上後可按「驗證」確認。' },
|
|
812
868
|
h(
|
|
813
869
|
'div',
|
|
814
870
|
{ style: S.note },
|
|
@@ -854,7 +910,7 @@ window.__ModuleLoader__.load({
|
|
|
854
910
|
// 3. Cloudflare Tunnel
|
|
855
911
|
h(
|
|
856
912
|
Card,
|
|
857
|
-
{ 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 模式可固定網域。' },
|
|
858
914
|
h('div', { style: S.grid },
|
|
859
915
|
h(Field, { label: '模式' },
|
|
860
916
|
h('select', {
|
|
@@ -892,10 +948,70 @@ window.__ModuleLoader__.load({
|
|
|
892
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 補上讀取權限)。'),
|
|
893
949
|
),
|
|
894
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
|
+
|
|
895
1011
|
// 4. 人設
|
|
896
1012
|
h(
|
|
897
1013
|
Card,
|
|
898
|
-
{ id: sectionDomId('persona'), title: '回覆人設', desc: 'LINE 的回覆會套用這裡選的人設。來源有兩種:大肥魚插件(有安裝時)、插件自己的自訂人設(不依賴任何外掛)。' },
|
|
1014
|
+
{ id: sectionDomId('persona'), order: 5, title: '回覆人設', desc: 'LINE 的回覆會套用這裡選的人設。來源有兩種:大肥魚插件(有安裝時)、插件自己的自訂人設(不依賴任何外掛)。' },
|
|
899
1015
|
persona.warning ? h('div', { style: S.warnBox }, persona.warning) : null,
|
|
900
1016
|
persona.error ? h('div', { style: S.warnBox }, `讀不到大肥魚插件:${persona.error}`) : null,
|
|
901
1017
|
h('div', { style: S.grid },
|
|
@@ -956,7 +1072,7 @@ window.__ModuleLoader__.load({
|
|
|
956
1072
|
// 4b. 貼吧學習同步
|
|
957
1073
|
h(
|
|
958
1074
|
Card,
|
|
959
|
-
{ id: sectionDomId('sync'), title: '貼吧學習同步', desc: '把「刷貼吧」每天演化學到的語氣樣本,寫回上方人設的「## 貼吧學來的口氣」段落,並重載大肥魚插件讓它立即生效。' },
|
|
1075
|
+
{ id: sectionDomId('sync'), order: 6, title: '貼吧學習同步', desc: '把「刷貼吧」每天演化學到的語氣樣本,寫回上方人設的「## 貼吧學來的口氣」段落,並重載大肥魚插件讓它立即生效。' },
|
|
960
1076
|
h(Toggle, { checked: syncCfg.enabled !== false, onChange: (value) => update({ sync: { enabled: value } }), label: '開啟自動同步', hint: '每小時檢查一次;貼吧人設版本有變才寫入(寫入前會備份 personas.json)' }),
|
|
961
1077
|
h('div', { style: S.grid },
|
|
962
1078
|
h(Field, { label: '同步到哪個人設' },
|
|
@@ -986,7 +1102,7 @@ window.__ModuleLoader__.load({
|
|
|
986
1102
|
// 5. 主動傳訊
|
|
987
1103
|
h(
|
|
988
1104
|
Card,
|
|
989
|
-
{ id: sectionDomId('nudge'), title: '主動傳訊(大肥魚自己找人聊天)', desc: '由插件內建排程器執行:每隔幾分鐘擲骰子,符合條件就請 Agent 主動傳一句;不適合時 Agent 可回 SKIP。' },
|
|
1105
|
+
{ id: sectionDomId('nudge'), order: 9, title: '主動傳訊(大肥魚自己找人聊天)', desc: '由插件內建排程器執行:每隔幾分鐘擲骰子,符合條件就請 Agent 主動傳一句;不適合時 Agent 可回 SKIP。' },
|
|
990
1106
|
h(Toggle, { checked: !!nudgeCfg.enabled, onChange: (value) => update({ nudge: { enabled: value } }), label: '開啟主動傳訊' }),
|
|
991
1107
|
h('div', { style: S.grid },
|
|
992
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 } }) })),
|
|
@@ -998,7 +1114,7 @@ window.__ModuleLoader__.load({
|
|
|
998
1114
|
h(Field, { label: '安靜開始(HH:MM)' }, h(Text, { value: nudgeCfg.quietStart, onChange: (value) => update({ nudge: { quietStart: value } }), placeholder: '00:30' })),
|
|
999
1115
|
h(Field, { label: '安靜結束(HH:MM)' }, h(Text, { value: nudgeCfg.quietEnd, onChange: (value) => update({ nudge: { quietEnd: value } }), placeholder: '09:30' })),
|
|
1000
1116
|
),
|
|
1001
|
-
h(Field, { label: '
|
|
1117
|
+
h(Field, { label: '指定對象', hint: '留空=所有已聊過的使用者中挑最久沒互動的;直接點選要傳給誰' }, h(UserChips, { users: status.users, value: nudgeCfg.targetUserIds, onChange: (ids) => update({ nudge: { targetUserIds: ids } }) })),
|
|
1002
1118
|
h(Field, { label: '主動傳訊提示詞(給 Agent 的指示)', hint: '留空=用內建預設(自然、簡短、不提系統、不重複、不適合就 SKIP)' }, h(Area, { value: nudgeCfg.prompt, onChange: (value) => update({ nudge: { prompt: value } }), rows: 4 })),
|
|
1003
1119
|
h('div', { style: S.row },
|
|
1004
1120
|
h(Btn, { onClick: () => act('nudge-test', { force: true }), disabled: !!busy, tone: 'primary' }, busy === 'nudge-test' ? '試跑中…' : '立即試傳一則'),
|
|
@@ -1019,7 +1135,7 @@ window.__ModuleLoader__.load({
|
|
|
1019
1135
|
// 6. 使用者
|
|
1020
1136
|
h(
|
|
1021
1137
|
Card,
|
|
1022
|
-
{ id: sectionDomId('users'), title: '使用者', desc: '與 bot 互動過的 LINE 使用者。每個人有自己的 DSH session 與工作資料夾(記憶、檔案不互通),人設也可以個別指定。' },
|
|
1138
|
+
{ id: sectionDomId('users'), order: 10, title: '使用者', desc: '與 bot 互動過的 LINE 使用者。每個人有自己的 DSH session 與工作資料夾(記憶、檔案不互通),人設也可以個別指定。' },
|
|
1023
1139
|
(status.users || []).length === 0
|
|
1024
1140
|
? h('div', { style: S.hint }, '還沒有人傳訊息給 bot。')
|
|
1025
1141
|
: h('table', { style: S.table },
|
|
@@ -1055,7 +1171,7 @@ window.__ModuleLoader__.load({
|
|
|
1055
1171
|
// 7. 進階設定
|
|
1056
1172
|
h(
|
|
1057
1173
|
Card,
|
|
1058
|
-
{ id: sectionDomId('advanced'), title: '進階設定', desc: '通常不用改;出問題時可從這裡調整。' },
|
|
1174
|
+
{ id: sectionDomId('advanced'), order: 11, title: '進階設定', desc: '通常不用改;出問題時可從這裡調整。' },
|
|
1059
1175
|
h('div', { style: S.grid },
|
|
1060
1176
|
h(Field, { label: 'Webhook 連接埠', hint: '改完按儲存;伺服器會自動重啟。' }, h(Num, { value: server.port, min: 1024, max: 65535, onChange: (value) => update({ server: { port: value } }) })),
|
|
1061
1177
|
h(Field, { label: '每位使用者獨立工作資料夾', hint: '開啟=每個 LINE 使用者有自己的 cwd(檔案、AGENTS.md、工作區記憶互不相通)' },
|
package/lib/config.mjs
CHANGED
|
@@ -99,6 +99,7 @@ export const DEFAULT_CONFIG = {
|
|
|
99
99
|
threshold: 10,
|
|
100
100
|
ask: true,
|
|
101
101
|
cooldownHours: 12,
|
|
102
|
+
targetUserIds: [],
|
|
102
103
|
},
|
|
103
104
|
memory: {
|
|
104
105
|
// 每位使用者各自的個人記憶:agent 覺得重要就自己記,有期限的到期自動刪。
|
|
@@ -221,6 +222,7 @@ export function sanitizePatch(current, patch) {
|
|
|
221
222
|
balance.ask = Boolean(balance.ask)
|
|
222
223
|
balance.threshold = clampFloat(balance.threshold, 0, 100000, 10)
|
|
223
224
|
balance.cooldownHours = clampInt(balance.cooldownHours, 1, 168, 12)
|
|
225
|
+
balance.targetUserIds = safeArray(balance.targetUserIds)
|
|
224
226
|
|
|
225
227
|
const memory = next.memory || (next.memory = {})
|
|
226
228
|
memory.enabled = Boolean(memory.enabled)
|
package/lib/host.mjs
CHANGED
|
@@ -32,9 +32,9 @@ const { createLocalPersonas, LOCAL_PREFIX } = localPersonasMod
|
|
|
32
32
|
const { buildTiebaContext } = tiebaContextMod
|
|
33
33
|
const { createBalanceMonitor } = balanceMod
|
|
34
34
|
const { createMemoryStore } = memoryMod
|
|
35
|
-
const { chunkText, clip, inQuietWindow, jsonFail, jsonOk, maskSecret, readJsonBody } = utilMod
|
|
35
|
+
const { chunkText, clip, inQuietWindow, jsonFail, jsonOk, maskSecret, readJsonBody, safeArray } = utilMod
|
|
36
36
|
|
|
37
|
-
export const VERSION = '0.
|
|
37
|
+
export const VERSION = '0.4.0'
|
|
38
38
|
|
|
39
39
|
// 環境變數提供的 Cloudflare API Token(方便用 shell 注入、不用把 token 寫進設定檔)。
|
|
40
40
|
// 設定檔裡的 token 優先;兩者都沒有時才用環境變數。
|
|
@@ -170,6 +170,7 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
170
170
|
// ── DeepSeek 餘額 ───────────────────────────────────────────────────────
|
|
171
171
|
const balance = createBalanceMonitor({ ctx, logger })
|
|
172
172
|
let balanceTimer = null
|
|
173
|
+
let balanceNextAt = 0
|
|
173
174
|
const memory = createMemoryStore({ dir, logger, getConfig: () => store.get() })
|
|
174
175
|
let memoryTimer = null
|
|
175
176
|
|
|
@@ -181,9 +182,64 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
181
182
|
return null
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
function modelSelection() {
|
|
186
|
+
const cfg = store.get()
|
|
187
|
+
let fallback = null
|
|
188
|
+
try {
|
|
189
|
+
fallback = ctx.get?.('agentDefaultModel')?.currentSelection?.() || null
|
|
190
|
+
} catch {
|
|
191
|
+
fallback = null
|
|
192
|
+
}
|
|
193
|
+
const custom = Boolean(String(cfg.agent.provider || '').trim() || String(cfg.agent.model || '').trim())
|
|
194
|
+
const provider = String(cfg.agent.provider || fallback?.provider || '').trim()
|
|
195
|
+
const model = String(cfg.agent.model || fallback?.model || '').trim()
|
|
196
|
+
return {
|
|
197
|
+
provider,
|
|
198
|
+
model,
|
|
199
|
+
source: custom ? 'custom' : 'default',
|
|
200
|
+
defaultProvider: fallback?.provider || '',
|
|
201
|
+
defaultModel: fallback?.model || '',
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 只有 DeepSeek 官方帳號(deepseek-account)才查得到餘額;其他來源停用餘額相關功能。
|
|
206
|
+
function balanceApplicable() {
|
|
207
|
+
return modelSelection().provider === 'deepseek-account'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let modelCache = null
|
|
211
|
+
async function modelCatalog({ force = false } = {}) {
|
|
212
|
+
const now = Date.now()
|
|
213
|
+
if (!force && modelCache && now - modelCache.at < 60_000) return modelCache.value
|
|
214
|
+
const llm = ctx.get?.('llm')
|
|
215
|
+
if (!llm || typeof llm.listProviders !== 'function') {
|
|
216
|
+
return { providers: [], current: modelSelection(), error: '這個 profile 沒有 llm 服務,無法列出模型' }
|
|
217
|
+
}
|
|
218
|
+
const providers = []
|
|
219
|
+
try {
|
|
220
|
+
const infos = await llm.listProviders()
|
|
221
|
+
for (const info of infos || []) {
|
|
222
|
+
const entry = { id: info.id, name: info.name || info.id, models: [] }
|
|
223
|
+
try {
|
|
224
|
+
const models = await llm.listModels(info.id)
|
|
225
|
+
entry.models = (models || []).map((item) => ({ id: item.id, name: item.name || item.id }))
|
|
226
|
+
} catch (error) {
|
|
227
|
+
entry.error = errorText(error)
|
|
228
|
+
}
|
|
229
|
+
providers.push(entry)
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return { providers: [], current: modelSelection(), error: errorText(error) }
|
|
233
|
+
}
|
|
234
|
+
const value = { providers, current: modelSelection(), at: now }
|
|
235
|
+
modelCache = { at: now, value }
|
|
236
|
+
return value
|
|
237
|
+
}
|
|
238
|
+
|
|
184
239
|
function balanceContextText() {
|
|
185
240
|
const cfg = store.get()
|
|
186
241
|
if (cfg.balance.enabled === false) return ''
|
|
242
|
+
if (!balanceApplicable()) return ''
|
|
187
243
|
const view = balanceView()
|
|
188
244
|
if (!view) return ''
|
|
189
245
|
const cached = balance.get()
|
|
@@ -238,6 +294,7 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
238
294
|
async function maybeAskForTopUp({ dryRun = true, force = false } = {}) {
|
|
239
295
|
const cfg = store.get()
|
|
240
296
|
if (cfg.balance.enabled === false || cfg.balance.ask === false) return { skipped: 'disabled' }
|
|
297
|
+
if (!balanceApplicable()) return { skipped: 'provider-not-deepseek', provider: modelSelection().provider }
|
|
241
298
|
const view = balanceView()
|
|
242
299
|
if (!view) {
|
|
243
300
|
const cached = balance.get()
|
|
@@ -253,7 +310,8 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
253
310
|
if (!force && lastAskAt && Date.now() - lastAskAt < cooldownMs) return { skipped: 'cooldown', lastAskAt }
|
|
254
311
|
if (!force && inQuietWindow(Date.now(), cfg.nudge.quietStart, cfg.nudge.quietEnd)) return { skipped: 'quiet-hours' }
|
|
255
312
|
const candidates = Object.entries(state.users || {}).filter(([, user]) => user?.sessionId)
|
|
256
|
-
const
|
|
313
|
+
const targetIds = safeArray(cfg.balance?.targetUserIds).length ? safeArray(cfg.balance?.targetUserIds) : safeArray(cfg.nudge?.targetUserIds)
|
|
314
|
+
const preferred = targetIds.find((id) => candidates.some(([key]) => key === id))
|
|
257
315
|
const picked = preferred ? candidates.find(([key]) => key === preferred) : [...candidates].sort((a, b) => (a[1].lastAt || 0) - (b[1].lastAt || 0))[0]
|
|
258
316
|
if (!picked) return { skipped: 'no-user' }
|
|
259
317
|
const [userId, user] = picked
|
|
@@ -285,14 +343,25 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
285
343
|
|
|
286
344
|
function startBalanceWatch() {
|
|
287
345
|
stopBalanceWatch()
|
|
288
|
-
const
|
|
346
|
+
const runCheck = () => {
|
|
347
|
+
balanceNextAt = Date.now() + 30 * 60_000
|
|
348
|
+
if (!balanceApplicable()) return
|
|
289
349
|
refreshBalance()
|
|
290
|
-
.then(() =>
|
|
350
|
+
.then((cache) => {
|
|
351
|
+
if (!cache || cache.status !== 'ready') return null
|
|
352
|
+
return maybeAskForTopUp({ dryRun: false })
|
|
353
|
+
})
|
|
354
|
+
.then((result) => {
|
|
355
|
+
if (!result || !result.skipped) return
|
|
356
|
+
if (['enough', 'disabled', 'provider-not-deepseek'].includes(result.skipped)) return
|
|
357
|
+
logger.info('餘額檢查:這次不提醒(' + result.skipped + ')')
|
|
358
|
+
})
|
|
291
359
|
.catch((error) => logger.warn('餘額檢查失敗:' + errorText(error)))
|
|
292
360
|
}
|
|
293
|
-
const initial = setTimeout(
|
|
361
|
+
const initial = setTimeout(runCheck, 8_000)
|
|
362
|
+
balanceNextAt = Date.now() + 8_000
|
|
294
363
|
initial.unref?.()
|
|
295
|
-
balanceTimer = setInterval(
|
|
364
|
+
balanceTimer = setInterval(runCheck, 30 * 60_000)
|
|
296
365
|
balanceTimer.unref?.()
|
|
297
366
|
}
|
|
298
367
|
|
|
@@ -301,6 +370,7 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
301
370
|
clearInterval(balanceTimer)
|
|
302
371
|
balanceTimer = null
|
|
303
372
|
}
|
|
373
|
+
balanceNextAt = 0
|
|
304
374
|
}
|
|
305
375
|
|
|
306
376
|
// ── 個人記憶:清理與每日整理 ────────────────────────────────────────────
|
|
@@ -1048,17 +1118,22 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
1048
1118
|
persona: personaSummary,
|
|
1049
1119
|
agent: {
|
|
1050
1120
|
...runner.stats(),
|
|
1121
|
+
...modelSelection(),
|
|
1051
1122
|
timeoutMs: cfg.agent.timeoutMs,
|
|
1052
1123
|
lastFinishedAt: runner.stats().lastFinishedAt,
|
|
1053
1124
|
},
|
|
1054
1125
|
nudge: { ...nudge.stats(), config: cfg.nudge },
|
|
1055
1126
|
balance: {
|
|
1056
1127
|
...balance.get(),
|
|
1128
|
+
applicable: balanceApplicable(),
|
|
1129
|
+
provider: modelSelection().provider,
|
|
1057
1130
|
enabled: cfg.balance.enabled !== false,
|
|
1058
1131
|
ask: cfg.balance.ask !== false,
|
|
1059
1132
|
threshold: Number(cfg.balance.threshold) || 0,
|
|
1060
1133
|
cooldownHours: Number(cfg.balance.cooldownHours) || 12,
|
|
1061
1134
|
lastAskAt: Number(state.balance?.lastAskAt) || 0,
|
|
1135
|
+
nextCheckAt: balanceNextAt,
|
|
1136
|
+
targetUserIds: safeArray(cfg.balance?.targetUserIds),
|
|
1062
1137
|
lastAskText: state.balance?.lastAskText || '',
|
|
1063
1138
|
low: Boolean(balanceView() && Number(cfg.balance.threshold) > 0 && balanceView().value.total < Number(cfg.balance.threshold)),
|
|
1064
1139
|
},
|
|
@@ -1228,7 +1303,10 @@ export async function create({ ctx, config: pluginConfig, logger: upstream }) {
|
|
|
1228
1303
|
personaCache.at = 0
|
|
1229
1304
|
return created
|
|
1230
1305
|
}
|
|
1306
|
+
case 'model-catalog':
|
|
1307
|
+
return modelCatalog({ force: payload.force === true })
|
|
1231
1308
|
case 'balance-refresh':
|
|
1309
|
+
if (!balanceApplicable()) return { skipped: 'provider-not-deepseek', provider: modelSelection().provider }
|
|
1232
1310
|
return refreshBalance()
|
|
1233
1311
|
case 'balance-ask':
|
|
1234
1312
|
return maybeAskForTopUp({ dryRun: payload.dryRun !== false, force: payload.force === true })
|
package/lib/tieba-sync.mjs
CHANGED
|
@@ -70,6 +70,11 @@ export function buildSection(tieba, date = new Date()) {
|
|
|
70
70
|
return `${lines.join('\n')}\n\n`
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// 只比較內容:標題裡的時間戳每次都會變,不能拿它判斷「語氣有變化」。
|
|
74
|
+
function sansStamp(text) {
|
|
75
|
+
return String(text).replace(/(##\s*貼吧學來的口氣(自動同步 )[^)]*())/, '$1…$2')
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
function findSection(text) {
|
|
74
79
|
const heading = /^##\s*貼吧學來的口氣[^\n]*$/m.exec(text)
|
|
75
80
|
if (!heading) return null
|
|
@@ -174,7 +179,7 @@ export function createTiebaSync({ ctx, store, logger }) {
|
|
|
174
179
|
const oldText = String(target.text || '')
|
|
175
180
|
let nextText = applySection(oldText, buildSection(tieba))
|
|
176
181
|
if (cfg.syncTraits !== false) nextText = applyTraits(nextText, tieba.traits)
|
|
177
|
-
const changed = nextText !== oldText
|
|
182
|
+
const changed = sansStamp(nextText) !== sansStamp(oldText)
|
|
178
183
|
const stateSync = store.getState().sync || {}
|
|
179
184
|
if (!changed && stateSync.version === tieba.version) {
|
|
180
185
|
return { skipped: 'unchanged', version: tieba.version, targetId }
|
|
@@ -195,17 +200,9 @@ export function createTiebaSync({ ctx, store, logger }) {
|
|
|
195
200
|
}
|
|
196
201
|
writeJsonAtomic(dafeiyuStorePath(), next)
|
|
197
202
|
logger.info(`已把刷貼吧學到的語氣寫回人設「${target.name || targetId}」(版本 ${tieba.version})`)
|
|
198
|
-
if (cfg.reloadPersona !== false) {
|
|
199
|
-
try {
|
|
200
|
-
reload = await reloadPersonaPlugin(ctx)
|
|
201
|
-
if (!reload.ok) logger.warn(`重載大肥魚外掛失敗:${reload.error}`)
|
|
202
|
-
} catch (error) {
|
|
203
|
-
reload = { ok: false, error: String(error?.message || error) }
|
|
204
|
-
logger.warn(`重載大肥魚外掛失敗:${reload.error}`)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
203
|
}
|
|
208
204
|
|
|
205
|
+
// 先記帳再重載:重載要幾百毫秒,中途若插件重啟,新實例讀到的 state 才不會還停在舊的 at 而立刻再跑一次。
|
|
209
206
|
store.updateState((state) => ({
|
|
210
207
|
...state,
|
|
211
208
|
sync: {
|
|
@@ -215,10 +212,21 @@ export function createTiebaSync({ ctx, store, logger }) {
|
|
|
215
212
|
samples: tieba.samples.length,
|
|
216
213
|
changed,
|
|
217
214
|
backupPath,
|
|
218
|
-
reloadOk:
|
|
215
|
+
reloadOk: null,
|
|
219
216
|
error: '',
|
|
220
217
|
},
|
|
221
218
|
}))
|
|
219
|
+
|
|
220
|
+
if (changed && cfg.reloadPersona !== false) {
|
|
221
|
+
try {
|
|
222
|
+
reload = await reloadPersonaPlugin(ctx)
|
|
223
|
+
if (!reload.ok) logger.warn(`重載大肥魚外掛失敗:${reload.error}`)
|
|
224
|
+
} catch (error) {
|
|
225
|
+
reload = { ok: false, error: String(error?.message || error) }
|
|
226
|
+
logger.warn(`重載大肥魚外掛失敗:${reload.error}`)
|
|
227
|
+
}
|
|
228
|
+
store.updateState((state) => ({ ...state, sync: { ...(state.sync || {}), reloadOk: Boolean(reload?.ok) } }))
|
|
229
|
+
}
|
|
222
230
|
return {
|
|
223
231
|
at,
|
|
224
232
|
version: tieba.version,
|