dsh-hackertube 0.1.3 → 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 CHANGED
@@ -57,7 +57,7 @@ Then confirm dsh-hackertube is enabled in the plugin list and remind me to refre
57
57
 
58
58
  ### Install with the plugin manager
59
59
 
60
- The package is published on npm as version 0.1.1:
60
+ The package is published on npm:
61
61
 
62
62
  ```
63
63
  plugin_manager install_bundle target=dsh-hackertube
@@ -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
@@ -57,7 +57,7 @@ npm 上取不到就改用 target=github:chow651/dsh-hackertube。
57
57
 
58
58
  ### 用插件管理器安装
59
59
 
60
- 包已发布到 npm,版本 0.1.1:
60
+ 包已发布到 npm:
61
61
 
62
62
  ```
63
63
  plugin_manager install_bundle target=dsh-hackertube
@@ -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
- '返回标题、频道、时长、播放量、HN 推荐理由和链接。',
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 items = await loadList()
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
- const scored = items
2982
- .map((it) => {
2983
- const hay = [it.t, it.c, (it.tags || []).join(' '), (it.cats || []).join(' ')]
2984
- .join(' ')
2985
- .toLowerCase()
2986
- let score = 0
2987
- for (const t of terms) if (hay.includes(t)) score += 1
2988
- return { it, score }
2989
- })
2990
- .filter((x) => x.score > 0)
2991
- .sort((a, b) => b.score - a.score || (b.it.w || 0) - (a.it.w || 0))
2992
- .slice(0, Math.min(Number(args.limit) || 5, 15))
2993
- if (!scored.length) return '没有找到匹配的视频。'
2994
- return scored
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 mins = Math.round((it.d || 0) / 60)
2998
- return (
2999
- `${i + 1}. ${it.t} — ${it.c} · ${mins} 分钟 · ${it.w || 0} 次观看\n` +
3000
- ` https://www.youtube.com/watch?v=${it.v}`
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() })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-hackertube",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "DeepSeek Harness 插件:在面板里看 Hacker News 二十年推荐的 6454 个技术视频",
5
5
  "type": "module",
6
6
  "license": "MIT",