dsh-qq-link 1.5.0 → 1.5.2

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/lib/host.js +59 -12
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,30 @@
1
+ ## 1.5.2 — 2026-11-06
2
+
3
+ **`[QQ]` 忘了写结尾,后面的整段都会发到手机上**
4
+
5
+ - 🐞 开着「只发 [QQ] 块」时,只写了开头、忘了写 `[/QQ]` 的那一段,按「`[QQ]` 之后全是要发的」处理。
6
+ 模型本来只想引用一小句,一忘闭合,后面的解释、路径、想法就整段进了 QQ;
7
+ 再叠上自动分段,一条能变成好几条。
8
+ - ✅ 改成**块没闭合就整轮不发**:边界分不清就不猜,日志里写明「只写了 [QQ] 没写 [/QQ]」。
9
+ - ✅ 空块 `[QQ][/QQ]` 同样不发(当成「这轮不推」)。
10
+ - ✅ 给模型的约定里补一句:写了 `[QQ]` 就一定要写 `[/QQ]`,没闭合的块不会被猜边界。
11
+
12
+ ## 1.5.1 — 2026-11-02
13
+
14
+ **语音转写:命令里的脚本路径失效时会自己修好**
15
+
16
+ - 🐞 现象:QQ 里来的语音条变成「语音(转写失败:一串 python 路径)」——
17
+ 报错里只有路径,看不出到底哪儿坏了。
18
+ - 根因:转写命令里存的是**绝对路径**(`<某个目录>/tools/asr_cli.py`),有两种情况会让它失效:
19
+ ① 仓库 / 目录**改名**(本机这次就是:`dsh-qq-bridge` → `dsh-qq-link`);
20
+ ② 插件**升级到新目录**(npm 每次装到不同版本的目录里),旧路径还留在配置里。
21
+ 实际报错是 `can't open file ... [Errno 2] No such file or directory`,但显示被截断成一串路径,很难认。
22
+ - ✅ 兜底:转写前先看命令里那个脚本**在不在**;不在就换成本包自带的 `tools/asr_cli.py`(存在才换),
23
+ 并且**把修好的命令写回配置**,下次不用再修。
24
+ - ✅ 报错话也改明白了:真遇到「脚本找不到」,会直接说
25
+ 「转写命令里的脚本找不到 —— 在插件页把「转写命令」重新填一下,或点一次「装语音依赖」」。
26
+ - 测试:`tests/转写路径.test.mjs`(5 条:失效能换、正常不动、没有 .py 原样、空命令不炸、自带脚本在包里)。
27
+
1
28
  ## 1.5.0 — 2026-11-01
2
29
 
3
30
  **能收「戳一戳」这类通知事件了(可选,默认关)**
