dsh-bili-scroll 0.1.7 → 2.0.4
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/CONTRIBUTORS.md +18 -0
- package/LICENSE +21 -31
- package/LICENSE.bak-20261020 +31 -0
- package/NOTICE.md +18 -0
- package/README.md +39 -18
- package/client.js +343 -84
- package/index.js +1298 -93
- package/lib/asr.py +3 -0
- package/lib/dm.mjs +101 -0
- package/lib/favops.py +84 -0
- package/lib/login.mjs +19 -0
- package/lib/state.mjs +39 -8
- package/lib/uid.mjs +61 -0
- package/lib/verify.mjs +1 -1
- package/package.json +4 -2
- package/prompts/duty.md +4 -1
- package/vendor/bilibili-mcp/mcp_server.py +36 -312
package/lib/asr.py
CHANGED
|
@@ -65,6 +65,9 @@ def fetch_audio(url: str, tmp: str, tag: str) -> str:
|
|
|
65
65
|
# ⭐ nopart:不写 .part 再改名,直接落最终文件。
|
|
66
66
|
# 沙箱里"新建文件 + 改名"会被拒,而这条能绕开(2026-10-14 值班那一轮自己试出来的)
|
|
67
67
|
'nopart': True,
|
|
68
|
+
# [2026-10-20 · 参考社区 PR #23] 进度条默认打到 stdout ⇒ 调用方"stdout 非空=成功"恒真 ✗
|
|
69
|
+
# 我们是读文件走的,把进度关掉,别污染输出
|
|
70
|
+
'noprogress': True,
|
|
68
71
|
'quiet': True,
|
|
69
72
|
'no_warnings': True,
|
|
70
73
|
'noplaylist': True,
|
package/lib/dm.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/dm.mjs —— B站 私信:读会话 / 读消息 / 发消息
|
|
3
|
+
*
|
|
4
|
+
* 为什么要它(2026-10-21 起):「私信双向」= 使用者在 B站 私信里跟它说话,它能看见并回一句。
|
|
5
|
+
* 接口是 2026-10-19 实测通过的(见记忆):**只读、纯代码**,不花 token;
|
|
6
|
+
* 只有真有人发来新消息时才去唤醒模型,所以平时零成本 ✓
|
|
7
|
+
*
|
|
8
|
+
* 接口(都要带登录 Cookie,且 GET):
|
|
9
|
+
* ① 列会话:/session_svr/v1/session_svr/get_sessions?session_type=1&group_id=0&Build=0&mobi_app=web
|
|
10
|
+
* ② 读消息:/svr_sync/v1/svr_sync/fetch_session_msgs?talker_id=<uid>&session_type=1&size=<n>&build=0&mobi_app=web
|
|
11
|
+
* ③ 发消息:走 vendor 里的 bili_send_message(不在这份文件里)
|
|
12
|
+
* ⚠️ 别用 /x/im/web/session/list(404)、/get_msgs(404)、fetch_session_msgs 用 POST(405)
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
|
|
17
|
+
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36'
|
|
18
|
+
|
|
19
|
+
export function loadCred(stateDir) {
|
|
20
|
+
const f = join(stateDir, 'bili_credential.json')
|
|
21
|
+
if (!existsSync(f)) return null
|
|
22
|
+
try {
|
|
23
|
+
const c = JSON.parse(readFileSync(f, 'utf8'))
|
|
24
|
+
return c?.sessdata ? c : null
|
|
25
|
+
} catch { return null }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function headers(cred) {
|
|
29
|
+
return {
|
|
30
|
+
'User-Agent': UA,
|
|
31
|
+
Referer: 'https://message.bilibili.com/',
|
|
32
|
+
Origin: 'https://message.bilibili.com',
|
|
33
|
+
Cookie: `SESSDATA=${cred.sessdata}; bili_jct=${cred.bili_jct}; buvid3=${cred.buvid3}`,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function jget(url, cred) {
|
|
38
|
+
const r = await fetch(url, { headers: headers(cred) })
|
|
39
|
+
const j = await r.json().catch(() => null)
|
|
40
|
+
if (!j || j.code !== 0) throw new Error(`HTTP ${r.status} code=${j?.code} msg=${j?.message || ''}`)
|
|
41
|
+
return j.data || {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 会话列表:[{ uid, unread, lastTs, preview }] */
|
|
45
|
+
export async function listSessions(cred) {
|
|
46
|
+
const d = await jget('https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions?session_type=1&group_id=0&Build=0&mobi_app=web', cred)
|
|
47
|
+
const list = d.session_list || []
|
|
48
|
+
return list.map((s) => {
|
|
49
|
+
let preview = ''
|
|
50
|
+
try {
|
|
51
|
+
const c = s.last_msg?.content ? JSON.parse(s.last_msg.content) : null
|
|
52
|
+
preview = String(c?.content || c?.text || '').slice(0, 80)
|
|
53
|
+
} catch { /* 非 JSON 就留空 */ }
|
|
54
|
+
return {
|
|
55
|
+
uid: Number(s.talker_id) || 0,
|
|
56
|
+
unread: Number(s.unread_count) || 0,
|
|
57
|
+
lastTs: Number(s.last_msg?.timestamp) || 0,
|
|
58
|
+
preview,
|
|
59
|
+
}
|
|
60
|
+
}).filter((x) => x.uid > 0)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 读某个会话最近的 n 条:[{ key, ts, fromMe, text }] */
|
|
64
|
+
export async function fetchMsgs(cred, talkerId, size = 10) {
|
|
65
|
+
const url = `https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs?talker_id=${Number(talkerId)}`
|
|
66
|
+
+ `&session_type=1&size=${Math.max(1, Math.min(50, size))}&build=0&mobi_app=web`
|
|
67
|
+
const d = await jget(url, cred)
|
|
68
|
+
const list = (d.messages || []).slice().reverse() // 接口是倒序的
|
|
69
|
+
return list.map((m) => {
|
|
70
|
+
const mt = Number(m.msg_type) || 1
|
|
71
|
+
let text = ''
|
|
72
|
+
let isCard = false
|
|
73
|
+
try {
|
|
74
|
+
const c = JSON.parse(m.content || '{}')
|
|
75
|
+
// ⭐ 2026-10-21:分享卡片是 msg_type=7(视频)/ 8,它的 content 里**没有 content 字段**,
|
|
76
|
+
// 只有 author / title / jump_url / thumb ⇒ 以前被当成"空消息"直接过滤掉,等于收不到分享 ✗
|
|
77
|
+
if ((mt === 7 || mt === 8) && (c.jump_url || c.bvid || c.author || c.title)) {
|
|
78
|
+
isCard = true
|
|
79
|
+
const bv = String(c.bvid || '') || (String(c.jump_url || '').match(/BV[0-9A-Za-z]{10}/) || [])[0] || ''
|
|
80
|
+
const title = String(c.title || c.headline || c.desc || '').slice(0, 80)
|
|
81
|
+
const who = String(c.author || '').slice(0, 30)
|
|
82
|
+
text = '[分享视频] ' + (title || '(无标题)') + (who ? ' / ' + who : '') + (bv ? ' ' + bv : '')
|
|
83
|
+
} else {
|
|
84
|
+
text = String(c.content || c.text || '')
|
|
85
|
+
}
|
|
86
|
+
} catch { text = String(m.content || '') }
|
|
87
|
+
if (!text) text = '[非文字消息]'
|
|
88
|
+
return {
|
|
89
|
+
key: String(m.msg_key || m.msg_seqno || m.timestamp),
|
|
90
|
+
ts: Number(m.timestamp) || 0,
|
|
91
|
+
fromMe: Number(m.sender_uid) === Number(cred.dedeuserid),
|
|
92
|
+
kind: isCard ? mt : 1,
|
|
93
|
+
text: text.slice(0, 500),
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 我自己的 uid(凭证里有) */
|
|
99
|
+
export function myUid(cred) {
|
|
100
|
+
return Number(cred?.dedeuserid) || 0
|
|
101
|
+
}
|
package/lib/favops.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""favops.py —— 「刷B站」的收藏边车(宿主直接调,不经过 MCP,模型拿不到这只手)
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
python favops.py folders # 列收藏夹 → 末行 JSON {"ok":true,"folders":[{id,title,count}]}
|
|
6
|
+
python favops.py add <bvid> <fid> # 收藏 → {"ok":true} / {"ok":false,"error":"..."}
|
|
7
|
+
"""
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
14
|
+
|
|
15
|
+
# 凭证找两处:优先**当前工作目录**(宿主 spawn 时 cwd = 插件家,凭证就在那儿),
|
|
16
|
+
# 退回脚本旁边(脚本被铺到插件家后两者一致)
|
|
17
|
+
CRED_CANDIDATES = [
|
|
18
|
+
os.path.join(os.getcwd(), 'bili_credential.json'),
|
|
19
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'bili_credential.json'),
|
|
20
|
+
]
|
|
21
|
+
CRED_FILE = next((p for p in CRED_CANDIDATES if os.path.exists(p)), CRED_CANDIDATES[0])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def out(obj):
|
|
25
|
+
print(json.dumps(obj, ensure_ascii=False))
|
|
26
|
+
sys.exit(0)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load():
|
|
30
|
+
from bilibili_api import Credential
|
|
31
|
+
if not os.path.exists(CRED_FILE):
|
|
32
|
+
out({'ok': False, 'error': '还没扫码登录(bili_credential.json 不存在)'})
|
|
33
|
+
d = json.load(open(CRED_FILE, encoding='utf-8'))
|
|
34
|
+
return Credential(
|
|
35
|
+
sessdata=d['sessdata'],
|
|
36
|
+
bili_jct=d['bili_jct'],
|
|
37
|
+
buvid3=d.get('buvid3', ''),
|
|
38
|
+
dedeuserid=str(d['dedeuserid']),
|
|
39
|
+
), int(d['dedeuserid'])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def folders():
|
|
43
|
+
from bilibili_api import favorite_list
|
|
44
|
+
cred, uid = load()
|
|
45
|
+
res = await favorite_list.get_video_favorite_list(uid, credential=cred)
|
|
46
|
+
items = (res or {}).get('list') or []
|
|
47
|
+
out({'ok': True, 'folders': [{'id': it.get('id'), 'title': it.get('title'), 'count': it.get('media_count', 0)} for it in items]})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def add(bvid, fid):
|
|
51
|
+
from bilibili_api import video as ba_video
|
|
52
|
+
cred, _ = load()
|
|
53
|
+
v = ba_video.Video(bvid=bvid, credential=cred)
|
|
54
|
+
info = await v.get_info()
|
|
55
|
+
aid = int(info['aid'])
|
|
56
|
+
res = await favorite_list.FavoriteList(aid, credential=cred).get_info()
|
|
57
|
+
try:
|
|
58
|
+
await favorite_list.FavoriteListResource(aid, type_=2, credential=cred).add(fid)
|
|
59
|
+
except Exception as error: # noqa: BLE001
|
|
60
|
+
msg = str(error)
|
|
61
|
+
# 已经收藏过不算失败
|
|
62
|
+
if '已经' in msg or 'already' in msg.lower():
|
|
63
|
+
out({'ok': True, 'already': True})
|
|
64
|
+
out({'ok': False, 'error': msg[:200]})
|
|
65
|
+
out({'ok': True})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
args = sys.argv[1:]
|
|
70
|
+
if not args:
|
|
71
|
+
out({'ok': False, 'error': '没有子命令(folders / add)'})
|
|
72
|
+
try:
|
|
73
|
+
if args[0] == 'folders':
|
|
74
|
+
asyncio.run(folders())
|
|
75
|
+
elif args[0] == 'add' and len(args) >= 3:
|
|
76
|
+
asyncio.run(add(args[1], int(args[2])))
|
|
77
|
+
else:
|
|
78
|
+
out({'ok': False, 'error': '子命令不认识:' + ' '.join(args)})
|
|
79
|
+
except Exception as error: # noqa: BLE001
|
|
80
|
+
out({'ok': False, 'error': str(error)[:300]})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == '__main__':
|
|
84
|
+
main()
|
package/lib/login.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { spawn } from 'node:child_process'
|
|
18
18
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { execFileSync } from 'node:child_process'
|
|
19
20
|
import { join } from 'node:path'
|
|
20
21
|
|
|
21
22
|
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36'
|
|
@@ -75,6 +76,23 @@ export async function loginPoll(key) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/** 把凭证写进 MCP 读的那个文件(家目录里的 bili_credential.json) */
|
|
79
|
+
/**
|
|
80
|
+
* ⭐ 2026-10-23 安全加固(审计 + issue #67):凭证文件权限收紧到"只有自己能读" ——
|
|
81
|
+
* Linux/macOS: chmod 0600;Windows: icacls 去掉继承(只留当前用户)。
|
|
82
|
+
* 失败也不报错(有些文件系统不支持),但会返回 false 让调用方可以说明。
|
|
83
|
+
*/
|
|
84
|
+
export function hardenPerms(file) {
|
|
85
|
+
try {
|
|
86
|
+
if (process.platform === 'win32') {
|
|
87
|
+
const user = process.env.USERNAME || ''
|
|
88
|
+
execFileSync('icacls', [file, '/inheritance:r', '/grant:r', user + ':F'], { stdio: 'ignore', windowsHide: true })
|
|
89
|
+
} else {
|
|
90
|
+
execFileSync('chmod', ['600', file], { stdio: 'ignore' })
|
|
91
|
+
}
|
|
92
|
+
return true
|
|
93
|
+
} catch { return false }
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
export function saveCredential(stateDir, credential) {
|
|
79
97
|
mkdirSync(stateDir, { recursive: true })
|
|
80
98
|
const file = join(stateDir, 'bili_credential.json')
|
|
@@ -84,6 +102,7 @@ export function saveCredential(stateDir, credential) {
|
|
|
84
102
|
buvid3: credential.buvid3 || '',
|
|
85
103
|
dedeuserid: credential.dedeuserid ?? '',
|
|
86
104
|
}, null, 2), 'utf8')
|
|
105
|
+
try { hardenPerms(file) } catch { /* 收紧失败也不影响登录 */ }
|
|
87
106
|
return file
|
|
88
107
|
}
|
|
89
108
|
|
package/lib/state.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* ============================================================================
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs'
|
|
13
|
+
import { createHash as nodeCreateHash } from 'node:crypto'
|
|
13
14
|
import { createHash } from 'node:crypto'
|
|
14
15
|
import { join } from 'node:path'
|
|
15
16
|
import { homedir } from 'node:os'
|
|
@@ -56,19 +57,49 @@ export function writeJson(file, value) {
|
|
|
56
57
|
writeFileSync(file, JSON.stringify(value, null, 2), 'utf8')
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* 把 vendored 的脚本铺进家里。
|
|
62
|
+
*
|
|
63
|
+
* ⭐ 2026-10-22(用户 @俺爱啃猪蹄子 指出策略不一致):以前是"内容不一样就覆盖" ⇒ **用户改过的直接丢** ✗,
|
|
64
|
+
* 而章程 duty.md 是"改过就保留" —— 同一个插件两套规矩,很怪。
|
|
65
|
+
* 现在改成**记住"上次我们发出去那份的指纹"**(`.shipped-<文件名>.sha`):
|
|
66
|
+
* · 家里那份 == 我们发的 ⇒ 正常升级覆盖 ✓
|
|
67
|
+
* · 家里那份 ≠ 我们发的 ⇒ **说明用户改过** ⇒ 不覆盖,新版写到 `<文件>.new` 并提示 ✓
|
|
68
|
+
* · 没有指纹(老版本升级上来)⇒ 按老行为覆盖一次并记指纹(从这一版起才有"改过"的概念)✓
|
|
69
|
+
*/
|
|
60
70
|
export function ensureHomeFiles(pluginDir, stateDir, log = () => {}) {
|
|
61
71
|
const copies = [
|
|
62
|
-
[join(pluginDir, 'vendor', 'bilibili-mcp', 'mcp_server.py'), join(stateDir, 'mcp_server.py')
|
|
63
|
-
[join(pluginDir, 'lib', 'asr.py'), join(stateDir, 'asr.py')
|
|
72
|
+
[join(pluginDir, 'vendor', 'bilibili-mcp', 'mcp_server.py'), join(stateDir, 'mcp_server.py')],
|
|
73
|
+
[join(pluginDir, 'lib', 'asr.py'), join(stateDir, 'asr.py')],
|
|
74
|
+
[join(pluginDir, 'lib', 'favops.py'), join(stateDir, 'favops.py')],
|
|
64
75
|
]
|
|
65
|
-
|
|
76
|
+
const sha = (buf) => {
|
|
77
|
+
try { return nodeCreateHash('sha256').update(buf).digest('hex') } catch { return '' }
|
|
78
|
+
}
|
|
79
|
+
for (const [from, to] of copies) {
|
|
66
80
|
try {
|
|
67
81
|
if (!existsSync(from)) { log(`缺文件:${from}`); continue }
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
82
|
+
const incoming = readFileSync(from, 'utf8')
|
|
83
|
+
const markFile = join(stateDir, '.shipped-' + to.split(/[\\/]/).pop() + '.sha')
|
|
84
|
+
const shipped = existsSync(markFile) ? readFileSync(markFile, 'utf8').trim() : ''
|
|
85
|
+
const writeMark = (txt) => { try { writeFileSync(markFile, sha(Buffer.from(txt, 'utf8')), 'utf8') } catch { /* 记不上就下次再说 */ } }
|
|
86
|
+
if (!existsSync(to)) {
|
|
87
|
+
copyFileSync(from, to); writeMark(incoming); log(`已铺好:${to}`); continue
|
|
88
|
+
}
|
|
89
|
+
const onDisk = readFileSync(to, 'utf8')
|
|
90
|
+
if (onDisk === incoming) { if (!shipped) writeMark(incoming); continue } // 本来就是同一份
|
|
91
|
+
if (shipped && sha(Buffer.from(onDisk, 'utf8')) === shipped) {
|
|
92
|
+
// 家里那份就是我们上次发的 ⇒ 正常升级
|
|
93
|
+
copyFileSync(from, to); writeMark(incoming); log(`已更新:${to}`)
|
|
94
|
+
} else if (!shipped) {
|
|
95
|
+
// 老版本升级上来,没有指纹:按老行为覆盖一次,从此开始记指纹
|
|
96
|
+
copyFileSync(from, to); writeMark(incoming); log(`已更新(首次记录指纹):${to}`)
|
|
97
|
+
} else {
|
|
98
|
+
// 用户改过 ⇒ 不覆盖,新版放旁边
|
|
99
|
+
const side = to + '.new'
|
|
100
|
+
try { writeFileSync(side, incoming, 'utf8') } catch { /* 写不了就算了 */ }
|
|
101
|
+
log(`注意:${to} 你改过,这次没覆盖;新版放在 ${side}`)
|
|
102
|
+
}
|
|
72
103
|
} catch (e) { log(`铺 ${to} 失败:${e?.message ?? e}`) }
|
|
73
104
|
}
|
|
74
105
|
}
|
package/lib/uid.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** lib/uid.mjs —— 用昵称查 B站 UID(私信默认化之后,用户只填「@ 谁」就得能发)
|
|
2
|
+
* 为什么要它:默认投递改成「私信」以后,如果只认 dmUid,那么没填 UID 的用户会**一条都收不到** ✗
|
|
3
|
+
* 接口:/x/web-interface/search/type?search_type=bili_user&keyword=<昵称>(要带登录 cookie)
|
|
4
|
+
* 策略:先精确匹配 uname,再退到第一个结果;结果缓存进 <家>/uid-cache.json,避免每轮都打接口
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
|
|
9
|
+
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36'
|
|
10
|
+
|
|
11
|
+
function loadCred(stateDir) {
|
|
12
|
+
const f = join(stateDir, 'bili_credential.json')
|
|
13
|
+
if (!existsSync(f)) return null
|
|
14
|
+
try {
|
|
15
|
+
const c = JSON.parse(readFileSync(f, 'utf8'))
|
|
16
|
+
return c?.sessdata ? c : null
|
|
17
|
+
} catch { return null }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readCache(stateDir) {
|
|
21
|
+
try { return JSON.parse(readFileSync(join(stateDir, 'uid-cache.json'), 'utf8')) || {} } catch { return {} }
|
|
22
|
+
}
|
|
23
|
+
function writeCache(stateDir, obj) {
|
|
24
|
+
try { writeFileSync(join(stateDir, 'uid-cache.json'), JSON.stringify(obj, null, 2), 'utf8') } catch { /* 写不了就算了 */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @returns {Promise<number>} 0 = 没查到 */
|
|
28
|
+
export async function resolveUid(stateDir, name, log = () => {}) {
|
|
29
|
+
const q = String(name || '').trim()
|
|
30
|
+
if (!q) return 0
|
|
31
|
+
const cache = readCache(stateDir)
|
|
32
|
+
if (Number(cache[q]) > 0) return Number(cache[q])
|
|
33
|
+
const cred = loadCred(stateDir)
|
|
34
|
+
if (!cred) { log('查 UID 失败:还没扫码登录 B站'); return 0 }
|
|
35
|
+
const url = 'https://api.bilibili.com/x/web-interface/search/type?search_type=bili_user&page=1&keyword='
|
|
36
|
+
+ encodeURIComponent(q)
|
|
37
|
+
try {
|
|
38
|
+
const r = await fetch(url, {
|
|
39
|
+
headers: {
|
|
40
|
+
'User-Agent': UA,
|
|
41
|
+
Referer: 'https://search.bilibili.com/',
|
|
42
|
+
Cookie: `SESSDATA=${cred.sessdata}; bili_jct=${cred.bili_jct}; buvid3=${cred.buvid3}`,
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
const j = await r.json().catch(() => null)
|
|
46
|
+
const list = j?.data?.result || []
|
|
47
|
+
if (!list.length) { log(`查 UID:搜「${q}」没有结果`); return 0 }
|
|
48
|
+
const exact = list.find((u) => String(u.uname) === q)
|
|
49
|
+
const pick = exact || list[0]
|
|
50
|
+
const mid = Number(pick.mid) || 0
|
|
51
|
+
if (mid > 0) {
|
|
52
|
+
cache[q] = mid
|
|
53
|
+
writeCache(stateDir, cache)
|
|
54
|
+
log(`查 UID:「${q}」→ ${mid}(${pick.uname}${exact ? ',精确匹配' : ',第一个结果'})`)
|
|
55
|
+
}
|
|
56
|
+
return mid
|
|
57
|
+
} catch (e) {
|
|
58
|
+
log('查 UID 失败:' + String(e?.message ?? e))
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
}
|
package/lib/verify.mjs
CHANGED
|
@@ -122,7 +122,7 @@ export async function verifyRecent(stateDir, { max = 6, log = () => {} } = {}) {
|
|
|
122
122
|
if (!cred) return { checked: [], skipped: 0, text: '⚠️ 还没扫码登录,没法复核(复核要用你自己的登录态)' }
|
|
123
123
|
|
|
124
124
|
const rows = parseSeen(readFileSync(seenFile, 'utf8'))
|
|
125
|
-
if (!rows.length) return { checked: [], skipped: 0, text: '
|
|
125
|
+
if (!rows.length) return { checked: [], skipped: 0, text: '还没发过评论,暂时不用核(等你开了「自动评论并 @ 你」、并且真的发出去过一条之后,这里才有东西可核)' }
|
|
126
126
|
|
|
127
127
|
// 状态账本:已经确认真见过的,7 天内不重复查
|
|
128
128
|
const stateFile = join(stateDir, 'verify-state.json')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-bili-scroll",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "DSH 插件:每天替你刷一轮 B站 —— 读字幕挑出值得的、写一份日报,值得的就 @ 你",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
"README.md",
|
|
32
32
|
"AGENTS.md",
|
|
33
33
|
"selfcheck.mjs",
|
|
34
|
-
"LICENSE"
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"NOTICE.md",
|
|
36
|
+
"CONTRIBUTORS.md"
|
|
35
37
|
],
|
|
36
38
|
"engines": {
|
|
37
39
|
"dsh": ">=0.1.7-rc.1",
|
package/prompts/duty.md
CHANGED
|
@@ -90,7 +90,10 @@
|
|
|
90
90
|
- 顺手翻一眼评论
|
|
91
91
|
4. 写一份 Markdown 报告:
|
|
92
92
|
⭐⭐ **每条挑中的视频单独一段,这一段的第一行固定写成 `### BV号`**(例:`### BV1xxxxxxxxx` —— BV 号就写在
|
|
93
|
-
`###` 后面、顶格);下一行再写 **标题 / UP / 链接 / 播放量 / 为什么值得(两三句)/ 你自己的一句话**,
|
|
93
|
+
`###` 后面、顶格);下一行再写 **标题 / UP / 链接 / 播放量 / **档位** / 为什么值得(两三句)/ 你自己的一句话**,
|
|
94
|
+
⭐ **`**档位**:<值得看 | 可以看 | 不值当>` 这一行必须写、而且只能用这三个词**(不许自己编、不许写分数)——
|
|
95
|
+
宿主会把它读进「它刷过的视频」列表,用来筛选。判据:**值得看** = 我真心希望他花这十几分钟;
|
|
96
|
+
**可以看** = 有信息但不看也不亏;**不值当** = 标题党 / 复读 / 水分大(这种通常根本不会进报告,进了就要标出来)。
|
|
94
97
|
结尾给一句总结。
|
|
95
98
|
⭐⭐ **这一行是宿主数配额用的**:`### BV…` 的行数 = 你这一轮挑中的条数(宿主拿它跟「每天最多几条」对账,
|
|
96
99
|
也算你下一轮还能挑几条)⇒ **挑中的一条都不能漏写这行**;
|