dsh-miniflux 0.0.0-stage → 0.8.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 +21 -0
- package/README.md +93 -2
- package/assets/support.jpg +0 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +373 -0
- package/lib/index.js +1389 -0
- package/package.json +63 -3
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,94 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-miniflux
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
**从仓库链接安装(开发态)**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone <repo> dsh-miniflux
|
|
29
|
+
cd dsh-miniflux && npm install # 装上 @deepseek-ai/schemastery
|
|
30
|
+
dsh plugin --profile <name> add link:$PWD
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`link:` 安装不会自动装依赖,所以要先 `npm install` 一次;如果改用 npm 上发布的包,`dsh plugin add dsh-miniflux` 会自己装。
|
|
34
|
+
|
|
35
|
+
装完重启一次 dsh web,左侧栏就会出现 **Miniflux** 图标。
|
|
36
|
+
|
|
37
|
+
## 设置项 / Settings
|
|
38
|
+
|
|
39
|
+
| 字段 Field | 说明 Description |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `enabled` | 总开关 / master switch |
|
|
42
|
+
| `minifluxUrl` / `minifluxToken` | 实例地址与 API token / base URL and API token |
|
|
43
|
+
| `aiSource` | `dsh` = 用 DSH 里已配置的模型(不用再填 key);`custom` = 自己的 OpenAI 兼容接口 / use DSH-configured models, or your own endpoint |
|
|
44
|
+
| `dshProvider` / `dshModel` | DSH 模型的 provider 与 model id / provider and model id from Settings → Models |
|
|
45
|
+
| `aiBaseUrl` / `aiApiKey` / `aiModel` | `custom` 模式用 / used when `aiSource=custom` |
|
|
46
|
+
| `digestCron` | 早晚报的 cron,如 `30 7,18 * * *` / cron for digests |
|
|
47
|
+
| `picksCron` | 「精选」EPUB 的 cron,留空则关闭 / cron for the picks EPUB |
|
|
48
|
+
| `feedEpubCron` | 「自选源」EPUB 的 cron,留空则关闭 / cron for the per-feed EPUB |
|
|
49
|
+
| `lookbackHours` / `digestMaxItems` / `aiMaxPicks` | 回看时长 / 简报条数 / 每次最多挑几条 |
|
|
50
|
+
| `epubFeedNames` | 「自选源」收录哪些源(逗号分隔的源名或 id,空=全部)/ feeds for the per-feed EPUB |
|
|
51
|
+
| `epubDir` | EPUB 保存目录(绝对路径,空=`$DSH_HOME/miniflux-epub`)/ output directory |
|
|
52
|
+
| `withImages` | EPUB 是否内嵌图片 / embed images |
|
|
53
|
+
| `imageProxy` | 抓图代理,如 `http://user:pass@host:port` / proxy for image downloads |
|
|
54
|
+
| `autoStar` | AI 挑中的文章自动加星 / star the model's picks |
|
|
55
|
+
| `language` | 简报语言 / digest language |
|
|
56
|
+
|
|
57
|
+
## 三种 EPUB
|
|
58
|
+
|
|
59
|
+
| 任务 | 触发器 | 内容 |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| 早晚报 | `digestCron` | AI 写的分类简报 |
|
|
62
|
+
| 精选 | `picksCron` | AI 挑的 3–5 篇,带挑选理由,并加星 |
|
|
63
|
+
| 自选源 | `feedEpubCron` | `epubFeedNames` 里那些源最近的文章 |
|
|
64
|
+
|
|
65
|
+
cron 是标准 5 字段(分 时 日 月 周),按宿主机本地时区。
|
|
66
|
+
Express cron (min hour day month weekday), host local time.
|
|
67
|
+
|
|
68
|
+
## 技术要点
|
|
69
|
+
|
|
70
|
+
- **图片**:下载后用魔数嗅探真实格式;WebP/AVIF 交给系统 `ffmpeg` 转 JPEG(EPUB 阅读器普遍不支持 WebP);配了 `imageProxy` 时用 `curl` 走代理。
|
|
71
|
+
- **ZIP**:自己实现的最小 ZIP 写入器,不引第三方依赖;mimetype 首位且不压缩。
|
|
72
|
+
- **模型**:`dsh` 模式走 `ctx.llm.stream({ provider, model, system, messages })`,和 DSH 的模型设置共用;`custom` 模式走 `POST {base}/chat/completions`。
|
|
73
|
+
- **判断**:没有任何本地打分公式,重要性完全由模型按提示词(影响面/时效性/信息增量/一手信源)判断。
|
|
74
|
+
|
|
75
|
+
## agent 工具
|
|
76
|
+
|
|
77
|
+
`miniflux_feeds` · `miniflux_search` · `miniflux_read` · `miniflux_digest` · `miniflux_epub` · `miniflux_feed_epub` · `miniflux_star`
|
|
78
|
+
|
|
79
|
+
## 路由
|
|
80
|
+
|
|
81
|
+
`GET /api/dsh-miniflux/status` — 连通性与生效配置
|
|
82
|
+
`POST /api/dsh-miniflux/generate?what=digest|feed-epub&kind=morning|evening` — 立刻生成
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 如果这个插件帮到你 / If this saved you some time
|
|
87
|
+
|
|
88
|
+
花了不少 token,有帮助的话帮帮忙吧。
|
|
89
|
+
|
|
90
|
+
It took a fair number of tokens to build this. If it helped you, a small tip is appreciated.
|
|
91
|
+
|
|
92
|
+
<p align="center">
|
|
93
|
+
<img src="assets/support.jpg" alt="收款码 / payment QR code" width="260">
|
|
94
|
+
</p>
|
|
Binary file
|
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
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: 'imageProxy', label: '抓图代理 / Image proxy', placeholder: 'http://user:pass@host:port' },
|
|
71
|
+
{ key: 'language', label: '语言 / Language', placeholder: '中文' },
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
function Field({ field, value, onChange }) {
|
|
75
|
+
return jsx.jsxs('div', {
|
|
76
|
+
style: S.row,
|
|
77
|
+
children: [
|
|
78
|
+
jsx.jsx('span', { style: S.label, children: field.label }),
|
|
79
|
+
jsx.jsx('input', {
|
|
80
|
+
style: S.input,
|
|
81
|
+
type: field.secret ? 'password' : 'text',
|
|
82
|
+
placeholder: field.placeholder || '',
|
|
83
|
+
value: value ?? '',
|
|
84
|
+
onChange: (event) => onChange(field.key, event.target.value),
|
|
85
|
+
}),
|
|
86
|
+
],
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Toggle({ label, checked, onChange }) {
|
|
91
|
+
return jsx.jsxs('div', {
|
|
92
|
+
style: S.row,
|
|
93
|
+
children: [
|
|
94
|
+
jsx.jsx('span', { style: S.label, children: label }),
|
|
95
|
+
jsx.jsx('input', { type: 'checkbox', checked: Boolean(checked), onChange: (event) => onChange(event.target.checked) }),
|
|
96
|
+
],
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function MinifluxPage() {
|
|
101
|
+
const [config, setConfig] = useState(null)
|
|
102
|
+
const [providers, setProviders] = useState([])
|
|
103
|
+
const [status, setStatus] = useState(null)
|
|
104
|
+
const [files, setFiles] = useState({ dir: '', files: [] })
|
|
105
|
+
const [busy, setBusy] = useState('')
|
|
106
|
+
const [message, setMessage] = useState('')
|
|
107
|
+
|
|
108
|
+
const load = useCallback(async () => {
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch('/api/dsh-miniflux/config', { headers: { Accept: 'application/json' } })
|
|
111
|
+
const data = await res.json()
|
|
112
|
+
setConfig(data.config || {})
|
|
113
|
+
} catch (error) {
|
|
114
|
+
setMessage(`读取配置失败 / failed to load config: ${error?.message || error}`)
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const res = await fetch('/api/dsh-miniflux/models', { headers: { Accept: 'application/json' } })
|
|
118
|
+
const data = await res.json()
|
|
119
|
+
setProviders(Array.isArray(data.providers) ? data.providers : [])
|
|
120
|
+
} catch { setProviders([]) }
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch('/api/dsh-miniflux/status', { headers: { Accept: 'application/json' } })
|
|
123
|
+
setStatus(await res.json())
|
|
124
|
+
} catch { setStatus(null) }
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch('/api/dsh-miniflux/files', { headers: { Accept: 'application/json' } })
|
|
127
|
+
const data = await res.json()
|
|
128
|
+
setFiles({ dir: data.dir || '', files: Array.isArray(data.files) ? data.files : [] })
|
|
129
|
+
} catch { setFiles({ dir: '', files: [] }) }
|
|
130
|
+
}, [])
|
|
131
|
+
|
|
132
|
+
useEffect(() => { void load() }, [load])
|
|
133
|
+
|
|
134
|
+
const setField = (key, value) => setConfig((current) => ({ ...(current || {}), [key]: value }))
|
|
135
|
+
|
|
136
|
+
const save = async () => {
|
|
137
|
+
setBusy('save'); setMessage('')
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch('/api/dsh-miniflux/config', {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: { 'Content-Type': 'application/json' },
|
|
142
|
+
body: JSON.stringify(config || {}),
|
|
143
|
+
})
|
|
144
|
+
const data = await res.json()
|
|
145
|
+
if (data.ok) { setConfig(data.config); setMessage('已保存 / saved') } else setMessage(`保存失败 / save failed: ${data.message || ''}`)
|
|
146
|
+
} catch (error) {
|
|
147
|
+
setMessage(`保存失败 / save failed: ${error?.message || error}`)
|
|
148
|
+
} finally { setBusy('') }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const run = async (what) => {
|
|
152
|
+
setBusy(what); setMessage('任务已提交,正在后台生成…(模型慢的话可能要几分钟)')
|
|
153
|
+
try {
|
|
154
|
+
const res = await fetch(`/api/dsh-miniflux/generate?what=${what}`, { method: 'POST' })
|
|
155
|
+
const data = await res.json()
|
|
156
|
+
if (!data.ok) { setMessage(`无法开始 / cannot start: ${data.message || ''}`); setBusy(''); return }
|
|
157
|
+
} catch (error) {
|
|
158
|
+
setMessage(`失败 / failed: ${error?.message || error}`); setBusy(''); return
|
|
159
|
+
}
|
|
160
|
+
// 轮询后台任务
|
|
161
|
+
for (;;) {
|
|
162
|
+
await new Promise((resolve) => setTimeout(resolve, 3000))
|
|
163
|
+
let state
|
|
164
|
+
try {
|
|
165
|
+
state = await (await fetch('/api/dsh-miniflux/job')).json()
|
|
166
|
+
} catch { continue }
|
|
167
|
+
if (state.state === 'running') { setMessage(`生成中… 已用 ${state.elapsedSec || 0} 秒(可切走,跑完回来点刷新)/ running`); continue }
|
|
168
|
+
if (state.state === 'done') {
|
|
169
|
+
const result = state.result || {}
|
|
170
|
+
const target = result.digestEpub || result.picksEpub || result.epub || result.title || ''
|
|
171
|
+
setMessage(result.ok
|
|
172
|
+
? `完成 / done(${state.elapsedSec} 秒): ${target}${result.skippedScreened ? ` · 跳过已筛 ${result.skippedScreened} 篇` : ''}${result.starred?.length ? ` · 收藏 ${result.starred.length} 条` : ''}${result.read ? ` · 已读 ${result.read} 条` : ''}`
|
|
173
|
+
: `失败 / failed: ${result.message || ''}`)
|
|
174
|
+
} else if (state.state === 'failed') {
|
|
175
|
+
setMessage(`失败 / failed: ${state.error || ''}`)
|
|
176
|
+
} else {
|
|
177
|
+
setMessage('空闲 / idle')
|
|
178
|
+
}
|
|
179
|
+
setBusy('')
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const testModel = async () => {
|
|
185
|
+
setBusy('test'); setMessage('')
|
|
186
|
+
try {
|
|
187
|
+
const res = await fetch('/api/dsh-miniflux/test-model', { method: 'POST' })
|
|
188
|
+
const data = await res.json()
|
|
189
|
+
if (data.ok) setMessage(`模型可用 / model ok: ${data.provider} / ${data.model} → “${data.reply}”`)
|
|
190
|
+
else setMessage(`模型调用失败 / model failed: ${data.provider || ''} / ${data.model || ''} → ${data.message || ''}`)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
setMessage(`测试失败 / test failed: ${error?.message || error}`)
|
|
193
|
+
} finally { setBusy('') }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const chosenProvider = (config && config.dshProvider) || ''
|
|
197
|
+
const providerEntry = providers.find((item) => item.id === chosenProvider)
|
|
198
|
+
const modelOptions = providerEntry?.models || []
|
|
199
|
+
|
|
200
|
+
return jsx.jsxs('div', {
|
|
201
|
+
style: S.page,
|
|
202
|
+
children: [
|
|
203
|
+
jsx.jsx('h2', { style: S.title, children: 'Miniflux' }),
|
|
204
|
+
jsx.jsx('p', {
|
|
205
|
+
style: S.hint,
|
|
206
|
+
children: 'AI 日报、精选与自选源 EPUB。连接参数和模型都在这里填,保存后立即生效(无需重启)。 / Fill in the connection and model here; changes apply immediately.',
|
|
207
|
+
}),
|
|
208
|
+
|
|
209
|
+
jsx.jsxs('div', {
|
|
210
|
+
style: S.group,
|
|
211
|
+
children: [
|
|
212
|
+
jsx.jsx('div', { style: S.legend, children: '连接 / Connection' }),
|
|
213
|
+
...FIELDS.map((field) => jsx.jsx(Field, {
|
|
214
|
+
field, value: config?.[field.key], onChange: setField,
|
|
215
|
+
}, field.key)),
|
|
216
|
+
],
|
|
217
|
+
}),
|
|
218
|
+
|
|
219
|
+
jsx.jsxs('div', {
|
|
220
|
+
style: S.group,
|
|
221
|
+
children: [
|
|
222
|
+
jsx.jsx('div', { style: S.legend, children: '模型 / Model' }),
|
|
223
|
+
jsx.jsxs('div', {
|
|
224
|
+
style: S.row,
|
|
225
|
+
children: [
|
|
226
|
+
jsx.jsx('span', { style: S.label, children: '来源 / Source' }),
|
|
227
|
+
jsx.jsxs('select', {
|
|
228
|
+
style: S.input,
|
|
229
|
+
value: config?.aiSource || 'dsh',
|
|
230
|
+
onChange: (event) => setField('aiSource', event.target.value),
|
|
231
|
+
children: [
|
|
232
|
+
jsx.jsx('option', { value: 'dsh', children: 'DSH 已连接的模型 / models connected to DSH' }),
|
|
233
|
+
jsx.jsx('option', { value: 'custom', children: '自定义接口 / custom OpenAI-compatible endpoint' }),
|
|
234
|
+
],
|
|
235
|
+
}),
|
|
236
|
+
],
|
|
237
|
+
}),
|
|
238
|
+
...(config?.aiSource === 'custom'
|
|
239
|
+
? [
|
|
240
|
+
jsx.jsx(Field, { field: { key: 'aiBaseUrl', label: '接口地址 / Base URL', placeholder: 'https://api.deepseek.com/v1' }, value: config?.aiBaseUrl, onChange: setField }, 'aiBaseUrl'),
|
|
241
|
+
jsx.jsx(Field, { field: { key: 'aiApiKey', label: 'API Key', placeholder: 'sk-...', secret: true }, value: config?.aiApiKey, onChange: setField }, 'aiApiKey'),
|
|
242
|
+
jsx.jsx(Field, { field: { key: 'aiModel', label: '模型名 / Model id', placeholder: 'deepseek-chat' }, value: config?.aiModel, onChange: setField }, 'aiModel'),
|
|
243
|
+
]
|
|
244
|
+
: [
|
|
245
|
+
jsx.jsxs('div', {
|
|
246
|
+
style: S.row,
|
|
247
|
+
children: [
|
|
248
|
+
jsx.jsx('span', { style: S.label, children: '服务商 / Provider' }),
|
|
249
|
+
jsx.jsxs('select', {
|
|
250
|
+
style: S.input,
|
|
251
|
+
value: chosenProvider,
|
|
252
|
+
onChange: (event) => {
|
|
253
|
+
const next = event.target.value
|
|
254
|
+
const entry = providers.find((item) => item.id === next)
|
|
255
|
+
setConfig((current) => ({
|
|
256
|
+
...(current || {}),
|
|
257
|
+
dshProvider: next,
|
|
258
|
+
dshModel: entry?.models?.[0] || current?.dshModel || '',
|
|
259
|
+
}))
|
|
260
|
+
},
|
|
261
|
+
children: [
|
|
262
|
+
jsx.jsx('option', { value: '', children: providers.length ? '请选择 / select' : '(模型列表不可用,可手填)/ list unavailable, type below' }),
|
|
263
|
+
...providers.map((item) => jsx.jsx('option', { value: item.id, children: item.name }, item.id)),
|
|
264
|
+
],
|
|
265
|
+
}),
|
|
266
|
+
],
|
|
267
|
+
}),
|
|
268
|
+
modelOptions.length
|
|
269
|
+
? jsx.jsxs('div', {
|
|
270
|
+
style: S.row,
|
|
271
|
+
children: [
|
|
272
|
+
jsx.jsx('span', { style: S.label, children: '模型 / Model' }),
|
|
273
|
+
jsx.jsxs('select', {
|
|
274
|
+
style: S.input,
|
|
275
|
+
value: config?.dshModel || '',
|
|
276
|
+
onChange: (event) => setField('dshModel', event.target.value),
|
|
277
|
+
children: modelOptions.map((id) => jsx.jsx('option', { value: id, children: id }, id)),
|
|
278
|
+
}),
|
|
279
|
+
],
|
|
280
|
+
})
|
|
281
|
+
: jsx.jsx(Field, { field: { key: 'dshModel', label: '模型 / Model', placeholder: 'deepseek-flash' }, value: config?.dshModel, onChange: setField }, 'dshModel'),
|
|
282
|
+
]),
|
|
283
|
+
],
|
|
284
|
+
}),
|
|
285
|
+
|
|
286
|
+
jsx.jsxs('div', {
|
|
287
|
+
style: S.group,
|
|
288
|
+
children: [
|
|
289
|
+
jsx.jsx('div', { style: S.legend, children: 'AI 能力、定时与产出 / AI, schedule & output' }),
|
|
290
|
+
...ADVANCED.map((field) => jsx.jsx(Field, { field, value: config?.[field.key], onChange: setField }, field.key)),
|
|
291
|
+
jsx.jsx(Toggle, { label: '内嵌图片 / Embed images', checked: config?.withImages !== false, onChange: (v) => setField('withImages', v) }),
|
|
292
|
+
jsx.jsx(Toggle, { label: '自动加星 / Auto star picks', checked: config?.autoStar !== false, onChange: (v) => setField('autoStar', v) }),
|
|
293
|
+
jsx.jsx(Toggle, { label: '跳过已筛过的 / Skip screened', checked: config?.picksSkipScreened !== false, onChange: (v) => setField('picksSkipScreened', v) }),
|
|
294
|
+
],
|
|
295
|
+
}),
|
|
296
|
+
|
|
297
|
+
jsx.jsxs('div', {
|
|
298
|
+
style: S.actions,
|
|
299
|
+
children: [
|
|
300
|
+
jsx.jsx('button', { style: S.button, disabled: busy === 'save', onClick: save, children: busy === 'save' ? '保存中…' : '保存 / Save' }),
|
|
301
|
+
jsx.jsx('button', { style: S.button, disabled: busy === 'digest', onClick: () => run('digest'), children: busy === 'digest' ? '生成中…' : '生成日报 / Daily digest' }),
|
|
302
|
+
jsx.jsx('button', { style: S.button, disabled: busy === 'picks', onClick: () => run('picks'), children: busy === 'picks' ? '生成中…' : '生成精选 / Picks now' }),
|
|
303
|
+
jsx.jsx('button', { style: S.button, disabled: busy === 'feed-epub', onClick: () => run('feed-epub'), children: busy === 'feed-epub' ? '生成中…' : '生成自选源 EPUB / Feed EPUB now' }),
|
|
304
|
+
jsx.jsx('button', { style: S.button, disabled: busy === 'test', onClick: testModel, children: busy === 'test' ? '测试中…' : '测试模型 / Test model' }),
|
|
305
|
+
jsx.jsx('button', {
|
|
306
|
+
style: S.button,
|
|
307
|
+
onClick: async () => {
|
|
308
|
+
setMessage('')
|
|
309
|
+
try {
|
|
310
|
+
const res = await fetch('/api/dsh-miniflux/reset-screened', { method: 'POST' })
|
|
311
|
+
const data = await res.json()
|
|
312
|
+
setMessage(`已清空筛选记录 / ledger cleared: 移除 ${data.removed ?? 0} 条`)
|
|
313
|
+
} catch (error) { setMessage(`清空失败 / failed: ${error?.message || error}`) }
|
|
314
|
+
},
|
|
315
|
+
children: '清空筛选记录 / Clear screened',
|
|
316
|
+
}),
|
|
317
|
+
jsx.jsx('button', { style: S.button, onClick: load, children: '刷新 / Refresh' }),
|
|
318
|
+
],
|
|
319
|
+
}),
|
|
320
|
+
|
|
321
|
+
message ? jsx.jsx('div', { style: S.status, children: message }) : null,
|
|
322
|
+
|
|
323
|
+
jsx.jsxs('div', {
|
|
324
|
+
style: S.group,
|
|
325
|
+
children: [
|
|
326
|
+
jsx.jsx('div', { style: S.legend, children: '最近生成 / Recent files' }),
|
|
327
|
+
jsx.jsx('div', { style: S.label, children: `目录 / dir: ${files.dir || '(未配置)'}` }),
|
|
328
|
+
...(files.files.length
|
|
329
|
+
? files.files.map((file) => jsx.jsx('div', {
|
|
330
|
+
style: S.label,
|
|
331
|
+
children: `${file.name} · ${Math.round(file.bytes / 1024)} KB · ${new Date(file.mtime).toLocaleString()}`,
|
|
332
|
+
}, file.path))
|
|
333
|
+
: [jsx.jsx('div', { style: S.label, children: '(还没有生成过文件)/ no files yet' })]),
|
|
334
|
+
],
|
|
335
|
+
}),
|
|
336
|
+
|
|
337
|
+
status
|
|
338
|
+
? jsx.jsxs('div', { style: S.status, children: [
|
|
339
|
+
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 || ''}` }),
|
|
340
|
+
jsx.jsx('div', { children: `模型 / model: ${status.model || '-'}` }),
|
|
341
|
+
jsx.jsx('div', { children: `EPUB 目录 / dir: ${status.epubDir || '-'}` }),
|
|
342
|
+
] })
|
|
343
|
+
: null,
|
|
344
|
+
],
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const inject = ['slots']
|
|
349
|
+
|
|
350
|
+
function apply(ctx) {
|
|
351
|
+
try {
|
|
352
|
+
const disposers = []
|
|
353
|
+
disposers.push(ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
|
|
354
|
+
name: 'sidebar.panellist',
|
|
355
|
+
id: 'miniflux',
|
|
356
|
+
order: 40,
|
|
357
|
+
label: () => 'Miniflux',
|
|
358
|
+
}, MinifluxIcon)))
|
|
359
|
+
disposers.push(ctx.slots.inject('main', () => ctx.slots.register({
|
|
360
|
+
name: 'main',
|
|
361
|
+
key: 'miniflux',
|
|
362
|
+
}, MinifluxPage)))
|
|
363
|
+
ctx.effect?.(() => () => { for (const dispose of disposers.splice(0)) dispose() }, 'dsh-miniflux: client slots')
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error('[dsh-miniflux] client mount failed:', error?.message || error)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
exports.apply = apply
|
|
370
|
+
exports.inject = inject
|
|
371
|
+
return module.exports
|
|
372
|
+
},
|
|
373
|
+
})
|