package/lib/host.js CHANGED
@@ -170,6 +170,31 @@ const 版本 = (() => {
170
170
  /** 跑一条外部命令(异步、带超时、拿 stdout)。
171
171
  * ⭐ 语音转写用:插件自己**不带 ASR 模型**,只负责「把音频文件交给用户指定的命令,拿回文字」。
172
172
  * 这样本机 whisper / SenseVoice / 云端 API 都能接,插件也不会因为塞模型变成几百 MB。 */
173
+ /**
174
+ * 转写命令里的脚本路径**可能失效** —— 两种常见原因:
175
+ * ① 仓库/目录改名了(2026-11-02 本机就是这么坏的:dsh-qq-bridge → dsh-qq-link);
176
+ * ② 插件升级后装到了新目录(npm 每次装到不同版本的目录里),旧绝对路径还在配置里。
177
+ * 失效时表现是「转写失败:<python 路径>」—— 很难看出是路径问题。
178
+ * 这里做兜底:路径不在就换成本包自带的 tools/asr_cli.py(存在才换)。
179
+ */
180
+ function 修转写脚本(模板) {
181
+ const 文 = String(模板 || '')
182
+ if (!文.includes('.py')) return 文
183
+ const m = /"([^"]+\.py)"|(\S+\.py)/.exec(文)
184
+ if (!m) return 文
185
+ const 旧 = m[1] || m[2]
186
+ try {
187
+ if (existsSync(旧)) return 文
188
+ } catch (error) {}
189
+ const 自备 = join(PACKAGE_ROOT, 'tools', 'asr_cli.py')
190
+ try {
191
+ if (!existsSync(自备)) return 文
192
+ } catch (error) {
193
+ return 文
194
+ }
195
+ return 文.split(旧).join(自备)
196
+ }
197
+
173
198
  function 跑命令(模板, 文件, 超时秒) {
174
199
  return new Promise((resolve) => {
175
200
  const 命令 = String(模板 || '').replace(/\{file\}/g, 文件)
@@ -291,9 +316,28 @@ async function 转写语音(段, 拉任意, 配置, 选项 = {}) {
291
316
  const 本地 = await 语音落文件(段, 拉任意, 家目录, 名)
292
317
  if (!本地) return '[语音(没下到音频)]'
293
318
  const 就绪 = await 转成wav(本地, 选项.ffmpeg || '', 家目录, 名)
294
- const 结果 = await 跑命令(模板, 就绪, 配置.转写超时秒)
319
+ // ⭐ 1.5.2:命令里的脚本路径失效就自动换成本包自带的那份(并写回配置,下次不用再修)
320
+ let 实际模板 = 模板
321
+ try {
322
+ const 修好 = 修转写脚本(模板)
323
+ if (修好 !== 模板) {
324
+ 实际模板 = 修好
325
+ try {
326
+ const 现 = 读配置()
327
+ if (String(现.转写命令 || '') === 模板) {
328
+ 现.转写命令 = 修好
329
+ 写配置(现)
330
+ }
331
+ } catch (error) {}
332
+ }
333
+ } catch (error) {}
334
+ const 结果 = await 跑命令(实际模板, 就绪, 配置.转写超时秒)
295
335
  if (结果.ok && 结果.文字) return '(语音)' + 结果.文字
296
- return '[语音(转写失败:' + String(结果.错 || '没输出').slice(0, 60) + ')]'
336
+ const 错文 = String(结果.错 || '没输出')
337
+ if (/can't open file|No such file/i.test(错文)) {
338
+ return '[语音(转写失败:转写命令里的脚本找不到 —— 在插件页把「转写命令」重新填一下,或点一次「装语音依赖」)]'
339
+ }
340
+ return '[语音(转写失败:' + 错文.slice(0, 60) + ')]'
297
341
  }
298
342
 
299
343
  /** 找 ffmpeg:环境变量 → PATH → 装依赖时随手装的那份 imageio-ffmpeg。
@@ -993,10 +1037,11 @@ export function 抠正文文件(文字) {
993
1037
  /**
994
1038
  * ⭐ 1.0.4:把 `[QQ]…[/QQ]` 处理成「真正要发出去的文字」。
995
1039
  *
996
- * 三种情况:
997
1040
  * 1. `只发标记内` 关:整段都发,但**标记本身要剥掉**(以前会原样发到手机上,她截图看到过 `[QQ]`);
998
1041
  * 2. 开 + 有完整 `[QQ]…[/QQ]`:只发块里那段;
999
- * 3. 开 + 只写了开头没写 `[/QQ]`:按「[QQ] 之后全是想发的」发(以前整条都不发,手机上什么都收不到)。
1042
+ * 3. ⭐ 1.5.2:开 + **只写了开头没写 `[/QQ]`** → 一律不发(块没闭合,边界分不清就不猜)。
1043
+ * 以前这里按「[QQ] 之后全是要发的」处理,于是模型想引用一小段、忘了闭合,
1044
+ * 后面的解释、路径、思考就整段进了 QQ —— 再叠上自动分段,一条变好几条。
1000
1045
  * 开 + 连 `[QQ]` 都没有 → `发:false`(这一轮不往 QQ 发)。
1001
1046
  * @param 文字 这一轮的正文
1002
1047
  * @param 只发标记内 配置开关
@@ -1010,11 +1055,12 @@ export function 抠QQ标记(文字, 只发标记内) {
1010
1055
  .trim()
1011
1056
  if (!只发标记内) return { 发: true, 文字: 洗(全) }
1012
1057
  const 块 = /\[QQ\]([\s\S]*?)\[\/QQ\]/.exec(全)
1013
- if (块 && String(块[1] || '').trim()) return { 发: true, 文字: 洗(块[1]) }
1014
- const 开 = 全.indexOf('[QQ]')
1015
- const 尾 = 开 >= 0 ? 全.slice(开 + 4) : ''
1016
- if (String(尾).trim()) {
1017
- return { 发: true, 文字: 洗(尾), 原因: '只写了 [QQ] 没写 [/QQ],按「[QQ] 之后全是要发的」处理' }
1058
+ if (块) {
1059
+ if (!String(块[1] || '').trim()) return { 发: false, 文字: '', 原因: '写了 [QQ][/QQ] 但中间是空的 —— 这轮不往 QQ 发' }
1060
+ return { 发: true, 文字: 洗(块[1]) }
1061
+ }
1062
+ if (全.indexOf('[QQ]') >= 0) {
1063
+ return { 发: false, 文字: '', 原因: '只写了 [QQ] 没写 [/QQ](块没闭合)—— 这轮不往 QQ 发' }
1018
1064
  }
1019
1065
  return { 发: false, 文字: '', 原因: '没写块' }
1020
1066
  }
@@ -1417,10 +1463,10 @@ export function apply(ctx) {
1417
1463
  } catch (error) {}
1418
1464
  if (!配置.总开关) return 记账('总开关关着,这轮该转但没转')
1419
1465
  // ⭐ 0.7.0 出站闸一:只发 [QQ]…[/QQ] 里的那一段(块外的解释/路径/思考留在本地)
1420
- // ⭐ 1.0.4:标记本身永远不往 QQ 里发;只写了开头没写结尾也照发(见 抠QQ标记)
1466
+ // ⭐ 1.5.2:标记本身永远不往 QQ 里发;**块没闭合就整轮不发**(分不清边界,宁可不发)
1421
1467
  const 标记 = 抠QQ标记(文字, 配置.只发标记内)
1422
1468
  if (!标记.发) {
1423
- 记账('这一轮没写 [QQ]…[/QQ] 块(「只发标记内」开着)—— 这轮不往 QQ 发')
1469
+ 记账(标记.原因 || '这一轮没写 [QQ]…[/QQ] 块(「只发标记内」开着)—— 这轮不往 QQ 发')
1424
1470
  return
1425
1471
  }
1426
1472
  if (标记.原因) 记账(标记.原因)
@@ -2397,7 +2443,8 @@ export function apply(ctx) {
2397
2443
  const 块提示 = 配置.只发标记内
2398
2444
  ? String.fromCharCode(10) + String.fromCharCode(10)
2399
2445
  + '(这台机器只把正文里 `[QQ]…[/QQ]` 中间那段发到 QQ —— 想让手机收到的就写在里面;'
2400
- + '块外的话只在本机看,不会发出去。)'
2446
+ + '块外的话只在本机看,不会发出去。⚠ 写了 `[QQ]` 就一定要写 `[/QQ]`:'
2447
+ + '没闭合的块不会被猜边界,这一轮就整条都不发。)'
2401
2448
  : ''
2402
2449
  // ⚠ 1.1.1(她定的):人设**只走系统提示词**,不塞进消息里 —— 塞进来会留在对话历史里堆着,
2403
2450
  // 而且只对 QQ 这半边生效,自己打字时又是另一个样子。挂不上就不带(备人设 里会记一句)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-qq-link",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "通到 QQ:DSH 里的 AI 伙伴和 QQ 双向互通 —— 它说完话自动发到 QQ,你在 QQ 里说的话自动转进 DSH。走 OneBot 11 HTTP(SnowLuma / NapCat 都行)。",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "engines": {