dsh-miniflux 0.0.0-stage → 0.9.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the dsh-miniflux authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,119 @@
1
- # Temporary Holding Version
1
+ # dsh-miniflux
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Miniflux 插件(DSH Web GUI):AI 写早晚报、挑精彩文章收藏、按 cron 自动生成三种 EPUB。
4
+ Settings live in the plugin settings page; every field is bilingual (中文 / English).
5
+
6
+ ## 安装 / Install
7
+
8
+ ```bash
9
+ dsh plugin --profile <name> add link:/path/to/dsh-miniflux
10
+ # 重启 dsh web 后生效 / restart the dsh web process to mount
11
+ ```
12
+
13
+ 设置页在「设置 → 插件 → Miniflux」。改完即时生效(`volatile` 字段热应用),不用重启。
14
+ Settings → Plugins → Miniflux. Changes apply live, no restart.
15
+
16
+
17
+ ## 依赖 / Requirements
18
+
19
+ - DSH `>=0.2.0-rc.2`
20
+ - 系统有 `ffmpeg`(EPUB 内嵌图片时把 WebP/AVIF 转 JPEG;没有也能跑,只是这类图会被跳过)
21
+ - 抓外网图片需要代理时,在设置里填 `imageProxy`(例如 `http://user:pass@host:port`)
22
+
23
+ ## 安装 / Install
24
+
25
+ **从 npm 安装(发布后可用)**
26
+
27
+ ```bash
28
+ dsh plugin --profile <name> add dsh-miniflux
29
+ ```
30
+
31
+ **从 GitHub 安装(当前可用)**
32
+
33
+ ```bash
34
+ git clone https://github.com/niupkk/dsh-miniflux.git
35
+ cd dsh-miniflux && npm install
36
+ dsh plugin --profile <name> add link:$PWD
37
+ ```
38
+
39
+ **从仓库链接安装(开发态)**
40
+
41
+ ```bash
42
+ git clone <repo> dsh-miniflux
43
+ cd dsh-miniflux && npm install # 装上 @deepseek-ai/schemastery
44
+ dsh plugin --profile <name> add link:$PWD
45
+ ```
46
+
47
+ `link:` 安装不会自动装依赖,所以要先 `npm install` 一次;如果改用 npm 上发布的包,`dsh plugin add dsh-miniflux` 会自己装。
48
+
49
+ 装完重启一次 dsh web,左侧栏就会出现 **Miniflux** 图标。
50
+
51
+
52
+ ### 自选源 EPUB(不经过模型)
53
+
54
+ 按设置的源列表,把最近的文章打包成一本电子书:
55
+
56
+ - **纯本地拼装**,不调用任何模型,所以零 token 消耗、速度只取决于图片下载
57
+ - **按 RSS 源分组**:一本书里一个源一章,文章在它下面按时间倒序
58
+ - **双层目录**:阅读器的目录面板是「源 → 文章」的嵌套结构;书开头还有一页可见的目录,能先总览再读
59
+ - 篇数上限由 `feedEpubMax` 控制(默认 120,最大 500)
60
+ - 生成完成后,收录的条目会标为已读
61
+
62
+ ## 设置项 / Settings
63
+
64
+ | 字段 Field | 说明 Description |
65
+ | --- | --- |
66
+ | `enabled` | 总开关 / master switch |
67
+ | `minifluxUrl` / `minifluxToken` | 实例地址与 API token / base URL and API token |
68
+ | `aiSource` | `dsh` = 用 DSH 里已配置的模型(不用再填 key);`custom` = 自己的 OpenAI 兼容接口 / use DSH-configured models, or your own endpoint |
69
+ | `dshProvider` / `dshModel` | DSH 模型的 provider 与 model id / provider and model id from Settings → Models |
70
+ | `aiBaseUrl` / `aiApiKey` / `aiModel` | `custom` 模式用 / used when `aiSource=custom` |
71
+ | `digestCron` | 早晚报的 cron,如 `30 7,18 * * *` / cron for digests |
72
+ | `picksCron` | 「精选」EPUB 的 cron,留空则关闭 / cron for the picks EPUB |
73
+ | `feedEpubCron` | 「自选源」EPUB 的 cron,留空则关闭 / cron for the per-feed EPUB |
74
+ | `lookbackHours` / `digestMaxItems` / `aiMaxPicks` | 回看时长 / 简报条数 / 每次最多挑几条 |
75
+ | `epubFeedNames` | 「自选源」收录哪些源(逗号分隔的源名或 id,空=全部)/ feeds for the per-feed EPUB |
76
+ | `epubDir` | EPUB 保存目录(绝对路径,空=`$DSH_HOME/miniflux-epub`)/ output directory |
77
+ | `withImages` | EPUB 是否内嵌图片 / embed images |
78
+ | `imageProxy` | 抓图代理,如 `http://user:pass@host:port` / proxy for image downloads |
79
+ | `autoStar` | AI 挑中的文章自动加星 / star the model's picks |
80
+ | `language` | 简报语言 / digest language |
81
+
82
+ ## 三种 EPUB
83
+
84
+ | 任务 | 触发器 | 内容 |
85
+ | --- | --- | --- |
86
+ | 早晚报 | `digestCron` | AI 写的分类简报 |
87
+ | 精选 | `picksCron` | AI 挑的 3–5 篇,带挑选理由,并加星 |
88
+ | 自选源 | `feedEpubCron` | `epubFeedNames` 里那些源最近的文章 |
89
+
90
+ cron 是标准 5 字段(分 时 日 月 周),按宿主机本地时区。
91
+ Express cron (min hour day month weekday), host local time.
92
+
93
+ ## 技术要点
94
+
95
+ - **图片**:下载后用魔数嗅探真实格式;WebP/AVIF 交给系统 `ffmpeg` 转 JPEG(EPUB 阅读器普遍不支持 WebP);配了 `imageProxy` 时用 `curl` 走代理。
96
+ - **ZIP**:自己实现的最小 ZIP 写入器,不引第三方依赖;mimetype 首位且不压缩。
97
+ - **模型**:`dsh` 模式走 `ctx.llm.stream({ provider, model, system, messages })`,和 DSH 的模型设置共用;`custom` 模式走 `POST {base}/chat/completions`。
98
+ - **判断**:没有任何本地打分公式,重要性完全由模型按提示词(影响面/时效性/信息增量/一手信源)判断。
99
+
100
+ ## agent 工具
101
+
102
+ `miniflux_feeds` · `miniflux_search` · `miniflux_read` · `miniflux_digest` · `miniflux_epub` · `miniflux_feed_epub` · `miniflux_star`
103
+
104
+ ## 路由
105
+
106
+ `GET /api/dsh-miniflux/status` — 连通性与生效配置
107
+ `POST /api/dsh-miniflux/generate?what=digest|feed-epub&kind=morning|evening` — 立刻生成
108
+
109
+ ---
110
+
111
+ ## 如果这个插件帮到你 / If this saved you some time
112
+
113
+ 花了不少 token,有帮助的话帮帮忙吧。
114
+
115
+ It took a fair number of tokens to build this. If it helped you, a small tip is appreciated.
116
+
117
+ <p align="center">
118
+ <img src="assets/support.jpg" alt="收款码 / payment QR code" width="260">
119
+ </p>
Binary file
@@ -0,0 +1,5 @@
1
+ # dsh-miniflux bundle patch: 在 web profile 的插件名单里插入本插件。
2
+ # 用 `dsh plugin --profile <name> add link:<仓库目录>` 激活。
3
+ - insert:
4
+ - id: miniflux
5
+ name: dsh-miniflux
package/lib/client.js ADDED
@@ -0,0 +1,374 @@
1
+ /**
2
+ * dsh-miniflux — 浏览器半身。
3
+ *
4
+ * 注册一个左侧栏入口(sidebar.panellist)和一个主区域页面(main),
5
+ * 页面上是 Miniflux 连接参数、模型选择、cron 与 EPUB 设置,保存后立刻生效。
6
+ *
7
+ * 任何异常都在本地吞掉,不能影响宿主界面。
8
+ */
9
+ window.__ModuleLoader__.load({
10
+ id: 'dsh-miniflux',
11
+ factory: (require) => {
12
+ var module = { exports: {} }
13
+ var exports = module.exports
14
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
15
+
16
+ const jsx = require('react/jsx-runtime')
17
+ const { useState, useEffect, useCallback } = require('react')
18
+
19
+ /** 面板入口图标:外壳靠这个 data 属性把图标和面板关联起来。 */
20
+ function MinifluxIcon({ size }) {
21
+ return jsx.jsxs('svg', {
22
+ 'data-dsh-panel-entry': 'miniflux',
23
+ viewBox: '0 0 16 16',
24
+ width: size,
25
+ height: size,
26
+ fill: 'none',
27
+ stroke: 'currentColor',
28
+ strokeWidth: '1.5',
29
+ strokeLinecap: 'round',
30
+ strokeLinejoin: 'round',
31
+ 'aria-hidden': 'true',
32
+ children: [
33
+ jsx.jsx('rect', { x: '1.75', y: '2.25', width: '12.5', height: '11.5', rx: '1.75' }),
34
+ jsx.jsx('path', { d: 'M4.5 5.5h5M4.5 8h7M4.5 10.5h4' }),
35
+ ],
36
+ })
37
+ }
38
+
39
+ const S = {
40
+ page: { padding: '18px 22px', maxWidth: 720, fontFamily: 'inherit', fontSize: 13 },
41
+ title: { fontSize: 16, fontWeight: 600, margin: '0 0 4px' },
42
+ hint: { opacity: 0.65, margin: '0 0 18px', lineHeight: 1.5 },
43
+ group: { margin: '0 0 18px', padding: '12px 14px', border: '1px solid rgba(128,128,128,.25)', borderRadius: 10 },
44
+ legend: { fontSize: 12, fontWeight: 600, opacity: 0.8, margin: '0 0 10px' },
45
+ row: { display: 'grid', gridTemplateColumns: '170px 1fr', gap: 10, alignItems: 'center', margin: '0 0 8px' },
46
+ label: { fontSize: 12, opacity: 0.85, lineHeight: 1.35 },
47
+ input: { width: '100%', padding: '6px 8px', fontSize: 12, borderRadius: 6, border: '1px solid rgba(128,128,128,.35)', background: 'transparent', color: 'inherit' },
48
+ actions: { display: 'flex', gap: 8, flexWrap: 'wrap', margin: '4px 0 0' },
49
+ button: { padding: '6px 12px', fontSize: 12, borderRadius: 6, border: '1px solid rgba(128,128,128,.35)', background: 'transparent', color: 'inherit', cursor: 'pointer' },
50
+ status: { fontSize: 12, lineHeight: 1.6, whiteSpace: 'pre-wrap', margin: '10px 0 0', opacity: 0.85 },
51
+ ok: { color: '#2e7d32' },
52
+ bad: { color: '#c62828' },
53
+ }
54
+
55
+ const FIELDS = [
56
+ { key: 'minifluxUrl', label: 'Miniflux 地址 / URL', placeholder: 'http://192.168.1.10:8080' },
57
+ { key: 'minifluxToken', label: 'API Token', placeholder: 'Miniflux → 设置 → API 密钥', secret: true },
58
+ ]
59
+
60
+ const ADVANCED = [
61
+ { key: 'epubFeedNames', label: '自选源 / Feed names', placeholder: '留空=全部,逗号分隔' },
62
+ { key: 'epubDir', label: 'EPUB 目录 / Output dir', placeholder: '留空 = $DSH_HOME/miniflux-epub' },
63
+ { key: 'digestCron', label: '日报 cron / Daily cron', placeholder: '30 7,18 * * *' },
64
+ { key: 'picksCron', label: '精选 cron / Picks cron', placeholder: '45 7,18 * * *' },
65
+ { key: 'feedEpubCron', label: '自选源 cron / Feed EPUB cron', placeholder: '0 8 * * *' },
66
+ { key: 'modelTimeoutSec', label: '模型超时(秒) / Model timeout', placeholder: '600' },
67
+ { key: 'aiBatchSize', label: 'AI 每批篇数 / AI batch size', placeholder: '12' },
68
+ { key: 'maxCandidates', label: '日报候选上限 / Digest candidates', placeholder: '200' },
69
+ { key: 'picksCandidates', label: '精选取未读篇数 / Picks candidates', placeholder: '200' },
70
+ { key: 'feedEpubMax', label: '自选源篇数上限 / Feed EPUB max', placeholder: '120' },
71
+ { key: 'imageProxy', label: '抓图代理 / Image proxy', placeholder: 'http://user:pass@host:port' },
72
+ { key: 'language', label: '语言 / Language', placeholder: '中文' },
73
+ ]
74
+
75
+ function Field({ field, value, onChange }) {
76
+ return jsx.jsxs('div', {
77
+ style: S.row,
78
+ children: [
79
+ jsx.jsx('span', { style: S.label, children: field.label }),
80
+ jsx.jsx('input', {
81
+ style: S.input,
82
+ type: field.secret ? 'password' : 'text',
83
+ placeholder: field.placeholder || '',
84
+ value: value ?? '',
85
+ onChange: (event) => onChange(field.key, event.target.value),
86
+ }),
87
+ ],
88
+ })
89
+ }
90
+
91
+ function Toggle({ label, checked, onChange }) {
92
+ return jsx.jsxs('div', {
93
+ style: S.row,
94
+ children: [
95
+ jsx.jsx('span', { style: S.label, children: label }),
96
+ jsx.jsx('input', { type: 'checkbox', checked: Boolean(checked), onChange: (event) => onChange(event.target.checked) }),
97
+ ],
98
+ })
99
+ }
100
+
101
+ function MinifluxPage() {
102
+ const [config, setConfig] = useState(null)
103
+ const [providers, setProviders] = useState([])
104
+ const [status, setStatus] = useState(null)
105
+ const [files, setFiles] = useState({ dir: '', files: [] })
106
+ const [busy, setBusy] = useState('')
107
+ const [message, setMessage] = useState('')
108
+
109
+ const load = useCallback(async () => {
110
+ try {
111
+ const res = await fetch('/api/dsh-miniflux/config', { headers: { Accept: 'application/json' } })
112
+ const data = await res.json()
113
+ setConfig(data.config || {})
114
+ } catch (error) {
115
+ setMessage(`读取配置失败 / failed to load config: ${error?.message || error}`)
116
+ }
117
+ try {
118
+ const res = await fetch('/api/dsh-miniflux/models', { headers: { Accept: 'application/json' } })
119
+ const data = await res.json()
120
+ setProviders(Array.isArray(data.providers) ? data.providers : [])
121
+ } catch { setProviders([]) }
122
+ try {
123
+ const res = await fetch('/api/dsh-miniflux/status', { headers: { Accept: 'application/json' } })
124
+ setStatus(await res.json())
125
+ } catch { setStatus(null) }
126
+ try {
127
+ const res = await fetch('/api/dsh-miniflux/files', { headers: { Accept: 'application/json' } })
128
+ const data = await res.json()
129
+ setFiles({ dir: data.dir || '', files: Array.isArray(data.files) ? data.files : [] })
130
+ } catch { setFiles({ dir: '', files: [] }) }
131
+ }, [])
132
+
133
+ useEffect(() => { void load() }, [load])
134
+
135
+ const setField = (key, value) => setConfig((current) => ({ ...(current || {}), [key]: value }))
136
+
137
+ const save = async () => {
138
+ setBusy('save'); setMessage('')
139
+ try {
140
+ const res = await fetch('/api/dsh-miniflux/config', {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify(config || {}),
144
+ })
145
+ const data = await res.json()
146
+ if (data.ok) { setConfig(data.config); setMessage('已保存 / saved') } else setMessage(`保存失败 / save failed: ${data.message || ''}`)
147
+ } catch (error) {
148
+ setMessage(`保存失败 / save failed: ${error?.message || error}`)
149
+ } finally { setBusy('') }
150
+ }
151
+
152
+ const run = async (what) => {
153
+ setBusy(what); setMessage('任务已提交,正在后台生成…(模型慢的话可能要几分钟)')
154
+ try {
155
+ const res = await fetch(`/api/dsh-miniflux/generate?what=${what}`, { method: 'POST' })
156
+ const data = await res.json()
157
+ if (!data.ok) { setMessage(`无法开始 / cannot start: ${data.message || ''}`); setBusy(''); return }
158
+ } catch (error) {
159
+ setMessage(`失败 / failed: ${error?.message || error}`); setBusy(''); return
160
+ }
161
+ // 轮询后台任务
162
+ for (;;) {
163
+ await new Promise((resolve) => setTimeout(resolve, 3000))
164
+ let state
165
+ try {
166
+ state = await (await fetch('/api/dsh-miniflux/job')).json()
167
+ } catch { continue }
168
+ if (state.state === 'running') { setMessage(`生成中… 已用 ${state.elapsedSec || 0} 秒(可切走,跑完回来点刷新)/ running`); continue }
169
+ if (state.state === 'done') {
170
+ const result = state.result || {}
171
+ const target = result.digestEpub || result.picksEpub || result.epub || result.title || ''
172
+ setMessage(result.ok
173
+ ? `完成 / done(${state.elapsedSec} 秒): ${target}${result.skippedScreened ? ` · 跳过已筛 ${result.skippedScreened} 篇` : ''}${result.starred?.length ? ` · 收藏 ${result.starred.length} 条` : ''}${result.read ? ` · 已读 ${result.read} 条` : ''}`
174
+ : `失败 / failed: ${result.message || ''}`)
175
+ } else if (state.state === 'failed') {
176
+ setMessage(`失败 / failed: ${state.error || ''}`)
177
+ } else {
178
+ setMessage('空闲 / idle')
179
+ }
180
+ setBusy('')
181
+ return
182
+ }
183
+ }
184
+
185
+ const testModel = async () => {
186
+ setBusy('test'); setMessage('')
187
+ try {
188
+ const res = await fetch('/api/dsh-miniflux/test-model', { method: 'POST' })
189
+ const data = await res.json()
190
+ if (data.ok) setMessage(`模型可用 / model ok: ${data.provider} / ${data.model} → “${data.reply}”`)
191
+ else setMessage(`模型调用失败 / model failed: ${data.provider || ''} / ${data.model || ''} → ${data.message || ''}`)
192
+ } catch (error) {
193
+ setMessage(`测试失败 / test failed: ${error?.message || error}`)
194
+ } finally { setBusy('') }
195
+ }
196
+
197
+ const chosenProvider = (config && config.dshProvider) || ''
198
+ const providerEntry = providers.find((item) => item.id === chosenProvider)
199
+ const modelOptions = providerEntry?.models || []
200
+
201
+ return jsx.jsxs('div', {
202
+ style: S.page,
203
+ children: [
204
+ jsx.jsx('h2', { style: S.title, children: 'Miniflux' }),
205
+ jsx.jsx('p', {
206
+ style: S.hint,
207
+ children: 'AI 日报、精选与自选源 EPUB。连接参数和模型都在这里填,保存后立即生效(无需重启)。 / Fill in the connection and model here; changes apply immediately.',
208
+ }),
209
+
210
+ jsx.jsxs('div', {
211
+ style: S.group,
212
+ children: [
213
+ jsx.jsx('div', { style: S.legend, children: '连接 / Connection' }),
214
+ ...FIELDS.map((field) => jsx.jsx(Field, {
215
+ field, value: config?.[field.key], onChange: setField,
216
+ }, field.key)),
217
+ ],
218
+ }),
219
+
220
+ jsx.jsxs('div', {
221
+ style: S.group,
222
+ children: [
223
+ jsx.jsx('div', { style: S.legend, children: '模型 / Model' }),
224
+ jsx.jsxs('div', {
225
+ style: S.row,
226
+ children: [
227
+ jsx.jsx('span', { style: S.label, children: '来源 / Source' }),
228
+ jsx.jsxs('select', {
229
+ style: S.input,
230
+ value: config?.aiSource || 'dsh',
231
+ onChange: (event) => setField('aiSource', event.target.value),
232
+ children: [
233
+ jsx.jsx('option', { value: 'dsh', children: 'DSH 已连接的模型 / models connected to DSH' }),
234
+ jsx.jsx('option', { value: 'custom', children: '自定义接口 / custom OpenAI-compatible endpoint' }),
235
+ ],
236
+ }),
237
+ ],
238
+ }),
239
+ ...(config?.aiSource === 'custom'
240
+ ? [
241
+ jsx.jsx(Field, { field: { key: 'aiBaseUrl', label: '接口地址 / Base URL', placeholder: 'https://api.deepseek.com/v1' }, value: config?.aiBaseUrl, onChange: setField }, 'aiBaseUrl'),
242
+ jsx.jsx(Field, { field: { key: 'aiApiKey', label: 'API Key', placeholder: 'sk-...', secret: true }, value: config?.aiApiKey, onChange: setField }, 'aiApiKey'),
243
+ jsx.jsx(Field, { field: { key: 'aiModel', label: '模型名 / Model id', placeholder: 'deepseek-chat' }, value: config?.aiModel, onChange: setField }, 'aiModel'),
244
+ ]
245
+ : [
246
+ jsx.jsxs('div', {
247
+ style: S.row,
248
+ children: [
249
+ jsx.jsx('span', { style: S.label, children: '服务商 / Provider' }),
250
+ jsx.jsxs('select', {
251
+ style: S.input,
252
+ value: chosenProvider,
253
+ onChange: (event) => {
254
+ const next = event.target.value
255
+ const entry = providers.find((item) => item.id === next)
256
+ setConfig((current) => ({
257
+ ...(current || {}),
258
+ dshProvider: next,
259
+ dshModel: entry?.models?.[0] || current?.dshModel || '',
260
+ }))
261
+ },
262
+ children: [
263
+ jsx.jsx('option', { value: '', children: providers.length ? '请选择 / select' : '(模型列表不可用,可手填)/ list unavailable, type below' }),
264
+ ...providers.map((item) => jsx.jsx('option', { value: item.id, children: item.name }, item.id)),
265
+ ],
266
+ }),
267
+ ],
268
+ }),
269
+ modelOptions.length
270
+ ? jsx.jsxs('div', {
271
+ style: S.row,
272
+ children: [
273
+ jsx.jsx('span', { style: S.label, children: '模型 / Model' }),
274
+ jsx.jsxs('select', {
275
+ style: S.input,
276
+ value: config?.dshModel || '',
277
+ onChange: (event) => setField('dshModel', event.target.value),
278
+ children: modelOptions.map((id) => jsx.jsx('option', { value: id, children: id }, id)),
279
+ }),
280
+ ],
281
+ })
282
+ : jsx.jsx(Field, { field: { key: 'dshModel', label: '模型 / Model', placeholder: 'deepseek-flash' }, value: config?.dshModel, onChange: setField }, 'dshModel'),
283
+ ]),
284
+ ],
285
+ }),
286
+
287
+ jsx.jsxs('div', {
288
+ style: S.group,
289
+ children: [
290
+ jsx.jsx('div', { style: S.legend, children: 'AI 能力、定时与产出 / AI, schedule & output' }),
291
+ ...ADVANCED.map((field) => jsx.jsx(Field, { field, value: config?.[field.key], onChange: setField }, field.key)),
292
+ jsx.jsx(Toggle, { label: '内嵌图片 / Embed images', checked: config?.withImages !== false, onChange: (v) => setField('withImages', v) }),
293
+ jsx.jsx(Toggle, { label: '自动加星 / Auto star picks', checked: config?.autoStar !== false, onChange: (v) => setField('autoStar', v) }),
294
+ jsx.jsx(Toggle, { label: '跳过已筛过的 / Skip screened', checked: config?.picksSkipScreened !== false, onChange: (v) => setField('picksSkipScreened', v) }),
295
+ ],
296
+ }),
297
+
298
+ jsx.jsxs('div', {
299
+ style: S.actions,
300
+ children: [
301
+ jsx.jsx('button', { style: S.button, disabled: busy === 'save', onClick: save, children: busy === 'save' ? '保存中…' : '保存 / Save' }),
302
+ jsx.jsx('button', { style: S.button, disabled: busy === 'digest', onClick: () => run('digest'), children: busy === 'digest' ? '生成中…' : '生成日报 / Daily digest' }),
303
+ jsx.jsx('button', { style: S.button, disabled: busy === 'picks', onClick: () => run('picks'), children: busy === 'picks' ? '生成中…' : '生成精选 / Picks now' }),
304
+ jsx.jsx('button', { style: S.button, disabled: busy === 'feed-epub', onClick: () => run('feed-epub'), children: busy === 'feed-epub' ? '生成中…' : '生成自选源 EPUB / Feed EPUB now' }),
305
+ jsx.jsx('button', { style: S.button, disabled: busy === 'test', onClick: testModel, children: busy === 'test' ? '测试中…' : '测试模型 / Test model' }),
306
+ jsx.jsx('button', {
307
+ style: S.button,
308
+ onClick: async () => {
309
+ setMessage('')
310
+ try {
311
+ const res = await fetch('/api/dsh-miniflux/reset-screened', { method: 'POST' })
312
+ const data = await res.json()
313
+ setMessage(`已清空筛选记录 / ledger cleared: 移除 ${data.removed ?? 0} 条`)
314
+ } catch (error) { setMessage(`清空失败 / failed: ${error?.message || error}`) }
315
+ },
316
+ children: '清空筛选记录 / Clear screened',
317
+ }),
318
+ jsx.jsx('button', { style: S.button, onClick: load, children: '刷新 / Refresh' }),
319
+ ],
320
+ }),
321
+
322
+ message ? jsx.jsx('div', { style: S.status, children: message }) : null,
323
+
324
+ jsx.jsxs('div', {
325
+ style: S.group,
326
+ children: [
327
+ jsx.jsx('div', { style: S.legend, children: '最近生成 / Recent files' }),
328
+ jsx.jsx('div', { style: S.label, children: `目录 / dir: ${files.dir || '(未配置)'}` }),
329
+ ...(files.files.length
330
+ ? files.files.map((file) => jsx.jsx('div', {
331
+ style: S.label,
332
+ children: `${file.name} · ${Math.round(file.bytes / 1024)} KB · ${new Date(file.mtime).toLocaleString()}`,
333
+ }, file.path))
334
+ : [jsx.jsx('div', { style: S.label, children: '(还没有生成过文件)/ no files yet' })]),
335
+ ],
336
+ }),
337
+
338
+ status
339
+ ? jsx.jsxs('div', { style: S.status, children: [
340
+ jsx.jsx('div', { style: status.miniflux?.ok ? S.ok : S.bad, children: status.miniflux?.ok ? `Miniflux 已连接 / connected · ${status.minifluxUrl || ''}` : `Miniflux 未连通 / not connected: ${status.miniflux?.error || ''}` }),
341
+ jsx.jsx('div', { children: `模型 / model: ${status.model || '-'}` }),
342
+ jsx.jsx('div', { children: `EPUB 目录 / dir: ${status.epubDir || '-'}` }),
343
+ ] })
344
+ : null,
345
+ ],
346
+ })
347
+ }
348
+
349
+ const inject = ['slots']
350
+
351
+ function apply(ctx) {
352
+ try {
353
+ const disposers = []
354
+ disposers.push(ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
355
+ name: 'sidebar.panellist',
356
+ id: 'miniflux',
357
+ order: 40,
358
+ label: () => 'Miniflux',
359
+ }, MinifluxIcon)))
360
+ disposers.push(ctx.slots.inject('main', () => ctx.slots.register({
361
+ name: 'main',
362
+ key: 'miniflux',
363
+ }, MinifluxPage)))
364
+ ctx.effect?.(() => () => { for (const dispose of disposers.splice(0)) dispose() }, 'dsh-miniflux: client slots')
365
+ } catch (error) {
366
+ console.error('[dsh-miniflux] client mount failed:', error?.message || error)
367
+ }
368
+ }
369
+
370
+ exports.apply = apply
371
+ exports.inject = inject
372
+ return module.exports
373
+ },
374
+ })