Skip to content

Commit 9a6c4e9

Browse files
committed
Merge branch 'dev'
2 parents 8e8b7f4 + 219921f commit 9a6c4e9

33 files changed

Lines changed: 200 additions & 283 deletions

File tree

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ jobs:
2626
runs-on: ubuntu-latest
2727
steps:
2828
- name: Checkout
29-
uses: actions/checkout@v4
29+
uses: actions/checkout@v7
3030

3131
- name: Setup node
32-
uses: actions/setup-node@v4
32+
uses: actions/setup-node@v7
3333
with:
3434
node-version: 24
3535
registry-url: 'https://registry.npmjs.org'

doc/更新日志.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
V8.1.9
2+
更新时间 2026-08-20
3+
4+
* 修复 sign-proxy.win-x64.node 被杀毒软件误报为病毒导致无法启动
5+
6+
=================
17
V8.1.8
28
更新时间 2026-08-14
39

package-dist.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"name":"llonebot-dist","version":"8.1.8","type":"module","description":"","main":"llbot.js","author":"linyuchen","repository":{"type":"git","url":"https://github.com/LLOneBot/LuckyLilliaBot"}}
1+
{"name":"llonebot-dist","version":"8.1.9","type":"module","description":"","main":"llbot.js","author":"linyuchen","repository":{"type":"git","url":"https://github.com/LLOneBot/LuckyLilliaBot"}}

src/common/utils/environment.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,28 @@ export function getSpecifiedUin(argv: string[] = process.argv): string | undefin
2727
if (a.startsWith('--qq=')) return a.slice('--qq='.length)
2828
}
2929
return undefined
30+
}
31+
32+
export type Cdn = 'cf' | 'china'
33+
34+
/**
35+
* 解析接入点 CDN, 传给 SignProxy init。
36+
* 优先级: argv `--cdn china` / `--cdn=china` > 环境变量 CDN > 默认 cf。
37+
* 环境变量名 (CDN/cdn) 与值 (china/CHINA/...) 都大小写无关。
38+
* cf (默认) = api-auth.luckylillia.com; china = llbot-api-auth.wumiao.wang。
39+
* 非 cf/china 的值回退到 cf (SignProxy 侧还会再校验一次并对非法值报错)。
40+
*/
41+
export function getCdn(argv: string[] = process.argv, env: NodeJS.ProcessEnv = process.env): Cdn {
42+
let raw: string | undefined
43+
for (let i = 0; i < argv.length; i++) {
44+
const a = argv[i]
45+
if (a === '--cdn' && i + 1 < argv.length) { raw = argv[i + 1]; break }
46+
if (a.startsWith('--cdn=')) { raw = a.slice('--cdn='.length); break }
47+
}
48+
// argv 未指定时回退环境变量; 变量名大小写无关 (CDN / cdn / Cdn 均可)
49+
if (raw === undefined) {
50+
const key = Object.keys(env).find(k => k.toLowerCase() === 'cdn')
51+
if (key) raw = env[key]
52+
}
53+
return raw?.trim().toLowerCase() === 'china' ? 'china' : 'cf'
3054
}

src/common/utils/file.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import fsPromise from 'node:fs/promises'
44
import path from 'node:path'
55
import * as fileType from 'file-type'
66
import { imageSizeFromFile } from '../image-size/lib/fromFile'
7-
import { TEMP_DIR } from '../globalVars'
7+
import { selfInfo, TEMP_DIR } from '../globalVars'
88
import { randomUUID, createHash } from 'node:crypto'
99
import { fileURLToPath } from 'node:url'
1010
import { Context } from 'cordis'
@@ -195,7 +195,7 @@ export async function uri2local(ctx: Context, uri: string, needExt?: boolean, fi
195195
} else if (fileCache[0].elementType === ElementType.Ptt) {
196196
url = await ctx.ntFileApi.getPttUrl(fileCache[0].fileUuid, isGroup)
197197
} else if (fileCache[0].elementType === ElementType.File) {
198-
url = (await ctx.ntFileApi.getFileUrl(fileCache[0].fileUuid, isGroup, isGroup ? +fileCache[0].peerUid : undefined)).url
198+
url = (await ctx.ntFileApi.getFileUrl(fileCache[0].fileUuid, isGroup, isGroup ? fileCache[0].peerUid : selfInfo.uid)).url
199199
}
200200
if (url) {
201201
return await uri2local(ctx, url, needExt, fileName)

src/common/utils/sign.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class MusicSign {
3232
})
3333
const data = await resp.text()
3434
if (!resp.ok) throw new Error(data)
35-
this.ctx.logger.info('音乐消息生成成功', data)
35+
this.ctx.logger.info('音乐消息生成内容:', data)
3636
return data
3737
}
3838
}

