dsh-bili-scroll 0.1.5 → 0.1.6

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/lib/login.mjs CHANGED
@@ -1,103 +1,103 @@
1
- /**
2
- * lib/login.mjs —— 扫码登录(直接在 JS 里走 B站 passport 接口)
3
- * ============================================================================
4
- * 为什么不用它自带的 `bili_login`:那个把码存成 PNG 就返回,**字段全是空**(踩过一次,见 lesson)。
5
- * 我们按实测跑通的那条来:
6
- * ① GET /x/passport-login/web/qrcode/generate?source=main-fe-header → data.url + data.qrcode_key
7
- * ② 手机扫 → 轮询 /x/passport-login/web/qrcode/poll?qrcode_key=…
8
- * data.code:86101 未扫 / 86090 已扫待确认 / 0 成功 / 86038 过期
9
- * ③ ⭐ **凭证在轮询响应的 `Set-Cookie` 头里**(成功时那条 `data.url` 里没有 SESSDATA,
10
- * 那是别的域的游戏票 —— 别在 url 里找)
11
- * ④ 写成 MCP 认识的四个字段:sessdata / bili_jct / buvid3 / dedeuserid
12
- *
13
- * 二维码图片:交给 venv 里的 Python `qrcode` 画(ASCII 的 URL,走 `-c` 安全;
14
- * 没装就退回"只给 URL",前端会提示先跑「建 venv 装依赖」)。
15
- * ============================================================================
16
- */
17
- import { spawn } from 'node:child_process'
18
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
19
- import { join } from 'node:path'
20
-
21
- const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36'
22
- const GEN = 'https://passport.bilibili.com/x/passport-login/web/qrcode/generate?source=main-fe-header'
23
- const pollUrl = (key) => 'https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=' + encodeURIComponent(key) + '&source=main-fe-header'
24
- const SPI = 'https://api.bilibili.com/x/frontend/finger/spi'
25
-
26
- /** 从 Set-Cookie 数组里抠出我们要的几个 */
27
- function pickCookies(list) {
28
- const jar = {}
29
- const want = { SESSDATA: 'sessdata', bili_jct: 'bili_jct', DedeUserID: 'dedeuserid', buvid3: 'buvid3' }
30
- for (const raw of list ?? []) {
31
- const first = String(raw).split(';')[0] || ''
32
- const i = first.indexOf('=')
33
- if (i <= 0) continue
34
- const name = first.slice(0, i).trim()
35
- const value = first.slice(i + 1).trim()
36
- if (want[name]) jar[want[name]] = value
37
- }
38
- return jar
39
- }
40
-
41
- /** 拿不到 buvid3 时补一张"指纹"(非致命,失败就当空) */
42
- async function ensureBuvid3(jar) {
43
- if (jar.buvid3) return jar
44
- try {
45
- const r = await fetch(SPI, { headers: { 'User-Agent': UA } })
46
- const j = await r.json()
47
- const v = j?.data?.b_3
48
- if (v) jar.buvid3 = v
49
- } catch { /* 拿不到就算了 */ }
50
- return jar
51
- }
52
-
53
- export async function loginStart() {
54
- const r = await fetch(GEN, { headers: { 'User-Agent': UA } })
55
- const j = await r.json()
56
- const data = j?.data
57
- if (!data?.qrcode_key || !data?.url) throw new Error(`生成二维码失败:${j?.message || '返回里没有 qrcode_key'}`)
58
- return { key: data.qrcode_key, url: data.url }
59
- }
60
-
61
- /** 轮询一次。state: waiting / scanned / ok / expired / unknown */
62
- export async function loginPoll(key) {
63
- const res = await fetch(pollUrl(key), { headers: { 'User-Agent': UA }, redirect: 'manual' })
64
- const setCookies = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : []
65
- let body = {}
66
- try { body = await res.json() } catch { /* poll 有时返回空体 */ }
67
- const code = body?.data?.code
68
- if (code === 0) {
69
- const jar = await ensureBuvid3(pickCookies(setCookies))
70
- if (!jar.sessdata) return { state: 'unknown', code, message: '登录成功了,但响应里没有 SESSDATA —— 把这次的结果记下来再排查' }
71
- return { state: 'ok', code, credential: jar }
72
- }
73
- const map = { 86101: 'waiting', 86090: 'scanned', 86038: 'expired' }
74
- return { state: map[code] ?? 'unknown', code, message: body?.message || '' }
75
- }
76
-
77
- /** 把凭证写进 MCP 读的那个文件(家目录里的 bili_credential.json) */
78
- export function saveCredential(stateDir, credential) {
79
- mkdirSync(stateDir, { recursive: true })
80
- const file = join(stateDir, 'bili_credential.json')
81
- writeFileSync(file, JSON.stringify({
82
- sessdata: credential.sessdata ?? '',
83
- bili_jct: credential.bili_jct ?? '',
84
- buvid3: credential.buvid3 || '',
85
- dedeuserid: credential.dedeuserid ?? '',
86
- }, null, 2), 'utf8')
87
- return file
88
- }
89
-
90
- /** 用 Python 的 qrcode 画一张 PNG,返回 base64(拿不到就返回 null) */
91
- export function renderQrPng(pythonPath, stateDir, url) {
92
- return new Promise((resolve) => {
93
- const png = join(stateDir, 'qrcode_login.png')
94
- const code = 'import sys, qrcode; qrcode.make(sys.argv[1]).save(sys.argv[2])'
95
- let child
96
- try {
97
- child = spawn(pythonPath || 'python', ['-c', code, url, png], { cwd: stateDir, windowsHide: true })
98
- } catch { return resolve(null) }
99
- const done = () => { try { resolve(existsSync(png) ? png : null) } catch { resolve(null) } }
100
- child.on('exit', (c) => { if (c === 0) done(); else resolve(null) })
101
- child.on('error', () => resolve(null))
102
- })
103
- }
1
+ /**
2
+ * lib/login.mjs —— 扫码登录(直接在 JS 里走 B站 passport 接口)
3
+ * ============================================================================
4
+ * 为什么不用它自带的 `bili_login`:那个把码存成 PNG 就返回,**字段全是空**(踩过一次,见 lesson)。
5
+ * 我们按实测跑通的那条来:
6
+ * ① GET /x/passport-login/web/qrcode/generate?source=main-fe-header → data.url + data.qrcode_key
7
+ * ② 手机扫 → 轮询 /x/passport-login/web/qrcode/poll?qrcode_key=…
8
+ * data.code:86101 未扫 / 86090 已扫待确认 / 0 成功 / 86038 过期
9
+ * ③ ⭐ **凭证在轮询响应的 `Set-Cookie` 头里**(成功时那条 `data.url` 里没有 SESSDATA,
10
+ * 那是别的域的游戏票 —— 别在 url 里找)
11
+ * ④ 写成 MCP 认识的四个字段:sessdata / bili_jct / buvid3 / dedeuserid
12
+ *
13
+ * 二维码图片:交给 venv 里的 Python `qrcode` 画(ASCII 的 URL,走 `-c` 安全;
14
+ * 没装就退回"只给 URL",前端会提示先跑「建 venv 装依赖」)。
15
+ * ============================================================================
16
+ */
17
+ import { spawn } from 'node:child_process'
18
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
19
+ import { join } from 'node:path'
20
+
21
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36'
22
+ const GEN = 'https://passport.bilibili.com/x/passport-login/web/qrcode/generate?source=main-fe-header'
23
+ const pollUrl = (key) => 'https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=' + encodeURIComponent(key) + '&source=main-fe-header'
24
+ const SPI = 'https://api.bilibili.com/x/frontend/finger/spi'
25
+
26
+ /** 从 Set-Cookie 数组里抠出我们要的几个 */
27
+ function pickCookies(list) {
28
+ const jar = {}
29
+ const want = { SESSDATA: 'sessdata', bili_jct: 'bili_jct', DedeUserID: 'dedeuserid', buvid3: 'buvid3' }
30
+ for (const raw of list ?? []) {
31
+ const first = String(raw).split(';')[0] || ''
32
+ const i = first.indexOf('=')
33
+ if (i <= 0) continue
34
+ const name = first.slice(0, i).trim()
35
+ const value = first.slice(i + 1).trim()
36
+ if (want[name]) jar[want[name]] = value
37
+ }
38
+ return jar
39
+ }
40
+
41
+ /** 拿不到 buvid3 时补一张"指纹"(非致命,失败就当空) */
42
+ async function ensureBuvid3(jar) {
43
+ if (jar.buvid3) return jar
44
+ try {
45
+ const r = await fetch(SPI, { headers: { 'User-Agent': UA } })
46
+ const j = await r.json()
47
+ const v = j?.data?.b_3
48
+ if (v) jar.buvid3 = v
49
+ } catch { /* 拿不到就算了 */ }
50
+ return jar
51
+ }
52
+
53
+ export async function loginStart() {
54
+ const r = await fetch(GEN, { headers: { 'User-Agent': UA } })
55
+ const j = await r.json()
56
+ const data = j?.data
57
+ if (!data?.qrcode_key || !data?.url) throw new Error(`生成二维码失败:${j?.message || '返回里没有 qrcode_key'}`)
58
+ return { key: data.qrcode_key, url: data.url }
59
+ }
60
+
61
+ /** 轮询一次。state: waiting / scanned / ok / expired / unknown */
62
+ export async function loginPoll(key) {
63
+ const res = await fetch(pollUrl(key), { headers: { 'User-Agent': UA }, redirect: 'manual' })
64
+ const setCookies = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : []
65
+ let body = {}
66
+ try { body = await res.json() } catch { /* poll 有时返回空体 */ }
67
+ const code = body?.data?.code
68
+ if (code === 0) {
69
+ const jar = await ensureBuvid3(pickCookies(setCookies))
70
+ if (!jar.sessdata) return { state: 'unknown', code, message: '登录成功了,但响应里没有 SESSDATA —— 把这次的结果记下来再排查' }
71
+ return { state: 'ok', code, credential: jar }
72
+ }
73
+ const map = { 86101: 'waiting', 86090: 'scanned', 86038: 'expired' }
74
+ return { state: map[code] ?? 'unknown', code, message: body?.message || '' }
75
+ }
76
+
77
+ /** 把凭证写进 MCP 读的那个文件(家目录里的 bili_credential.json) */
78
+ export function saveCredential(stateDir, credential) {
79
+ mkdirSync(stateDir, { recursive: true })
80
+ const file = join(stateDir, 'bili_credential.json')
81
+ writeFileSync(file, JSON.stringify({
82
+ sessdata: credential.sessdata ?? '',
83
+ bili_jct: credential.bili_jct ?? '',
84
+ buvid3: credential.buvid3 || '',
85
+ dedeuserid: credential.dedeuserid ?? '',
86
+ }, null, 2), 'utf8')
87
+ return file
88
+ }
89
+
90
+ /** 用 Python 的 qrcode 画一张 PNG,返回**文件路径**(拿不到就返回 null) */
91
+ export function renderQrPng(pythonPath, stateDir, url) {
92
+ return new Promise((resolve) => {
93
+ const png = join(stateDir, 'qrcode_login.png')
94
+ const code = 'import sys, qrcode; qrcode.make(sys.argv[1]).save(sys.argv[2])'
95
+ let child
96
+ try {
97
+ child = spawn(pythonPath || 'python', ['-c', code, url, png], { cwd: stateDir, windowsHide: true })
98
+ } catch { return resolve(null) }
99
+ const done = () => { try { resolve(existsSync(png) ? png : null) } catch { resolve(null) } }
100
+ child.on('exit', (c) => { if (c === 0) done(); else resolve(null) })
101
+ child.on('error', () => resolve(null))
102
+ })
103
+ }
package/lib/mcp.mjs CHANGED
@@ -1,115 +1,119 @@
1
- /**
2
- * lib/mcp.mjs —— 一个极小的 MCP stdio 客户端(只做我们需要的三件事)
3
- * ============================================================================
4
- * 为什么不用 `@deepseek-ai/dsh-mcp-client`:那是个"把 MCP 挂成 dsh 工具"的完整插件,
5
- * 而我们要的是**插件内部自己起这个 Python MCP**(用户选了 A 方案:装完就能用,
6
- * Python 路径和登录都在设置页里,不用去改 profile 的 patch)。
7
- *
8
- * 协议就三句话:`initialize` → `notifications/initialized` → `tools/call`,
9
- * stdio 上是**一行一个 JSON-RPC**。
10
- * ============================================================================
11
- */
12
- import { spawn } from 'node:child_process'
13
-
14
- export class McpClient {
15
- constructor({ command, args = [], cwd, env, log = () => {} }) {
16
- this.cfg = { command, args, cwd, env, log }
17
- this.proc = null
18
- this.buf = ''
19
- this.nextId = 1
20
- this.pending = new Map()
21
- this.stderr = ''
22
- this.starting = null
23
- }
24
-
25
- get running() { return Boolean(this.proc) }
26
-
27
- async start() {
28
- if (this.proc) return
29
- if (this.starting) return this.starting
30
- this.starting = (async () => {
31
- const { command, args, cwd, env, log } = this.cfg
32
- this.proc = spawn(command, args, {
33
- cwd,
34
- env: { ...process.env, ...(env || {}) },
35
- stdio: ['pipe', 'pipe', 'pipe'],
36
- windowsHide: true,
37
- })
38
- this.proc.stdout.setEncoding('utf8')
39
- this.proc.stdout.on('data', (chunk) => this.#onData(chunk))
40
- this.proc.stderr.setEncoding('utf8')
41
- this.proc.stderr.on('data', (c) => { this.stderr = (this.stderr + c).slice(-4000) })
42
- this.proc.on('error', (e) => log(`MCP 子进程启动失败:${e?.message ?? e}`))
43
- this.proc.on('exit', (code) => {
44
- log(`MCP 子进程退出(code=${code})`)
45
- for (const [, p] of this.pending) p.reject(new Error(`MCP 子进程退出(code=${code})${this.stderr ? ':' + this.stderr.slice(-300) : ''}`))
46
- this.pending.clear()
47
- this.proc = null
48
- })
49
- await this.#request('initialize', {
50
- protocolVersion: '2024-11-05',
51
- capabilities: {},
52
- clientInfo: { name: 'dsh-bili-duty', version: '0.1.0' },
53
- })
54
- this.#notify('notifications/initialized', {})
55
- log('MCP 已就绪')
56
- })()
57
- try { await this.starting } finally { this.starting = null }
58
- }
59
-
60
- stop() {
61
- try { this.proc?.kill() } catch { /* 已经没了 */ }
62
- this.proc = null
63
- }
64
-
65
- /** 调一个 MCP 工具:能解析成 JSON 就返回对象,否则返回字符串 */
66
- async call(name, args = {}, { timeoutMs = 180000 } = {}) {
67
- await this.start()
68
- const result = await this.#request('tools/call', { name, arguments: args }, timeoutMs)
69
- const text = (result?.content ?? []).map((c) => c?.text ?? '').join('\n').trim()
70
- if (result?.isError) throw new Error(text || `MCP 工具 ${name} 报错`)
71
- try { return text ? JSON.parse(text) : {} } catch { return text }
72
- }
73
-
74
- #onData(chunk) {
75
- this.buf += chunk
76
- let idx
77
- while ((idx = this.buf.indexOf('\n')) >= 0) {
78
- const line = this.buf.slice(0, idx).trim()
79
- this.buf = this.buf.slice(idx + 1)
80
- if (!line) continue
81
- let msg
82
- try { msg = JSON.parse(line) } catch { continue } // 非 JSON 行(有些库会往 stdout 打日志)忽略
83
- if (msg.id !== undefined && this.pending.has(msg.id)) {
84
- const p = this.pending.get(msg.id)
85
- this.pending.delete(msg.id)
86
- if (msg.error) p.reject(new Error(msg.error?.message ? String(msg.error.message) : JSON.stringify(msg.error)))
87
- else p.resolve(msg.result)
88
- }
89
- }
90
- }
91
-
92
- #send(obj) {
93
- if (!this.proc?.stdin?.writable) throw new Error('MCP 子进程不在运行')
94
- this.proc.stdin.write(JSON.stringify(obj) + '\n')
95
- }
96
-
97
- #notify(method, params) { this.#send({ jsonrpc: '2.0', method, params }) }
98
-
99
- #request(method, params, timeoutMs = 60000) {
100
- const id = this.nextId++
101
- return new Promise((resolve, reject) => {
102
- const timer = setTimeout(() => {
103
- this.pending.delete(id)
104
- reject(new Error(`MCP ${method} 超时(${timeoutMs}ms)${this.stderr ? ':' + this.stderr.slice(-200) : ''}`))
105
- }, timeoutMs)
106
- this.pending.set(id, {
107
- resolve: (v) => { clearTimeout(timer); resolve(v) },
108
- reject: (e) => { clearTimeout(timer); reject(e) },
109
- })
110
- try { this.#send({ jsonrpc: '2.0', id, method, params }) } catch (e) {
111
- clearTimeout(timer); this.pending.delete(id); reject(e)
112
- }
113
- })
114
- }
115
- }
1
+ /**
2
+ * lib/mcp.mjs —— 一个极小的 MCP stdio 客户端(只做我们需要的三件事)
3
+ * ============================================================================
4
+ * 为什么不用 `@deepseek-ai/dsh-mcp-client`:那是个"把 MCP 挂成 dsh 工具"的完整插件,
5
+ * 而我们要的是**插件内部自己起这个 Python MCP**(用户选了 A 方案:装完就能用,
6
+ * Python 路径和登录都在设置页里,不用去改 profile 的 patch)。
7
+ *
8
+ * 协议就三句话:`initialize` → `notifications/initialized` → `tools/call`,
9
+ * stdio 上是**一行一个 JSON-RPC**。
10
+ * ============================================================================
11
+ */
12
+ import { spawn } from 'node:child_process'
13
+ import { readFileSync } from 'node:fs'
14
+
15
+ // 报给对端的身份:版本号**从 package.json 读**,别手写(手写的那份永远追不上发版)
16
+ const PKG = (() => { try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) } catch { return {} } })()
17
+
18
+ export class McpClient {
19
+ constructor({ command, args = [], cwd, env, log = () => {} }) {
20
+ this.cfg = { command, args, cwd, env, log }
21
+ this.proc = null
22
+ this.buf = ''
23
+ this.nextId = 1
24
+ this.pending = new Map()
25
+ this.stderr = ''
26
+ this.starting = null
27
+ }
28
+
29
+ get running() { return Boolean(this.proc) }
30
+
31
+ async start() {
32
+ if (this.proc) return
33
+ if (this.starting) return this.starting
34
+ this.starting = (async () => {
35
+ const { command, args, cwd, env, log } = this.cfg
36
+ this.proc = spawn(command, args, {
37
+ cwd,
38
+ env: { ...process.env, ...(env || {}) },
39
+ stdio: ['pipe', 'pipe', 'pipe'],
40
+ windowsHide: true,
41
+ })
42
+ this.proc.stdout.setEncoding('utf8')
43
+ this.proc.stdout.on('data', (chunk) => this.#onData(chunk))
44
+ this.proc.stderr.setEncoding('utf8')
45
+ this.proc.stderr.on('data', (c) => { this.stderr = (this.stderr + c).slice(-4000) })
46
+ this.proc.on('error', (e) => log(`MCP 子进程启动失败:${e?.message ?? e}`))
47
+ this.proc.on('exit', (code) => {
48
+ log(`MCP 子进程退出(code=${code})`)
49
+ for (const [, p] of this.pending) p.reject(new Error(`MCP 子进程退出(code=${code})${this.stderr ? ':' + this.stderr.slice(-300) : ''}`))
50
+ this.pending.clear()
51
+ this.proc = null
52
+ })
53
+ await this.#request('initialize', {
54
+ protocolVersion: '2024-11-05',
55
+ capabilities: {},
56
+ clientInfo: { name: PKG.name || 'dsh-bili-scroll', version: PKG.version || '0.0.0' },
57
+ })
58
+ this.#notify('notifications/initialized', {})
59
+ log('MCP 已就绪')
60
+ })()
61
+ try { await this.starting } finally { this.starting = null }
62
+ }
63
+
64
+ stop() {
65
+ try { this.proc?.kill() } catch { /* 已经没了 */ }
66
+ this.proc = null
67
+ }
68
+
69
+ /** 调一个 MCP 工具:能解析成 JSON 就返回对象,否则返回字符串 */
70
+ async call(name, args = {}, { timeoutMs = 180000 } = {}) {
71
+ await this.start()
72
+ const result = await this.#request('tools/call', { name, arguments: args }, timeoutMs)
73
+ const text = (result?.content ?? []).map((c) => c?.text ?? '').join('\n').trim()
74
+ if (result?.isError) throw new Error(text || `MCP 工具 ${name} 报错`)
75
+ try { return text ? JSON.parse(text) : {} } catch { return text }
76
+ }
77
+
78
+ #onData(chunk) {
79
+ this.buf += chunk
80
+ let idx
81
+ while ((idx = this.buf.indexOf('\n')) >= 0) {
82
+ const line = this.buf.slice(0, idx).trim()
83
+ this.buf = this.buf.slice(idx + 1)
84
+ if (!line) continue
85
+ let msg
86
+ try { msg = JSON.parse(line) } catch { continue } // 非 JSON 行(有些库会往 stdout 打日志)忽略
87
+ if (msg.id !== undefined && this.pending.has(msg.id)) {
88
+ const p = this.pending.get(msg.id)
89
+ this.pending.delete(msg.id)
90
+ if (msg.error) p.reject(new Error(msg.error?.message ? String(msg.error.message) : JSON.stringify(msg.error)))
91
+ else p.resolve(msg.result)
92
+ }
93
+ }
94
+ }
95
+
96
+ #send(obj) {
97
+ if (!this.proc?.stdin?.writable) throw new Error('MCP 子进程不在运行')
98
+ this.proc.stdin.write(JSON.stringify(obj) + '\n')
99
+ }
100
+
101
+ #notify(method, params) { this.#send({ jsonrpc: '2.0', method, params }) }
102
+
103
+ #request(method, params, timeoutMs = 60000) {
104
+ const id = this.nextId++
105
+ return new Promise((resolve, reject) => {
106
+ const timer = setTimeout(() => {
107
+ this.pending.delete(id)
108
+ reject(new Error(`MCP ${method} 超时(${timeoutMs}ms)${this.stderr ? ':' + this.stderr.slice(-200) : ''}`))
109
+ }, timeoutMs)
110
+ this.pending.set(id, {
111
+ resolve: (v) => { clearTimeout(timer); resolve(v) },
112
+ reject: (e) => { clearTimeout(timer); reject(e) },
113
+ })
114
+ try { this.#send({ jsonrpc: '2.0', id, method, params }) } catch (e) {
115
+ clearTimeout(timer); this.pending.delete(id); reject(e)
116
+ }
117
+ })
118
+ }
119
+ }