dsh-animebar 0.1.0 → 0.3.2

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/README.md CHANGED
@@ -32,14 +32,15 @@
32
32
  ### 安装
33
33
 
34
34
  ```sh
35
- # 直接从仓库装
35
+ # 从 npm 装(推荐)
36
+ dsh plugin --profile desktop add dsh-animebar
37
+
38
+ # 或者直接从仓库装
36
39
  dsh plugin --profile desktop add github:V1Y0h/animebar-plugin
37
40
  ```
38
41
 
39
42
  也可以把本地目录加进 **设置 → 插件**。安装后重启 DeepSeek Harness,工具才会注册。
40
43
 
41
- npm 版本还没发布;发出去之后 `dsh plugin --profile desktop add dsh-animebar` 也能用。
42
-
43
44
  ### 平台
44
45
 
45
46
  插件是纯 Host 侧 JavaScript,没有原生代码,所以只要能跑 DeepSeek Harness 就能用——macOS、Linux、Windows 都行。只要求能访问 Bangumi 的网络。
@@ -83,6 +84,10 @@ Registers one tool, `anime_today`, which reads the public [Bangumi](https://bgm.
83
84
  ### Install
84
85
 
85
86
  ```sh
87
+ # from npm
88
+ dsh plugin --profile desktop add dsh-animebar
89
+
90
+ # or straight from the repository
86
91
  dsh plugin --profile desktop add github:V1Y0h/animebar-plugin
87
92
  ```
88
93
 
package/client.js ADDED
@@ -0,0 +1,325 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-animebar',
3
+ factory(require) {
4
+ const React = require('react');
5
+ const h = React.createElement;
6
+ const { useState, useEffect, useMemo, useCallback } = React;
7
+
8
+ const UA = { 'user-agent': 'dsh-animebar/0.3' };
9
+ const DAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
10
+ const CACHE_KEY = 'animebar.panel.cache.v2';
11
+
12
+ function todayKey() {
13
+ return new Date().toDateString();
14
+ }
15
+
16
+ function readCache() {
17
+ try {
18
+ const raw = localStorage.getItem(CACHE_KEY);
19
+ if (!raw) return null;
20
+ const parsed = JSON.parse(raw);
21
+ return parsed && parsed.day === todayKey() ? parsed : null;
22
+ } catch (e) {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function writeCache(data) {
28
+ try {
29
+ localStorage.setItem(CACHE_KEY, JSON.stringify(Object.assign({ day: todayKey() }, data)));
30
+ } catch (e) {
31
+ /* 存储不可用时每次重新抓 */
32
+ }
33
+ }
34
+
35
+ async function fetchTv() {
36
+ const res = await fetch('https://api.bgm.tv/calendar', { headers: UA });
37
+ if (!res.ok) throw new Error('Bangumi ' + res.status);
38
+ const data = await res.json();
39
+ const out = [];
40
+ for (const day of data) {
41
+ for (const it of day.items || []) {
42
+ out.push({
43
+ id: it.id,
44
+ title: it.name_cn || it.name,
45
+ orig: it.name,
46
+ score: (it.rating && it.rating.score) || 0,
47
+ img: (it.images && (it.images.common || it.images.large)) || '',
48
+ wd: (day.weekday && day.weekday.id) || 0,
49
+ date: it.air_date || '',
50
+ url: 'https://bgm.tv/subject/' + it.id,
51
+ });
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+
57
+ async function fetchMovies() {
58
+ const out = [];
59
+ for (let page = 0; page < 4; page += 1) {
60
+ const res = await fetch('https://api.bgm.tv/v0/search/subjects?limit=20&offset=' + page * 20, {
61
+ method: 'POST',
62
+ headers: Object.assign({ 'content-type': 'application/json' }, UA),
63
+ body: JSON.stringify({ keyword: '劇場版', filter: { type: [2] }, sort: 'heat' }),
64
+ });
65
+ if (!res.ok) break;
66
+ const data = await res.json();
67
+ const rows = data.data || [];
68
+ if (rows.length === 0) break;
69
+ for (const it of rows) {
70
+ out.push({
71
+ id: it.id,
72
+ title: it.name_cn || it.name,
73
+ orig: it.name,
74
+ score: (it.rating && it.rating.score) || 0,
75
+ img: (it.images && (it.images.common || it.images.large)) || '',
76
+ date: (it.date || '').slice(0, 10),
77
+ url: 'https://bgm.tv/subject/' + it.id,
78
+ });
79
+ }
80
+ }
81
+ return out;
82
+ }
83
+
84
+
85
+ // 无封面时用同款阿卡林幽灵占位(内联 SVG,不依赖文件)
86
+ const GHOST = 'data:image/svg+xml;utf8,' + encodeURIComponent(
87
+ "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 84'>" +
88
+ "<rect width='60' height='84' fill='#1e293b'/>" +
89
+ "<path d='M30 20c-9 0-14 7-14 15v20c0 4 3 4 5 2l3-3 3 4c1 2 5 2 6 0l3-4 3 4c2 2 5 2 5-2V35c0-8-5-15-14-15z' fill='#475569' opacity='.65'/>" +
90
+ "<circle cx='25' cy='38' r='2.4' fill='#1e293b'/><circle cx='35' cy='38' r='2.4' fill='#1e293b'/>" +
91
+ "</svg>");
92
+
93
+ const S = {
94
+ btn: {
95
+ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 5,
96
+ height: 28, padding: '0 11px', borderRadius: 999, cursor: 'pointer',
97
+ border: '1px solid #22c55e', background: '#22c55e', color: '#04140a',
98
+ fontSize: 12, fontWeight: 600, lineHeight: 1,
99
+ },
100
+ hint: {
101
+ position: 'absolute', left: 0, bottom: 36, whiteSpace: 'nowrap',
102
+ background: '#0f172a', color: '#e2e8f0', border: '1px solid #22c55e',
103
+ borderRadius: 8, padding: '5px 9px', fontSize: 11,
104
+ boxShadow: '0 8px 20px rgba(0,0,0,.35)',
105
+ },
106
+ panel: {
107
+ position: 'fixed', right: 16, bottom: 88, width: 400, maxHeight: '76vh',
108
+ display: 'flex', flexDirection: 'column', zIndex: 60,
109
+ background: '#020617', color: '#e2e8f0', borderRadius: 14,
110
+ border: '1px solid #1e293b', boxShadow: '0 18px 48px rgba(0,0,0,.5)',
111
+ fontFamily: 'system-ui, -apple-system, "PingFang SC", sans-serif',
112
+ overflow: 'hidden', fontSize: 13,
113
+ },
114
+ head: { display: 'flex', alignItems: 'center', gap: 6, padding: '10px 12px 8px', borderBottom: '1px solid #1e293b' },
115
+ tabs: { display: 'flex', gap: 4, flex: 1 },
116
+ tab: {
117
+ border: '1px solid transparent', background: 'transparent', color: '#94a3b8',
118
+ borderRadius: 7, padding: '4px 10px', fontSize: 12, cursor: 'pointer',
119
+ },
120
+ tabOn: { background: 'rgba(34,197,94,.16)', color: '#22c55e', border: '1px solid #166534' },
121
+ icon: { border: 'none', background: 'transparent', color: '#94a3b8', cursor: 'pointer', fontSize: 15, lineHeight: 1, padding: 2 },
122
+ filters: { display: 'flex', flexWrap: 'wrap', gap: 4, padding: '8px 12px', borderBottom: '1px solid #0f172a' },
123
+ chip: {
124
+ border: '1px solid #1e293b', background: 'transparent', color: '#94a3b8',
125
+ borderRadius: 999, padding: '2px 8px', fontSize: 11, cursor: 'pointer',
126
+ },
127
+ chipOn: { background: '#1e293b', color: '#e2e8f0', border: '1px solid #334155' },
128
+ search: {
129
+ flex: 1, minWidth: 90, background: 'transparent', border: '1px solid #1e293b',
130
+ borderRadius: 999, color: 'inherit', fontSize: 11, padding: '3px 9px', outline: 'none',
131
+ },
132
+ list: { overflowY: 'auto', padding: 6 },
133
+ row: { display: 'flex', gap: 9, alignItems: 'center', padding: '5px 7px', borderRadius: 8, textDecoration: 'none', color: 'inherit' },
134
+ coverBox: {
135
+ width: 32, height: 45, borderRadius: 5, flex: '0 0 auto', overflow: 'hidden',
136
+ backgroundColor: '#1e293b', backgroundImage: 'url("' + GHOST + '")',
137
+ backgroundSize: 'cover', backgroundPosition: 'center',
138
+ },
139
+ cover: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' },
140
+ title: { fontSize: 12.5, lineHeight: 1.35, margin: 0, flex: 1, minWidth: 0 },
141
+ score: { fontSize: 11, fontVariantNumeric: 'tabular-nums', color: '#94a3b8', flex: '0 0 auto' },
142
+ act: { border: '1px solid #1e293b', background: 'transparent', color: '#64748b', borderRadius: 6, fontSize: 10, padding: '2px 6px', cursor: 'pointer', flex: '0 0 auto' },
143
+ status: { padding: 16, fontSize: 12, color: '#94a3b8', textAlign: 'center' },
144
+ toast: {
145
+ position: 'fixed', right: 24, bottom: 96, zIndex: 70,
146
+ background: '#166534', color: '#dcfce7', border: '1px solid #22c55e',
147
+ borderRadius: 8, padding: '6px 12px', fontSize: 12,
148
+ boxShadow: '0 8px 20px rgba(0,0,0,.4)',
149
+ },
150
+ };
151
+
152
+ function scoreMatch(score, filter) {
153
+ if (filter === 'all') return true;
154
+ if (filter === '8') return score >= 8;
155
+ if (filter === '7') return score >= 7 && score < 8;
156
+ return score < 7;
157
+ }
158
+
159
+ function AnimePanel() {
160
+ const [open, setOpen] = useState(false);
161
+ const [hint, setHint] = useState(() => {
162
+ try { return localStorage.getItem('animebar.hint') !== 'seen'; } catch (e) { return true; }
163
+ });
164
+ const [tab, setTab] = useState('tv');
165
+ const [tv, setTv] = useState(null);
166
+ const [movies, setMovies] = useState(null);
167
+ const [error, setError] = useState('');
168
+ const [wd, setWd] = useState(0);
169
+ const [score, setScore] = useState('all');
170
+ const [sortKey, setSortKey] = useState('score');
171
+ const [kw, setKw] = useState('');
172
+ const [toast, setToast] = useState('');
173
+
174
+ useEffect(() => {
175
+ if (!toast) return undefined;
176
+ const timer = setTimeout(() => setToast(''), 2000);
177
+ return () => clearTimeout(timer);
178
+ }, [toast]);
179
+
180
+ const load = useCallback(async (force) => {
181
+ if (!force) {
182
+ const cached = readCache();
183
+ if (cached) {
184
+ setTv(cached.tv || []);
185
+ setMovies(cached.movies || []);
186
+ return;
187
+ }
188
+ }
189
+ setError('');
190
+ try {
191
+ const tvData = await fetchTv();
192
+ setTv(tvData);
193
+ writeCache({ tv: tvData, movies });
194
+ // 剧场版单独抓,失败不影响新番
195
+ try {
196
+ const movieData = await fetchMovies();
197
+ setMovies(movieData);
198
+ writeCache({ tv: tvData, movies: movieData });
199
+ } catch (e) {
200
+ setMovies([]);
201
+ }
202
+ } catch (e) {
203
+ setError('抓取失败,稍后再试');
204
+ }
205
+ }, [movies]);
206
+
207
+ useEffect(() => {
208
+ if (open && tv === null && !error) load(false);
209
+ }, [open, tv, error, load]);
210
+
211
+ const source = tab === 'tv' ? tv || [] : movies || [];
212
+ const rows = useMemo(() => {
213
+ const k = kw.trim().toLowerCase();
214
+ const list = source.filter((it) => {
215
+ if (tab === 'tv' && wd !== 0 && it.wd !== wd) return false;
216
+ if (!scoreMatch(it.score, score)) return false;
217
+ if (k && !(it.title + (it.orig || '')).toLowerCase().includes(k)) return false;
218
+ return true;
219
+ });
220
+ return list.sort((a, b) => (sortKey === 'date'
221
+ ? (b.date || '').localeCompare(a.date || '')
222
+ : b.score - a.score));
223
+ }, [source, tab, wd, score, kw, sortKey]);
224
+
225
+ const dismissHint = () => {
226
+ if (!hint) return;
227
+ setHint(false);
228
+ try { localStorage.setItem('animebar.hint', 'seen'); } catch (e) { /* 忽略 */ }
229
+ };
230
+
231
+ const chip = (label, active, onClick, key) => h('button', {
232
+ key, type: 'button', onClick,
233
+ style: active ? Object.assign({}, S.chip, S.chipOn) : S.chip,
234
+ }, label);
235
+
236
+ return h('div', { style: { display: 'inline-flex', position: 'relative' } },
237
+ hint && !open ? h('div', { style: S.hint }, '点这里看今天的番') : null,
238
+ h('button', {
239
+ type: 'button',
240
+ title: '番表(Bangumi)',
241
+ onClick: () => { setOpen((v) => !v); dismissHint(); },
242
+ style: S.btn,
243
+ }, '番表'),
244
+
245
+ open ? h('div', { style: S.panel },
246
+ h('div', { style: S.head },
247
+ h('div', { style: S.tabs },
248
+ h('button', { type: 'button', onClick: () => setTab('tv'), style: Object.assign({}, S.tab, tab === 'tv' ? S.tabOn : null) }, '新番'),
249
+ h('button', { type: 'button', onClick: () => setTab('movie'), style: Object.assign({}, S.tab, tab === 'movie' ? S.tabOn : null) }, '剧场版'),
250
+ ),
251
+ h('button', { type: 'button', title: '刷新', style: S.icon, onClick: () => load(true) }, '↻'),
252
+ h('button', { type: 'button', title: '关闭', style: S.icon, onClick: () => setOpen(false) }, '×'),
253
+ ),
254
+
255
+ h('div', { style: S.filters },
256
+ tab === 'tv' ? chip('全部', wd === 0, () => setWd(0), 'wd0') : null,
257
+ tab === 'tv' ? DAYS.map((d, i) => chip(d, wd === i + 1, () => setWd(i + 1), 'wd' + (i + 1))) : null,
258
+ chip('评分不限', score === 'all', () => setScore('all'), 's0'),
259
+ chip('≥8', score === '8', () => setScore('8'), 's8'),
260
+ chip('7–8', score === '7', () => setScore('7'), 's7'),
261
+ chip('<7', score === 'low', () => setScore('low'), 'slow'),
262
+ chip('按评分', sortKey === 'score', () => setSortKey('score'), 'k1'),
263
+ chip('按最新', sortKey === 'date', () => setSortKey('date'), 'k2'),
264
+ h('input', {
265
+ value: kw,
266
+ placeholder: '搜名字…',
267
+ onChange: (e) => setKw(e.target.value),
268
+ style: S.search,
269
+ }),
270
+ ),
271
+
272
+ error
273
+ ? h('div', { style: S.status }, error)
274
+ : source.length === 0
275
+ ? h('div', { style: S.status }, '正在抓取…')
276
+ : rows.length === 0
277
+ ? h('div', { style: S.status }, '没有符合条件的作品')
278
+ : h('div', { style: S.list },
279
+ rows.map((it) => h('div', {
280
+ key: tab + '-' + it.id,
281
+ style: S.row,
282
+ },
283
+ h('a', { href: it.url, target: '_blank', rel: 'noopener noreferrer', style: { display: 'flex', flex: 1, gap: 9, alignItems: 'center', textDecoration: 'none', color: 'inherit', minWidth: 0 } },
284
+ h('div', { style: S.coverBox },
285
+ it.img
286
+ ? h('img', {
287
+ src: it.img,
288
+ alt: '',
289
+ loading: 'lazy',
290
+ style: S.cover,
291
+ onError: (e) => { e.currentTarget.style.display = 'none'; },
292
+ })
293
+ : null,
294
+ ),
295
+ h('p', { style: S.title }, it.title),
296
+ h('span', { style: S.score }, it.score ? it.score.toFixed(1) : '—'),
297
+ ),
298
+ h('a', {
299
+ href: 'animebar://anich?title=' + encodeURIComponent(it.title),
300
+ style: Object.assign({}, S.act, { textDecoration: 'none' }),
301
+ title: '唤起 AniCh 并复制标题(需安装 AnimeBar.app)',
302
+ onClick: () => {
303
+ try { navigator.clipboard.writeText(it.title); } catch (e) { /* 忽略 */ }
304
+ setToast('已复制:' + it.title);
305
+ },
306
+ }, 'AniCh'),
307
+ )),
308
+ ),
309
+ ) : null,
310
+ toast ? h('div', { style: S.toast }, toast) : null,
311
+ );
312
+ }
313
+
314
+ return {
315
+ inject: ['slots'],
316
+ apply(ctx) {
317
+ ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
318
+ name: 'conversation.composer.dock',
319
+ id: 'animebar-button',
320
+ order: 5,
321
+ }, AnimePanel));
322
+ },
323
+ };
324
+ },
325
+ });
package/docs/DETAIL.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  给 DeepSeek Harness 加一个查番表的工具。
4
4
 
5
+ ## 安装
6
+
7
+ ```sh
8
+ dsh plugin --profile desktop add dsh-animebar
9
+ ```
10
+
11
+ 装完重启 DeepSeek Harness。
12
+
5
13
  ## 用法
6
14
 
7
15
  直接问「今天有什么番」,或者指定星期:
@@ -11,7 +19,9 @@
11
19
  周三有哪些番在播
12
20
  ```
13
21
 
14
- 工具会返回当天在播、评分不低于配置阈值的作品,按分数从高到低。
22
+ 工具会返回当天在播的作品,按分数从高到低。默认不过滤分数,低分和没评分的也在里面。
23
+
24
+ 想看剧场版就说「最近有什么剧场版动画」,工具会切换到 `movie` 模式翻页抓取。
15
25
 
16
26
  ## 配置
17
27
 
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "dsh-animebar",
3
- "version": "0.1.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "Anime airing board for DeepSeek Harness — today's lineup from Bangumi.",
6
6
  "exports": {
7
7
  ".": "./index.js",
8
+ "./client": "./client.js",
8
9
  "./locale/*.json": "./locale/*.json",
9
10
  "./icon": "./icon.svg"
10
11
  },
11
12
  "files": [
12
13
  "index.js",
14
+ "client.js",
13
15
  "cordis.patch.yml",
14
16
  "locale/*.json",
15
17
  "docs",
@@ -18,6 +20,13 @@
18
20
  "dsh": {
19
21
  "bundle": {
20
22
  "patch": "./cordis.patch.yml"
23
+ },
24
+ "client": {
25
+ "platform": "web",
26
+ "immediately": true,
27
+ "inject": [
28
+ "@deepseek-ai/dsh-client-ui-conversation"
29
+ ]
21
30
  }
22
31
  },
23
32
  "dependencies": {