src/main/qqProtocol/direct-lib/client.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getLogger } from '@/common/logger'
1+
import { getLogger, isDebugEnabled } from '@/common/logger'
22
import { TcpConnection } from './connection'
33
import { buildServicePacket, buildServicePacket13, parseServicePacket, EncryptType, PacketContext, SsoPacket } from './packet'
44
import { generateEcdhKeyPair, EcdhKeyPair } from './ecdh'
@@ -23,6 +23,8 @@ export interface DirectClientConfig {
2323
dataDir?: string
2424
/** 当前账号 uin, 可选. */
2525
uin?: number
26+
/** 接入点 CDN: 'cf' (默认) / 'china'. 传给 SignProxy 决定 base_url + TLS pin. */
27+
cdn?: string
2628
}
2729

2830
const DEFAULT_CONFIG: DirectClientConfig = {
@@ -63,6 +65,12 @@ export class DirectProtocolClient extends EventEmitter {
6365
private signTokenRefreshInflight: Promise<void> | null = null
6466
private signTokenLastFetchAt = 0
6567
private heartbeatAliveTimer: NodeJS.Timeout | null = null
68+
// 计时诊断: 按 seq 记响应帧「进 handlePacket」的时刻, 拆 wire vs parse.
69+
private frameArriveAt: Map<number, number> = new Map()
70+
// event loop lag 采样: setInterval 期望 50ms 一跳, 实测跳间隔 - 50 = 主线程被阻塞的量.
71+
private maxLoopLag = 0
72+
private loopLagTimer: NodeJS.Timeout | null = null
73+
private lastLoopTick = 0
6674

6775
constructor(config: Partial<DirectClientConfig> = {}) {
6876
super()
@@ -96,6 +104,7 @@ export class DirectProtocolClient extends EventEmitter {
96104
authToken: this.config.authToken,
97105
machineGuid: this.guid,
98106
uin: this.config.uin,
107+
cdn: this.config.cdn,
99108
sendPacket: async ({ cmd, body }) => {
100109
const resp = (await this.sendCommand(cmd, Buffer.from(body))).payload
101110
logger.debug(`[relay] ${cmd}: req=${body.length}B resp=${resp.length}B hex=%h`, resp)
@@ -111,6 +120,7 @@ export class DirectProtocolClient extends EventEmitter {
111120
await this.ensureSignSetup()
112121
await this.conn.connect({ useIPv6: this.config.useIPv6 })
113122
this.emit('connected')
123+
this.startLoopLagMonitor()
114124

115125
// Send initial heartbeat (required before other commands)
116126
await this.sendHeartbeat()
@@ -135,6 +145,28 @@ export class DirectProtocolClient extends EventEmitter {
135145
}
136146
}
137147

148+
// 诊断用: 每 50ms 一跳, 记录实际间隔超出 50ms 的部分 = 主线程被同步阻塞的时长.
149+
// native signRequest 若是同步阻塞调用, 会在这里体现, 且会推迟响应帧回调 (灌进 wire/netRTT).
150+
// 只在 --debug 下开: 常驻 50ms setInterval 在生产会白费 CPU.
151+
private startLoopLagMonitor(): void {
152+
if (this.loopLagTimer || !isDebugEnabled()) return
153+
const EXPECT = 50
154+
this.lastLoopTick = Date.now()
155+
this.loopLagTimer = setInterval(() => {
156+
const now = Date.now()
157+
const lag = now - this.lastLoopTick - EXPECT
158+
if (lag > this.maxLoopLag) this.maxLoopLag = lag
159+
this.lastLoopTick = now
160+
}, EXPECT)
161+
this.loopLagTimer.unref?.()
162+
}
163+
164+
private takeMaxLoopLag(): number {
165+
const v = this.maxLoopLag
166+
this.maxLoopLag = 0
167+
return v
168+
}
169+
138170

139171
async sendHeartbeat(): Promise<void> {
140172
const seq = this.nextSeq()
@@ -727,14 +759,20 @@ export class DirectProtocolClient extends EventEmitter {
727759
const ctx = this.getPacketContext()
728760
const enc = encryptType ?? (this.session ? EncryptType.EncryptD2Key : EncryptType.EncryptEmpty)
729761

762+
const t0 = Date.now()
763+
let tTokenDone = t0
764+
let tSignDone = t0
765+
730766
let signResult: SignResult | null = null
731767
if (this.config.authToken && this.SIGN_ALLOWLIST.has(cmd)) {
732768
// uin 优先 session (登录成功后), 未登录时 fallback 到构造时传的 config.uin.
733769
// 快速登录场景 session 解不开时 session 为 null, 但 config.uin 已从明文元数据 / -q 拿到,
734770
// 缺了它 sign 服务器会 400 'missing uin' 直接拒
735771
const uin = this.session?.uin ? Number(this.session.uin) : (this.config.uin || undefined)
736772
await this.ensureSignTokenFresh(uin)
773+
tTokenDone = Date.now()
737774
signResult = await requestSign(cmd, payload, seq, this.guid, AppInfo.qua, uin, this.session?.signToken12B)
775+
tSignDone = Date.now()
738776
if (signResult?.token.length === 0) {
739777
signResult.token = Buffer.from(this.session?.signToken12B ?? '')
740778
}
@@ -753,25 +791,41 @@ export class DirectProtocolClient extends EventEmitter {
753791
logger.debug(`[SSO send] ${cmd} seq=${seq} frame=${packet.length}B hex=%h`, packet)
754792
}
755793

794+
const tSendStart = Date.now()
756795
return new Promise((resolve, reject) => {
757796
const timer = setTimeout(() => {
758797
this.pendingPackets.delete(seq)
759798
reject(new Error(`Command ${cmd} timed out after ${timeout}ms`))
760799
}, timeout)
761800

762-
this.pendingPackets.set(seq, { resolve, reject, timeout: timer })
801+
const finish = (fn: (v: any) => void) => (v: any) => {
802+
if (isDebugEnabled()) {
803+
const tDone = Date.now()
804+
const arrive = this.frameArriveAt.get(seq)
805+
this.frameArriveAt.delete(seq)
806+
const wire = arrive ? arrive - tSendStart : -1 // send -> 字节到达 socket (网络+服务端+event-loop 投递)
807+
const parse = arrive ? tDone - arrive : -1 // 字节到达 -> resolve (解密/解析, 纯本地)
808+
logger.debug(
809+
`[timing] ${cmd} seq=${seq}: token=${tTokenDone - t0}ms sign=${tSignDone - tTokenDone}ms wire=${wire}ms parse=${parse}ms netRTT=${tDone - tSendStart}ms maxLoopLag=${this.takeMaxLoopLag()}ms total=${tDone - t0}ms`
810+
)
811+
}
812+
fn(v)
813+
}
814+
815+
this.pendingPackets.set(seq, { resolve: finish(resolve), reject: finish(reject), timeout: timer })
763816
this.conn.send(packet)
764817
})
765818
}
766819

767820
private handlePacket(frame: Buffer): void {
821+
const tArrive = Date.now()
768822
const d2Key = this.session?.d2Key || Buffer.alloc(16)
769823
const parsed = parseServicePacket(frame, d2Key)
770824
if (!parsed) {
771825
this.emit('error', new Error('Failed to parse incoming packet'))
772826
return
773827
}
774-
828+
if (isDebugEnabled()) this.frameArriveAt.set(parsed.seq, tArrive)
775829
const pending = this.pendingPackets.get(parsed.seq)
776830
if (pending) {
777831
clearTimeout(pending.timeout)

src/main/qqProtocol/direct-lib/sign-proxy/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export interface InitArgs {
9797
machineGuidHex: string
9898
/** 当前账号 uin, 可选; 没有就等登录完成 SignProxy 自己绑. */
9999
uin?: number
100+
/** 接入点 CDN: "cf" (默认) / "china". 决定 base_url + TLS pin. 不传按 cf. */
101+
cdn?: string
100102
}
101103

102104
/** 发包回调入参: 把 body relay 给 QQ 的 cmd. */

src/main/qqProtocol/direct-lib/sign-proxy/sign-proxy.package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lucky-lillia/sign-proxy-loader",
3-
"version": "0.1.0-20260711155903",
3+
"version": "0.1.0-20260813171147",
44
"private": true,
55
"description": "Loader for the bundled lucky-lillia-sign-proxy .node. Bump version on every .node sync so Bot copies to a fresh path (the running process locks the prior file).",
66
"main": "index.ts"
-11.9 MB
Binary file not shown.

0 commit comments

Comments
 (0)