realheart 0.2.7 → 0.2.9
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/CHANGELOG.md +46 -0
- package/index.mjs +15 -1
- package/lib/instance-tag.mjs +54 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,52 @@
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## 0.2.9(2026-10-31)
|
|
9
|
+
|
|
10
|
+
> **为什么不叫 0.2.8 第二版**:0.2.8 已经装到云端了,但还没发上 registry。
|
|
11
|
+
> 改内容不升版本号,就是「同一个版本号、两份不一样的代码」——
|
|
12
|
+
> 0.2.2 那节写过这个教训,这里不重演。
|
|
13
|
+
|
|
14
|
+
### 新加的落点没跟着收权限(0.2.8 装上去两分钟后被抓)
|
|
15
|
+
|
|
16
|
+
`last-inject.qqbot.md` 出来是 `-rw-r--r--`,而 `last-inject.md` 是 `-rw-------`。
|
|
17
|
+
doctor 的「账本权限」当场从 ok 变成 warn —— 而那两份文件里装的是她这一轮看到的全部状态。
|
|
18
|
+
|
|
19
|
+
根因不是「漏写了一项」,是**时序**:`tightenStatePerms()` 只在**启动时**跑一次,
|
|
20
|
+
而新文件是**启动之后第一轮**才出现的,那一趟根本没收不到它头上。
|
|
21
|
+
0.2.7 的 `last-inject.md` 也是这么来的 —— 第一次生成时 644,撑到下次重启才被收,只是没人那时候去看。
|
|
22
|
+
|
|
23
|
+
改法:写的时候就带 `mode: 0o600`,写完再补一次 `chmod`。
|
|
24
|
+
只给 `mode` 不够 —— 它只管新建,已存在的文件它不管;只补 `chmod` 也不够 —— 中间有一个窗口。
|
|
25
|
+
|
|
26
|
+
这条是云端装完 0.2.8 之后、对方当场核权限核出来的。
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 0.2.8(2026-10-31)
|
|
31
|
+
|
|
32
|
+
### 多实例:注入原文按实例分开落盘
|
|
33
|
+
|
|
34
|
+
`last-inject.md` 是按「只有一个实例」写的 —— 后起的实例一覆盖,上一个实例那轮「她到底看到什么」就没了。
|
|
35
|
+
2026-10-31 云端实测撞到:主实例 `qqbot` 与临时起的 `rh2` 共用一份账本,`last-inject.md` 从 1019 字节被改成 782。
|
|
36
|
+
|
|
37
|
+
现在写两份:
|
|
38
|
+
|
|
39
|
+
| 文件 | 谁写 | 用途 |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `last-inject.md` | 谁跑谁写(永远是最新) | doctor 和观测页照旧读它 |
|
|
42
|
+
| `last-inject.<profile>.md` | 只有这个实例写 | 取证,别的实例碰不到 |
|
|
43
|
+
|
|
44
|
+
实例标识从宿主的 `--profile` 取(插件跑在宿主进程里,`process.argv` 就是宿主的);
|
|
45
|
+
取不到就叫 `default`,不猜。带路径分隔符或空格的一律不认,免得标识拼出别的路径。
|
|
46
|
+
|
|
47
|
+
数据格式没变,旧文件继续可用;新文件在重启后第一轮才出现。
|
|
48
|
+
|
|
49
|
+
这版没动的:`jobs-controller.jsonl` 也还是共用、也会混入别的实例的记录。
|
|
50
|
+
它的性质是「只追加的流水」,毁不了数据,所以先留着 —— 但它是共用的,不是每实例一份。
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
8
54
|
## 0.2.7(2026-10-31)
|
|
9
55
|
|
|
10
56
|
### 卸载脚本:把共用资料整条路径扫了一遍(不只修一处)
|
package/index.mjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { Config, DEFAULTS, DSH_HOME, QQROOT, validateConfig, loadUserConfig, describeConfig } from './lib/config.mjs';
|
|
23
|
+
import { instanceTagFrom, lastInjectPaths } from './lib/instance-tag.mjs';
|
|
23
24
|
import { maskSecret, pageHint, whereSecret } from './lib/secrets.mjs';
|
|
24
25
|
import { execFile, spawn } from 'node:child_process';
|
|
25
26
|
import { randomBytes } from 'node:crypto';
|
|
@@ -751,7 +752,20 @@ export function apply(ctx, config = {}) {
|
|
|
751
752
|
// 把她这一轮看到的原文落一份 —— debug 页和“她自己查”都靠它。
|
|
752
753
|
// 写失败不能影响注入,所以单独包一层。
|
|
753
754
|
try {
|
|
754
|
-
|
|
755
|
+
// 两份:latest 给 doctor 和页面(保持旧接口),perInstance 只归这个实例写。
|
|
756
|
+
// 只写 latest 就是 2026-10-31 撞到的那个 bug:后起的实例一覆盖,
|
|
757
|
+
// 上一个实例那轮「她到底看到什么」就没了。
|
|
758
|
+
const injectPaths = lastInjectPaths(cfg.stateDir, instanceTagFrom(process.argv));
|
|
759
|
+
// 600 要在创建那一刻就给:writeFile 默认吃 umask(一般是 644),
|
|
760
|
+
// 而收紧权限那段(tightenStatePerms)只在启动时跑一次 —— 新文件是之后才出现的,
|
|
761
|
+
// 它收不到。2026-10-31 实测:`last-inject.qqbot.md` 出来时是 -rw-r--r--,
|
|
762
|
+
// doctor 的「账本权限」当场从 ok 变成 warn。
|
|
763
|
+
// mode 只管新建;已存在的文件得补一次 chmod,两个都做才算收干净。
|
|
764
|
+
const W600 = { encoding: 'utf8', mode: 0o600 };
|
|
765
|
+
await writeFile(injectPaths.latest, payload, W600);
|
|
766
|
+
await writeFile(injectPaths.perInstance, payload, W600);
|
|
767
|
+
await chmod(injectPaths.latest, 0o600).catch(() => {});
|
|
768
|
+
await chmod(injectPaths.perInstance, 0o600).catch(() => {});
|
|
755
769
|
} catch {
|
|
756
770
|
/* 落盘失败就算了,注入优先 */
|
|
757
771
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 实例标识:多个 dsh 实例共跑时,分清「这份取证是谁写的」。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要:`last-inject.md` 原来按「只有一个实例」写 —— 后起的实例一覆盖,
|
|
5
|
+
* 上一个实例那一轮「她到底看到什么」就没了。2026-10-31 云端实测撞到:
|
|
6
|
+
* 主实例 qqbot 与临时起的 rh2 共用一份账本,`last-inject.md` 从 1019 字节被改成 782。
|
|
7
|
+
*
|
|
8
|
+
* 标识从宿主的 `--profile` 取 —— 插件跑在宿主进程里,`process.argv` 就是宿主的。
|
|
9
|
+
* 取不到就叫 `default`,不猜。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
/** 只允许这种字符 —— 标识会拼进文件名,不能让它带路径分隔符或 `..` */
|
|
15
|
+
const SAFE = /^[A-Za-z0-9._-]+$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string[]} argv 宿主进程的 argv
|
|
19
|
+
* @returns {string} profile 名;无法确定时 'default'
|
|
20
|
+
*/
|
|
21
|
+
export function instanceTagFrom(argv) {
|
|
22
|
+
const list = Array.isArray(argv) ? argv.map(String) : [];
|
|
23
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
24
|
+
const a = list[i];
|
|
25
|
+
if (a === '--profile' || a === '-p') {
|
|
26
|
+
const v = list[i + 1];
|
|
27
|
+
return v && SAFE.test(v) ? v : 'default';
|
|
28
|
+
}
|
|
29
|
+
if (a.startsWith('--profile=')) {
|
|
30
|
+
const v = a.slice('--profile='.length);
|
|
31
|
+
return v && SAFE.test(v) ? v : 'default';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return 'default';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 注入原文的两个落点:
|
|
39
|
+
* - latest:永远是最新的那一份(跟以前一样,doctor 和页面读它)
|
|
40
|
+
* - perInstance:这一份只归这个实例写,别的实例碰不到
|
|
41
|
+
*
|
|
42
|
+
* 两个都写。只写 perInstance 会让原有读 `last-inject.md` 的地方一下子读空;
|
|
43
|
+
* 只写 latest 就是现在的 bug。
|
|
44
|
+
*
|
|
45
|
+
* @param {string} stateDir 账本目录
|
|
46
|
+
* @param {string} tag instanceTagFrom() 的结果
|
|
47
|
+
*/
|
|
48
|
+
export function lastInjectPaths(stateDir, tag) {
|
|
49
|
+
const safe = typeof tag === 'string' && SAFE.test(tag) ? tag : 'default';
|
|
50
|
+
return {
|
|
51
|
+
latest: join(stateDir, 'last-inject.md'),
|
|
52
|
+
perInstance: join(stateDir, `last-inject.${safe}.md`),
|
|
53
|
+
};
|
|
54
|
+
}
|