dsh-bili-watch 0.2.15 → 0.2.16
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 +2 -1
- package/index.js +19 -2
- package/package.json +77 -77
- package/tools/look.mjs +83 -10
package/README.md
CHANGED
|
@@ -146,7 +146,8 @@ macOS/Linux 的 ffmpeg 用系统包管理器装,脚本只给提示。其余目
|
|
|
146
146
|
|
|
147
147
|
用户的消息里只有一条 B 站视频链接(BV 号或 `b23.tv` 短链)时,插件往消息流里补一句提示,告诉 agent 有这条链接可读、`peek` 最便宜。它**不会**替用户调用工具,也不会自动跑起来——只是让「贴链接不说话」这种场景不容易被漏掉。
|
|
148
148
|
|
|
149
|
-
- `b23.tv`
|
|
149
|
+
- `b23.tv` 短链会先自动展开:问公共 DNS(`223.5.5.5` → `119.29.29.29` → `1.1.1.1`)拿真实 IP,再直连读 302 里的 BV 号,绕开系统 DNS;展开失败才提示用户换成浏览器地址栏里的完整链接。
|
|
150
|
+
- 为什么要兜底:部分内网 DNS 会把 `b23.tv` 答成 `31.13.x.x`(Facebook 段),这种环境下只有公共 DNS 这条路能展开(2026-10-23 实测)。
|
|
150
151
|
- 关掉:`deps/ui-config.json` 里 `"autoHint": false`。
|
|
151
152
|
- 消息里除了链接还有别的文字(问题、指令)时不提示,避免打断正常对话。
|
|
152
153
|
|
package/index.js
CHANGED
|
@@ -503,6 +503,21 @@ function sendJson(res, code, obj) {
|
|
|
503
503
|
res.end(JSON.stringify(obj))
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
/** 写端点的同源闸:Origin 主机名须等于 Host,回环互放;sec-fetch-site 只认 same-origin/none。 */
|
|
507
|
+
const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1", "[::1]"])
|
|
508
|
+
function sameOriginBlocked(req) {
|
|
509
|
+
const host = String(req.headers?.host ?? "").split(":")[0]
|
|
510
|
+
const origin = req.headers?.origin
|
|
511
|
+
if (origin) {
|
|
512
|
+
let oh = ""
|
|
513
|
+
try { oh = new URL(origin).hostname } catch { return "非法 Origin 头" }
|
|
514
|
+
if (oh !== host && !(LOOPBACK.has(host) && LOOPBACK.has(oh))) return "跨源访问被拒绝"
|
|
515
|
+
}
|
|
516
|
+
const site = req.headers?.["sec-fetch-site"]
|
|
517
|
+
if (site && site !== "same-origin" && site !== "none") return `跨站请求被拒绝(sec-fetch-site=${site})`
|
|
518
|
+
return null
|
|
519
|
+
}
|
|
520
|
+
|
|
506
521
|
/** 读 POST 的小 JSON body;读不到就返回 {},端点自己决定默认值。 */
|
|
507
522
|
function readJsonBody(req) {
|
|
508
523
|
return new Promise((resolve) => {
|
|
@@ -627,8 +642,8 @@ function apply(ctx) {
|
|
|
627
642
|
const r = matchSingleVideoLink(text)
|
|
628
643
|
if (!r.hit) return null
|
|
629
644
|
const line = r.kind === 'short'
|
|
630
|
-
// 短链:look.mjs
|
|
631
|
-
? '[bili-watch] 这条消息只有一条 b23.tv
|
|
645
|
+
// 短链:look.mjs 会先按公共 DNS 展开;展开失败才让用户贴 BV 号
|
|
646
|
+
? '[bili-watch] 这条消息只有一条 b23.tv 短链。要读内容就调 bili_watch,它会先按公共 DNS 展开短链;只有展开失败时才请用户贴 BV 号。不要凭短链猜内容。'
|
|
632
647
|
: `[bili-watch] 这条消息只有一条 B 站视频链接(BV ${r.bv})。要读内容就调 bili_watch(command=peek 最便宜);用户没说要看时不要自己跑。`
|
|
633
648
|
return createUserMessage({
|
|
634
649
|
content: [{ type: 'text', text: line }],
|
|
@@ -779,6 +794,8 @@ function apply(ctx) {
|
|
|
779
794
|
kind: 'prefix',
|
|
780
795
|
path: '/api/bili-watch',
|
|
781
796
|
handler: async (req, res) => {
|
|
797
|
+
const blocked = sameOriginBlocked(req)
|
|
798
|
+
if (blocked) return sendJson(res, 403, { ok: false, error: blocked })
|
|
782
799
|
try {
|
|
783
800
|
const url = new URL(req.url, 'http://127.0.0.1')
|
|
784
801
|
if (req.method === 'GET' && url.pathname === '/api/bili-watch/status') {
|
package/package.json
CHANGED
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-bili-watch",
|
|
3
|
+
"version": "0.2.16",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Reads Bilibili (and generic URLs via yt-dlp) into text for DSH agents: search, hot, metadata, comments, danmaku, AI subtitles, local ASR, frame OCR.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"bili-watch": "tools/look.mjs"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./index.js",
|
|
13
|
+
"./client": "./client.js",
|
|
14
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"dsh": {
|
|
18
|
+
"bundle": {
|
|
19
|
+
"patch": "./cordis.patch.yml"
|
|
20
|
+
},
|
|
21
|
+
"client": {
|
|
22
|
+
"platform": "web",
|
|
23
|
+
"inject": [
|
|
24
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
25
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"index.js",
|
|
31
|
+
"client.js",
|
|
32
|
+
"cordis.patch.yml",
|
|
33
|
+
"tools/",
|
|
34
|
+
"scripts/",
|
|
35
|
+
"deps/.gitkeep",
|
|
36
|
+
"deps/_bili-asr.py",
|
|
37
|
+
"auto-trigger.js",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"keywords": [
|
|
41
|
+
"bilibili",
|
|
42
|
+
"video",
|
|
43
|
+
"asr",
|
|
44
|
+
"ocr",
|
|
45
|
+
"danmaku",
|
|
46
|
+
"dsh",
|
|
47
|
+
"deepseek",
|
|
48
|
+
"subtitles"
|
|
49
|
+
],
|
|
50
|
+
"author": "Sacredbluedragon",
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/Sacredbluedragon/dsh-bili-watch.git"
|
|
55
|
+
},
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/Sacredbluedragon/dsh-bili-watch/issues"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/Sacredbluedragon/dsh-bili-watch#readme",
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": "\u003e=22"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"qrcode": "^1.5.4"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@deepseek-ai/dsh": "\u003e=0.2.0-rc.1 \u003c0.3.0-0",
|
|
68
|
+
"@deepseek-ai/dsh-tools": "\u003e=0.2.0-rc.1 \u003c0.3.0-0"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"@deepseek-ai/dsh": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"@deepseek-ai/dsh-tools": {
|
|
75
|
+
"optional": true
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
78
|
}
|
package/tools/look.mjs
CHANGED
|
@@ -318,14 +318,25 @@ async function downloadTo(url, dest) {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
// 同目录并发保护:拿到锁的进程抽帧,另一个等它写完再按缓存判
|
|
321
|
+
// 判锁的主人是否还活着:被 Ctrl-C / 超时 kill 的进程会留下 .lock,不判活就永远等一个不存在的进程
|
|
322
|
+
function pidAlive(pid) {
|
|
323
|
+
if (!Number.isInteger(pid) || pid <= 0) return false
|
|
324
|
+
try { process.kill(pid, 0); return true } catch (e) { return e.code === 'EPERM' }
|
|
325
|
+
}
|
|
321
326
|
function tryLock(dir) {
|
|
322
327
|
const f = join(dir, '.lock')
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
+
for (let i = 0; i < 2; i++) {
|
|
329
|
+
try {
|
|
330
|
+
writeFileSync(f, String(process.pid), { flag: 'wx' })
|
|
331
|
+
return f
|
|
332
|
+
} catch {
|
|
333
|
+
let owner = 0
|
|
334
|
+
try { owner = Number(readFileSync(f, 'utf8').trim()) } catch {}
|
|
335
|
+
if (pidAlive(owner)) return null
|
|
336
|
+
unlock(f)
|
|
337
|
+
}
|
|
328
338
|
}
|
|
339
|
+
return null
|
|
329
340
|
}
|
|
330
341
|
function unlock(f) {
|
|
331
342
|
try {
|
|
@@ -1585,12 +1596,74 @@ const flag = (name) => argv.includes(`--${name}`)
|
|
|
1585
1596
|
|
|
1586
1597
|
// ── 通用入口(yt-dlp):B 站外任意 http(s) 视频 ──────────────────────
|
|
1587
1598
|
// 目标可以是裸 BV 号,也可以是 App/网页分享出来的整条链接(带 buvid/spmid/unique_k 等参数)。
|
|
1588
|
-
//
|
|
1589
|
-
|
|
1599
|
+
// 短链 b23.tv 先按公共 DNS 展开(本机内网 DNS 会把它答成 31.13.x.x 这类错误地址,见 2026-10-23 实测)。
|
|
1600
|
+
const B23_DNS_SERVERS = ['223.5.5.5', '119.29.29.29', '1.1.1.1']
|
|
1601
|
+
|
|
1602
|
+
/**
|
|
1603
|
+
* 展开 b23.tv 短链:绕过系统 DNS,直接问公共 DNS 拿 IP,再按 IP 直连读 30x 的 Location。
|
|
1604
|
+
* 失败返回 null,由调用方给出可执行的替代方案(贴 BV 号)。
|
|
1605
|
+
*/
|
|
1606
|
+
async function expandB23(shortUrl) {
|
|
1607
|
+
const m = String(shortUrl).match(/b23\.tv\/([A-Za-z0-9]+)/i)
|
|
1608
|
+
if (!m) return null
|
|
1609
|
+
const code = m[1]
|
|
1610
|
+
let ips = []
|
|
1611
|
+
try {
|
|
1612
|
+
const { Resolver } = await import('node:dns/promises')
|
|
1613
|
+
const resolver = new Resolver({ timeout: 3000, tries: 2 })
|
|
1614
|
+
resolver.setServers(B23_DNS_SERVERS)
|
|
1615
|
+
ips = await resolver.resolve4('b23.tv')
|
|
1616
|
+
} catch { ips = [] }
|
|
1617
|
+
// 31.13.0.0/16 是污染常见落点(Facebook 段),直接剔掉
|
|
1618
|
+
ips = (ips || []).filter((ip) => /^\d+\.\d+\.\d+\.\d+$/.test(ip) && !/^31\.13\./.test(ip)).slice(0, 3)
|
|
1619
|
+
if (ips.length === 0) return null
|
|
1620
|
+
const { request } = await import('node:https')
|
|
1621
|
+
for (const ip of ips) {
|
|
1622
|
+
let loc = ''
|
|
1623
|
+
let body = ''
|
|
1624
|
+
try {
|
|
1625
|
+
const got = await new Promise((resolve, reject) => {
|
|
1626
|
+
const req = request({
|
|
1627
|
+
host: 'b23.tv', servername: 'b23.tv', port: 443, path: `/${code}`,
|
|
1628
|
+
method: 'GET',
|
|
1629
|
+
headers: { host: 'b23.tv', 'user-agent': UA, referer: REF, accept: '*/*' },
|
|
1630
|
+
// Node 20+ 开了 autoSelectFamily 时会用 {all:true} 回调,两种签名都要兼容
|
|
1631
|
+
lookup: (_hostname, opts, cb) => (opts && opts.all
|
|
1632
|
+
? cb(null, [{ address: ip, family: 4 }])
|
|
1633
|
+
: cb(null, ip, 4)),
|
|
1634
|
+
timeout: 6000,
|
|
1635
|
+
autoSelectFamily: false,
|
|
1636
|
+
}, (res) => {
|
|
1637
|
+
let buf = ''
|
|
1638
|
+
res.setEncoding('utf8')
|
|
1639
|
+
res.on('data', (chunk) => {
|
|
1640
|
+
if (buf.length < 8192) buf += chunk
|
|
1641
|
+
else res.destroy()
|
|
1642
|
+
})
|
|
1643
|
+
res.on('end', () => resolve({ location: String(res.headers.location || ''), body: buf }))
|
|
1644
|
+
res.on('error', () => resolve({ location: String(res.headers.location || ''), body: buf }))
|
|
1645
|
+
})
|
|
1646
|
+
req.on('timeout', () => req.destroy(new Error('timeout')))
|
|
1647
|
+
req.on('error', reject)
|
|
1648
|
+
req.end()
|
|
1649
|
+
})
|
|
1650
|
+
loc = got.location
|
|
1651
|
+
body = got.body
|
|
1652
|
+
} catch { continue }
|
|
1653
|
+
// 正常短链是 302 + Location;少数短链返回带跳转的 HTML/JS,再从正文里捞一次
|
|
1654
|
+
const bv = `${loc}\n${body}`.match(/BV[0-9A-Za-z]{10}/)
|
|
1655
|
+
if (bv) return bv[0]
|
|
1656
|
+
}
|
|
1657
|
+
return null
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
async function resolveSource(input) {
|
|
1590
1661
|
const s = String(input ?? '').trim()
|
|
1591
1662
|
if (!s) return { type: 'empty' }
|
|
1592
1663
|
if (/^https?:\/\/b23\.tv\//i.test(s)) {
|
|
1593
|
-
|
|
1664
|
+
const bv = await expandB23(s)
|
|
1665
|
+
if (bv) return { type: 'bilibili', bv }
|
|
1666
|
+
return { type: 'error', message: 'b23.tv 短链展开失败:本机 DNS 把 b23.tv 解析到了错误地址,公共 DNS 也没连上。请在浏览器打开短链,把地址栏里的完整链接复制过来,或直接贴 BV 号。' }
|
|
1594
1667
|
}
|
|
1595
1668
|
const direct = s.match(/BV[0-9A-Za-z]{10}/)
|
|
1596
1669
|
if (direct) return { type: 'bilibili', bv: direct[0] }
|
|
@@ -1815,7 +1888,7 @@ let BV = ''
|
|
|
1815
1888
|
let src = { type: 'empty' }
|
|
1816
1889
|
let srcErr = ''
|
|
1817
1890
|
try {
|
|
1818
|
-
src = resolveSource(pos[1])
|
|
1891
|
+
src = await resolveSource(pos[1])
|
|
1819
1892
|
if (src.type === 'bilibili') BV = src.bv
|
|
1820
1893
|
} catch (e) {
|
|
1821
1894
|
srcErr = e?.message ?? String(e)
|
|
@@ -1845,7 +1918,7 @@ const HELP = `用法:
|
|
|
1845
1918
|
--format:sub 支持 text|srt|json|md|txt(srt/md 带真时间戳,md 的时间戳可跳回原片,txt 是一行一句);
|
|
1846
1919
|
text 支持 text|json|md(json 带时间轴口径:字幕 exact / 转写与 OCR 是估算);
|
|
1847
1920
|
comments 与 danmaku 支持 json(stdout 只有 JSON,可接 jq)。
|
|
1848
|
-
B 站目标可以直接贴分享链接,会自己提取 BV 号;短链 b23.tv/...
|
|
1921
|
+
B 站目标可以直接贴分享链接,会自己提取 BV 号;短链 b23.tv/... 也会先按公共 DNS 展开成 BV 号。
|
|
1849
1922
|
通用视频 URL(YouTube/X 等)需要 yt-dlp:跑 scripts/fetch-deps.ps1 -Target ytdlp 装到 deps\\yt-dlp.exe(或设 BILI_WATCH_YTDLP / 系统 PATH)。
|
|
1850
1923
|
`
|
|
1851
1924
|
try {
|