dsh-hackertube 0.1.4 → 0.1.5
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.en.md +18 -0
- package/README.md +18 -0
- package/index.js +98 -25
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -108,6 +108,24 @@ Likes, watch later, and history live on the host side, so clearing browser data
|
|
|
108
108
|
| `?` | Show all shortcuts |
|
|
109
109
|
| `Esc` | Close overlay |
|
|
110
110
|
|
|
111
|
+
## A tool for your agent
|
|
112
|
+
|
|
113
|
+
The plugin registers a tool named `hackertube_search`. Once installed, your agent can search this video library from any conversation, without opening the panel.
|
|
114
|
+
|
|
115
|
+
Good requests for it:
|
|
116
|
+
|
|
117
|
+
- Any talk explaining Raft consensus
|
|
118
|
+
- A few lectures on compiler optimization
|
|
119
|
+
- Which Karpathy video covers backpropagation
|
|
120
|
+
|
|
121
|
+
It searches titles, channels, tags and categories in English and Chinese, plus each video's summary and Hacker News recommendation note. Results carry title, channel, duration, view count, recommender count, the recommendation note and a link, ordered by how strongly Hacker News recommends them. Chinese queries work, and titles come back in Chinese when a translation exists.
|
|
122
|
+
|
|
123
|
+
| Parameter | Meaning |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `query` | Search terms, English or Chinese |
|
|
126
|
+
| `limit` | Result count, default 5, max 15 |
|
|
127
|
+
| `lang` | Language for the returned text, `zh` or `en`, default `zh` |
|
|
128
|
+
|
|
111
129
|
## First launch
|
|
112
130
|
|
|
113
131
|
The plugin starts downloading the official export as soon as it loads, 17.8 MB in one request, and at the same time requests upstream pages to fill in YouTube duration, view count, and publish date. The list is only complete once both finish. A skeleton screen covers the panel in the meantime.
|
package/README.md
CHANGED
|
@@ -108,6 +108,24 @@ DSH 从 profile 目录加载插件,在普通项目里安装不会生效。需
|
|
|
108
108
|
| `?` | 显示快捷键一览 |
|
|
109
109
|
| `Esc` | 关闭浮层 |
|
|
110
110
|
|
|
111
|
+
## 给 agent 用的工具
|
|
112
|
+
|
|
113
|
+
插件注册了一个工具 `hackertube_search`。装了插件之后,不用打开面板,agent 在对话里就能检索这个视频库。
|
|
114
|
+
|
|
115
|
+
适合这类请求:
|
|
116
|
+
|
|
117
|
+
- 有没有讲 Raft 共识算法的视频
|
|
118
|
+
- 找几个讲编译器优化的演讲
|
|
119
|
+
- Karpathy 讲反向传播的是哪一个
|
|
120
|
+
|
|
121
|
+
它按中英文检索标题、频道、标签、分类,以及每个视频的简介和 HN 推荐理由,返回标题、频道、时长、观看量、HN 推荐人数、推荐理由与链接,并按 HN 推荐强度排序。中文查询也能命中,结果里的标题会显示中文译文。
|
|
122
|
+
|
|
123
|
+
| 参数 | 说明 |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `query` | 检索词,中英文均可 |
|
|
126
|
+
| `limit` | 返回条数,默认 5,最多 15 |
|
|
127
|
+
| `lang` | 返回文案的语言,`zh` 或 `en`,默认 `zh` |
|
|
128
|
+
|
|
111
129
|
## 首次启动
|
|
112
130
|
|
|
113
131
|
插件加载时即开始下载官方数据导出(17.8 MB,一次性),并同时请求上游接口补齐 YouTube 的时长、播放量与发布时间。两者完成后列表才是完整状态,期间界面显示骨架屏。
|
package/index.js
CHANGED
|
@@ -2954,53 +2954,126 @@ export function apply(ctx) {
|
|
|
2954
2954
|
name: 'hackertube_search',
|
|
2955
2955
|
description:
|
|
2956
2956
|
'在 Hacker News 二十年推荐的 6454 个技术视频里检索。' +
|
|
2957
|
-
'
|
|
2958
|
-
'
|
|
2957
|
+
'当用户讨论某个技术概念、想找深入讲解,或问有没有相关的演讲、课程、访谈时使用。' +
|
|
2958
|
+
'支持中英文检索词,返回标题、频道、时长、观看量、HN 推荐人数、推荐理由与链接。',
|
|
2959
2959
|
parameters: {
|
|
2960
2960
|
query: {
|
|
2961
2961
|
type: 'string',
|
|
2962
2962
|
required: true,
|
|
2963
|
-
description: '
|
|
2963
|
+
description: '检索词,可以是技术概念、语言、框架名或自然语言描述,中英文均可',
|
|
2964
2964
|
},
|
|
2965
2965
|
limit: {
|
|
2966
2966
|
type: 'number',
|
|
2967
2967
|
description: '返回条数,默认 5,最多 15',
|
|
2968
2968
|
},
|
|
2969
|
+
lang: {
|
|
2970
|
+
type: 'string',
|
|
2971
|
+
description: '返回文案的语言,zh 或 en,默认 zh',
|
|
2972
|
+
},
|
|
2969
2973
|
},
|
|
2970
2974
|
output: {
|
|
2971
2975
|
schema: { type: 'string' },
|
|
2972
2976
|
render: (_args, value) => [{ type: 'text', text: String(value) }],
|
|
2973
2977
|
},
|
|
2974
2978
|
async execute(args) {
|
|
2975
|
-
const
|
|
2979
|
+
const lang = args.lang === 'en' ? 'en' : 'zh'
|
|
2980
|
+
const tz = (zh, en) => (lang === 'zh' ? zh : en)
|
|
2976
2981
|
const terms = String(args.query || '')
|
|
2977
2982
|
.toLowerCase()
|
|
2978
2983
|
.split(/\s+/)
|
|
2979
2984
|
.filter(Boolean)
|
|
2980
|
-
if (!terms.length) return '请提供检索词。'
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2985
|
+
if (!terms.length) return tz('请提供检索词。', 'Provide a search query.')
|
|
2986
|
+
|
|
2987
|
+
const items = await loadList()
|
|
2988
|
+
const zhTitles = await loadI18n('zh-titles').catch(() => ({}))
|
|
2989
|
+
let detailMap = null
|
|
2990
|
+
try {
|
|
2991
|
+
detailMap = (await loadDataset()).detail
|
|
2992
|
+
} catch {
|
|
2993
|
+
/* 导出不可用时退回只按列表字段检索 */
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
/* 拉丁词按词边界匹配,否则 raft 会命中 craft、Minecraft;中文按子串 */
|
|
2997
|
+
const escapeRe = (x) => String(x).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
2998
|
+
const matchers = terms.map((term) => {
|
|
2999
|
+
if (/[\u4e00-\u9fff]/.test(term)) return (hay) => hay.indexOf(term) >= 0
|
|
3000
|
+
const re = new RegExp('(^|[^a-z0-9])' + escapeRe(term) + '([^a-z0-9]|$)', 'i')
|
|
3001
|
+
return (hay) => re.test(hay)
|
|
3002
|
+
})
|
|
3003
|
+
|
|
3004
|
+
const scored = []
|
|
3005
|
+
for (const it of items) {
|
|
3006
|
+
const d = detailMap ? detailMap.get(it.v) : null
|
|
3007
|
+
const titleBoth = it.t + ' ' + (zhTitles[it.v] || '')
|
|
3008
|
+
const hay = [
|
|
3009
|
+
titleBoth,
|
|
3010
|
+
it.c || '',
|
|
3011
|
+
(it.tags || []).join(' '),
|
|
3012
|
+
(it.cats || []).join(' '),
|
|
3013
|
+
(d && d.summary) || '',
|
|
3014
|
+
(d && d.why_hn) || '',
|
|
3015
|
+
(d && d.primary_topic) || '',
|
|
3016
|
+
]
|
|
3017
|
+
.join(' ')
|
|
3018
|
+
.toLowerCase()
|
|
3019
|
+
let hits = 0
|
|
3020
|
+
for (const m of matchers) if (m(hay)) hits += 1
|
|
3021
|
+
if (!hits) continue
|
|
3022
|
+
const titleLower = titleBoth.toLowerCase()
|
|
3023
|
+
const titleHit = matchers.some((m) => m(titleLower))
|
|
3024
|
+
scored.push({ it, d, hits, titleHit })
|
|
3025
|
+
}
|
|
3026
|
+
if (!scored.length) {
|
|
3027
|
+
return tz(
|
|
3028
|
+
'没有找到匹配的视频。可以换更宽泛的关键词,中英文都试试。',
|
|
3029
|
+
'No matching videos. Try broader keywords, in English or Chinese.',
|
|
3030
|
+
)
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
/* 先看命中词数,再看是否命中标题,最后用 HN 推荐强度排序 */
|
|
3034
|
+
scored.sort(
|
|
3035
|
+
(a, b) =>
|
|
3036
|
+
b.hits - a.hits ||
|
|
3037
|
+
(b.titleHit ? 1 : 0) - (a.titleHit ? 1 : 0) ||
|
|
3038
|
+
(b.it.s || 0) - (a.it.s || 0),
|
|
3039
|
+
)
|
|
3040
|
+
const pick = scored.slice(0, Math.min(Number(args.limit) || 5, 15))
|
|
3041
|
+
|
|
3042
|
+
const nf = (n) => {
|
|
3043
|
+
const v = Number(n) || 0
|
|
3044
|
+
if (lang === 'zh') {
|
|
3045
|
+
if (v >= 1e8) return (v / 1e8).toFixed(1).replace(/\.0$/, '') + ' 亿'
|
|
3046
|
+
if (v >= 1e4) return (v / 1e4).toFixed(1).replace(/\.0$/, '') + ' 万'
|
|
3047
|
+
return String(v)
|
|
3048
|
+
}
|
|
3049
|
+
if (v >= 1e9) return (v / 1e9).toFixed(1).replace(/\.0$/, '') + 'B'
|
|
3050
|
+
if (v >= 1e6) return (v / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'
|
|
3051
|
+
if (v >= 1e3) return (v / 1e3).toFixed(1).replace(/\.0$/, '') + 'K'
|
|
3052
|
+
return String(v)
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
return pick
|
|
2995
3056
|
.map((x, i) => {
|
|
2996
3057
|
const it = x.it
|
|
2997
|
-
const
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3058
|
+
const zh = zhTitles[it.v] || ''
|
|
3059
|
+
const title = lang === 'zh' && zh ? zh : it.t
|
|
3060
|
+
const alt = lang === 'zh' && zh && zh !== it.t ? '(' + it.t + ')' : ''
|
|
3061
|
+
const mins = Math.max(1, Math.round((it.d || 0) / 60))
|
|
3062
|
+
const bits = [
|
|
3063
|
+
it.c || '',
|
|
3064
|
+
tz(mins + ' 分钟', mins + ' min'),
|
|
3065
|
+
tz(nf(it.w) + '次观看', nf(it.w) + ' views'),
|
|
3066
|
+
]
|
|
3067
|
+
if (it.r) bits.push(tz(it.r + ' 人推荐', it.r + ' recommenders'))
|
|
3068
|
+
let line = (i + 1) + '. ' + title + alt + ' — ' + bits.filter(Boolean).join(' · ')
|
|
3069
|
+
const why = (x.d && (x.d.why_hn || x.d.summary)) || ''
|
|
3070
|
+
if (why) {
|
|
3071
|
+
line += '\n ' + tz('HN:', 'HN: ') + (why.length > 110 ? why.slice(0, 110) + '…' : why)
|
|
3072
|
+
}
|
|
3073
|
+
line += '\n https://www.youtube.com/watch?v=' + it.v
|
|
3074
|
+
return line
|
|
3002
3075
|
})
|
|
3003
|
-
.join('\n')
|
|
3076
|
+
.join('\n\n')
|
|
3004
3077
|
},
|
|
3005
3078
|
})
|
|
3006
3079
|
void writeCache('tool-registered.json', { ok: true, at: Date.now() })
|