dsh-qiyu-stack 0.15.6 → 0.15.7
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/AGENTS.md +33 -3
- package/CHANGELOG.md +39 -0
- package/README.md +15 -4
- package/bin/qiyu.mjs +11 -1
- package/docs/status.md +32 -1
- package/package.json +3 -2
- package/runtime/panel/cordis.patch.yml +7 -0
- package/runtime/panel/lib/client.js +272 -0
- package/runtime/panel/lib/host.js +81 -0
- package/runtime/panel/package.json +33 -0
- package/src/changelog.mjs +33 -0
- package/src/doctor.mjs +30 -4
- package/src/handover.mjs +207 -0
- package/src/install.mjs +39 -4
package/AGENTS.md
CHANGED
|
@@ -20,6 +20,19 @@ npx dsh-qiyu-stack doctor --json # 5. 验收
|
|
|
20
20
|
|
|
21
21
|
**不要 `git clone` 仓库**——`npx` 就够了,clone 反而会让人以为要改源码。
|
|
22
22
|
|
|
23
|
+
## 第六步:给她一个会话(命令装不出来,必须人来做)
|
|
24
|
+
|
|
25
|
+
五条命令只装东西,**不会创建 DSH 工作区,也不会创建会话**。装完还要人工做三件:
|
|
26
|
+
|
|
27
|
+
1. 在 DSH 里**新建一个会话**,工作目录选 **`install` 打印的那个绝对路径**
|
|
28
|
+
(就是部署文件夹里的 `characters/<角色名>`)
|
|
29
|
+
——她的 `AGENTS.md` 在这一层,DSH 才会每一轮读到;**别选 DSH 默认工作区,也别选 `~/.dsh/qiyu`**
|
|
30
|
+
(那是运行时副本,不是她的角色包)
|
|
31
|
+
2. 设置 → 通到 QQ → **「目标会话」选这个会话**(不选的话,QQ 消息进来会被直接丢掉)
|
|
32
|
+
3. **打开「总开关」**(`install` 默认关着——关着就只收不发)
|
|
33
|
+
|
|
34
|
+
判断标准:`doctor --json` 里「桥还没选目标会话」是 **fail**;「总开关关着」「试运行开着」是 warn。
|
|
35
|
+
|
|
23
36
|
## 必须先问用户,不许猜
|
|
24
37
|
|
|
25
38
|
| 要问什么 | 为什么 |
|
|
@@ -59,14 +72,15 @@ v0.15.4 起 `qiyu` 会自动兜底(把输出重定向到临时文件再读)
|
|
|
59
72
|
|
|
60
73
|
**永远不要**在拿到「沙箱限制」标记时,让用户重装 Docker / 改配置。
|
|
61
74
|
|
|
62
|
-
##
|
|
75
|
+
## 必须交还给人做的五件事
|
|
63
76
|
|
|
64
|
-
|
|
77
|
+
这五件 agent **做不了,也不该做**,直接告诉用户:
|
|
65
78
|
|
|
66
79
|
1. **装 Docker Desktop** —— 要管理员权限 + 重启电脑。给他链接和步骤,等他做完再回来
|
|
67
80
|
2. **重启 DSH** —— 装完之后必须重启。**重启会结束当前会话**,所以先把状态写下来(见下)
|
|
68
81
|
3. **手机扫码** —— `deploy` 会自动打开二维码图片,但"用哪个号扫"只有人知道
|
|
69
82
|
4. **在手机确认页勾「下次登录无需确认」** —— 不勾的话每次重启容器都要重新验证
|
|
83
|
+
5. **建她的会话并选目标会话** —— 见上面「第六步」。DSH 的会话只能在界面里建,CLI 没有这个权限
|
|
70
84
|
|
|
71
85
|
## 千万别碰的东西
|
|
72
86
|
|
|
@@ -89,11 +103,27 @@ npx dsh-qiyu-stack doctor --json
|
|
|
89
103
|
```
|
|
90
104
|
|
|
91
105
|
- `不通`(fail)必须为 **0**
|
|
92
|
-
-
|
|
106
|
+
- **「目标会话」必须是 `ok`** —— 是 fail 就说明桥还不知道该把消息送进哪个会话
|
|
107
|
+
- `待看`(warn)可以有——但「总开关关着」「试运行开着」这两条要处理,不是可以忽略的提示
|
|
93
108
|
- 模板体检必须是 `ok`(说明没有别人的私人值混进来)
|
|
94
109
|
|
|
95
110
|
过了这关,才算装完。
|
|
96
111
|
|
|
112
|
+
## 部署完成后:写交接文档
|
|
113
|
+
|
|
114
|
+
`doctor --json` 过了(目标会话 ok、总开关已开)之后,跑:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npx dsh-qiyu-stack handover
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
它会在部署文件夹生成 `交接.md`:部署位置、QQ、容器、桥的目标会话、升级命令、
|
|
121
|
+
回退方法,以及一段「直接丢给新会话」的开场白。**你还要做**:
|
|
122
|
+
|
|
123
|
+
1. 把 doctor 里发现的问题、用户的特殊设置补进去(文档末尾有「用户自己补」)
|
|
124
|
+
2. 提醒用户:这份含 QQ 号,别公开、别提交到公开仓库
|
|
125
|
+
3. 让用户把这份归档——以后 GitHub 升级、换会话、回退,新会话都从它开始
|
|
126
|
+
|
|
97
127
|
## 出问题去哪查
|
|
98
128
|
|
|
99
129
|
- 部署细节和踩过的坑:`docs/deployment.md`
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 更新记录
|
|
2
|
+
|
|
3
|
+
> 每次发版都往这里加一段——`qiyu install` 会把当前版本的这段写进
|
|
4
|
+
> `~/.dsh/qiyu/version.json`,设置里「栖语 → 版本」页签会显示它。
|
|
5
|
+
|
|
6
|
+
## 0.15.7(2026-10-23)
|
|
7
|
+
|
|
8
|
+
- 设置面板新增「版本」页签:栈版本、安装时间、各运行时版本,以及**这次更新做了什么**
|
|
9
|
+
- 新增 `qiyu handover`:在部署文件夹生成 `交接.md`(位置 / QQ / 桥的目标会话 / 升级命令 / 回退方法 / 给新会话的开场白)
|
|
10
|
+
- `doctor` 新增验收:桥没选「目标会话」→ **fail**;目标会话没开着 / 总开关关着 / 试运行开着 → warn
|
|
11
|
+
- `install` 的交接语补上「第六步」:新建会话(工作目录选绝对路径)、选目标会话、打开总开关
|
|
12
|
+
- `install` 不再说「设置里已经填好了,不用改」——地址和令牌填好,但目标会话/总开关要人来选
|
|
13
|
+
- 文档:`AGENTS.md` / `README` 补第六步和交接文档说明
|
|
14
|
+
|
|
15
|
+
## 0.15.6(2026-10-23)
|
|
16
|
+
|
|
17
|
+
- `doctor` 自动探测 DSH 端口:不再被 `qiyu.config.json` 里的旧端口骗(DSH 每次启动端口可能变)
|
|
18
|
+
- 新增 `src/dsh-port.mjs`:先试配置端口,再扫本机监听端口,按特征认(`/dsh-qq-bridge/state` 或根路径 401 的提示语)
|
|
19
|
+
|
|
20
|
+
## 0.15.5(2026-10-23)
|
|
21
|
+
|
|
22
|
+
- `doctor` 也接上沙箱降级:Docker / 容器 / 端口相关的 fail 在受限沙箱里降成 warn,不再让 agent 误以为装失败
|
|
23
|
+
- `qr.mjs` 的 `docker cp` 改走 `跑()`:沙箱里不再假失败「拿不到二维码」
|
|
24
|
+
|
|
25
|
+
## 0.15.4(2026-10-23)
|
|
26
|
+
|
|
27
|
+
- 新增 `src/run.mjs`:管道被拒(EPERM)时改成「重定向到临时文件再读」,受限沙箱里也能跑外部命令
|
|
28
|
+
- `preflight` 沙箱降级:读不到命令输出的结论标成「沙箱限制,这条不可信」
|
|
29
|
+
- `AGENTS.md` 进 npm 包(从这版起 80 个文件)
|
|
30
|
+
|
|
31
|
+
## 0.15.3(2026-10-23)
|
|
32
|
+
|
|
33
|
+
- 仓库根加 `AGENTS.md`:给部署助手看的说明书(五条命令、必须先问的、沙箱、交还给人做的事)
|
|
34
|
+
- `preflight --json` / `doctor --json`:给 agent 机器判断用
|
|
35
|
+
- `install` 交接语改写:重启后回原会话,或开新会话跑 `doctor --json`
|
|
36
|
+
|
|
37
|
+
## 0.15.1(2026-10-23)
|
|
38
|
+
|
|
39
|
+
- 首个发布版:`npx dsh-qiyu-stack` 可跑,含 preflight / deploy / install / doctor / qr
|
package/README.md
CHANGED
|
@@ -76,6 +76,15 @@ npx dsh-qiyu-stack install --character 小满 --yes # 装完重启 DSH(
|
|
|
76
76
|
|
|
77
77
|
# 5. 扫码(图会自动打开),然后自查
|
|
78
78
|
npx dsh-qiyu-stack doctor
|
|
79
|
+
|
|
80
|
+
# 6. 给她一个会话 —— 这一步命令装不出来,得人在界面里做:
|
|
81
|
+
# · 新建会话,工作目录选 install 打印的那个绝对路径(部署文件夹里的 characters/小满)
|
|
82
|
+
# 她的 AGENTS.md 在这一层,DSH 才会每轮读到;别选 DSH 默认工作区,也别选 ~/.dsh/qiyu
|
|
83
|
+
# · 设置 → 通到 QQ → 「目标会话」选这个会话,再打开「总开关」
|
|
84
|
+
# · 再跑一次 npx dsh-qiyu-stack doctor:目标会话应该是 ok
|
|
85
|
+
|
|
86
|
+
# 7. 写交接文档 —— 以后升级 / 换会话 / 回退都从这份开始
|
|
87
|
+
npx dsh-qiyu-stack handover
|
|
79
88
|
```
|
|
80
89
|
|
|
81
90
|
> **用 npx 也没关系。** `install` 会先把运行时装一份到 `~/.dsh/qiyu/runtime/`,
|
|
@@ -83,7 +92,7 @@ npx dsh-qiyu-stack doctor
|
|
|
83
92
|
> npx 的临时缓存被清掉、或者你把 qiyu 删了,DSH 那边照样能跑。
|
|
84
93
|
|
|
85
94
|
`doctor` 会逐项告诉你:Docker 在不在、容器起没起、三个端口通不通、OneBot 认不认得出人、
|
|
86
|
-
事件流连不连得上、DSH
|
|
95
|
+
事件流连不连得上、DSH 桥接没接上、**桥有没有选「目标会话」、总开关开没开**。每条失败都带一句怎么修。
|
|
87
96
|
|
|
88
97
|
---
|
|
89
98
|
|
|
@@ -100,11 +109,12 @@ dsh-qiyu-stack/ 引擎(角色无关,可升级)
|
|
|
100
109
|
│ └── qr.mjs 取二维码并直接打开
|
|
101
110
|
├── tools/
|
|
102
111
|
│ └── merge-settings-tabs.mjs 把插件设置合并成「一个页面 + 页签」
|
|
103
|
-
├── runtime/
|
|
112
|
+
├── runtime/ 运行时五块 + 设置面板(从真跑着的机器上搬来、洗过私人值)
|
|
104
113
|
│ ├── bridge/ QQ ↔ DSH 双向(全配置驱动)
|
|
105
114
|
│ ├── memory/ 长期记忆(含 1MB 中文词典)
|
|
106
115
|
│ ├── mood/ 心情
|
|
107
116
|
│ ├── rhythm/ 作息 / 主动搭话
|
|
117
|
+
│ ├── panel/ 设置总面板(「栖语」页 + 页签,含「版本」页签)
|
|
108
118
|
│ └── voice/ 语音转写脚本(TTS 还在 bridge 里,待拆)
|
|
109
119
|
├── templates/
|
|
110
120
|
│ └── blank/ 默认模板:行为齐全、性格空白、character.json 全空
|
|
@@ -149,12 +159,13 @@ npx dsh-qiyu-stack doctor # 自检(加 --json 给 agent 用)
|
|
|
149
159
|
每个问题都有默认值;带名字调用则全用默认值(给脚本用)
|
|
150
160
|
- [x] **`qiyu deploy`**:查 Docker(没开就**静默拉起、不弹窗**)→ 选目录(挑非系统盘、查中文路径和空间)
|
|
151
161
|
→ 写 OneBot/WebUI 配置 → 跑容器 → **直接打开二维码**;令牌写回 `qiyu.config.json`
|
|
152
|
-
- [x] **`qiyu install`**:把 runtime
|
|
162
|
+
- [x] **`qiyu install`**:把 runtime 四块 + 设置面板装进 DSH profile;**按内容判断要不要替换**,
|
|
153
163
|
换代码时保留用户自己的文件(原图之类),`package.json` 改动前必留 `.bak`
|
|
154
164
|
- [x] **`qiyu preflight`**:Node / Docker / Windows / WSL / 路径 / 空间 / 网络 / 杀软 /
|
|
155
165
|
DSH / 模型凭证 / Python;`--fix` 能修引擎启动、占端口容器、edge-tts
|
|
156
166
|
- [x] **`qiyu doctor`**:全链路自检 + 模板体检 + **runtime 与已装副本的代码一致性**
|
|
157
|
-
- [x] **runtime
|
|
167
|
+
- [x] **runtime 四块 + 设置面板**:bridge / memory / mood / rhythm / panel(去个性化,配置统一读活动角色)
|
|
168
|
+
- [x] **设置里「栖语」页签里的「版本」**:显示栈版本 / 安装时间 / 各运行时版本(来自 `~/.dsh/qiyu/version.json`)
|
|
158
169
|
- [x] **语音拆干净**:`bridge/lib/voice.mjs` 管说和听,桥只管向 NapCat 要 wav
|
|
159
170
|
- [x] 端到端:空白 profile → install → 独立实例 → 备号收发都通
|
|
160
171
|
|
package/bin/qiyu.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { qr } from '../src/qr.mjs'
|
|
|
14
14
|
import { deploy } from '../src/deploy.mjs'
|
|
15
15
|
import { install } from '../src/install.mjs'
|
|
16
16
|
import { preflight } from '../src/preflight.mjs'
|
|
17
|
+
import { handover } from '../src/handover.mjs'
|
|
17
18
|
import { 读配置, 配置文件路径 } from '../src/config.mjs'
|
|
18
19
|
|
|
19
20
|
const 参数 = process.argv.slice(2)
|
|
@@ -63,7 +64,7 @@ const 帮助 = `
|
|
|
63
64
|
qiyu qr 把登录二维码取出来并**直接打开**给你扫
|
|
64
65
|
(不会只给一个路径让你自己找文件)
|
|
65
66
|
|
|
66
|
-
qiyu install
|
|
67
|
+
qiyu install 把运行时(桥/记忆/心情/作息 + 设置面板)装进 DSH profile
|
|
67
68
|
--profile <目录> 默认 ~/.dsh/profiles/desktop
|
|
68
69
|
--character <名字> 顺便把这个角色设为「活动角色」(拷到 ~/.dsh/qiyu/)
|
|
69
70
|
--only bridge,memory 只装其中几块
|
|
@@ -73,6 +74,11 @@ const 帮助 = `
|
|
|
73
74
|
|
|
74
75
|
qiyu config 显示配置来源(文件 / 环境变量 / 默认值)
|
|
75
76
|
|
|
77
|
+
qiyu handover 在部署文件夹写「交接.md」:位置 / QQ / 桥的目标会话 /
|
|
78
|
+
升级命令 / 回退方法 / 给新会话的开场白
|
|
79
|
+
--character <名字> 不指定时自动认(配置里记的,或 characters/ 下唯一的那个)
|
|
80
|
+
--out <文件名> 默认 交接.md
|
|
81
|
+
|
|
76
82
|
配置文件:qiyu.config.json(当前目录),也可以用环境变量覆盖:
|
|
77
83
|
QIYU_ONEBOT_TOKEN OneBot HTTP 令牌
|
|
78
84
|
QIYU_WS_TOKEN OneBot WS 令牌
|
|
@@ -153,6 +159,10 @@ async function 主() {
|
|
|
153
159
|
return 0
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
if (命令 === 'handover' || 命令 === '交接') {
|
|
163
|
+
return await handover({ 角色: 取值('--character'), 输出: 取值('--out', '交接.md') })
|
|
164
|
+
}
|
|
165
|
+
|
|
156
166
|
console.error('不认识的命令:' + 命令)
|
|
157
167
|
console.log(帮助)
|
|
158
168
|
return 1
|
package/docs/status.md
CHANGED
|
@@ -6,10 +6,41 @@
|
|
|
6
6
|
|
|
7
7
|
**能用了,也发出去了,但还没被陌生人在干净机器上验证过。**
|
|
8
8
|
|
|
9
|
-
- npm:`dsh-qiyu-stack@0.15.
|
|
9
|
+
- npm:`dsh-qiyu-stack@0.15.7`(`latest`)
|
|
10
10
|
- GitHub:<https://github.com/aesfls/dsh-qiyu-stack>(公开,main)
|
|
11
11
|
- 千语(爸爸本人的实例):正常运行中,桥、NapCat、Docker 都通
|
|
12
12
|
|
|
13
|
+
## 发版规矩(以后照做)
|
|
14
|
+
|
|
15
|
+
每次版本更新:先在 `CHANGELOG.md` 写一段「这版做了什么」,再发 npm。
|
|
16
|
+
|
|
17
|
+
- `qiyu install` 会把当前版本这段写进 `~/.dsh/qiyu/version.json`
|
|
18
|
+
- 设置里「栖语 → 版本」页签显示版本、安装时间、这次更新内容、各运行时版本
|
|
19
|
+
- `qiyu handover` 也会把它写进 `交接.md`
|
|
20
|
+
|
|
21
|
+
## 2026-10-23 第六轮:把「第六步」补上(v0.15.7)
|
|
22
|
+
|
|
23
|
+
陌生人在默认会话里部署会发现:五条命令全绿,桥却其实是哑的。原因有四层:
|
|
24
|
+
|
|
25
|
+
1. `qiyu` 全程**不创建 DSH 工作区/会话**(CLI 在 DSH 进程外,没有会话 API 权限)
|
|
26
|
+
2. 桥的「目标会话」是空的,收到 QQ 消息只记一句「还没填目标会话,这轮不转」,直接丢掉
|
|
27
|
+
3. 她的 `AGENTS.md` 在 `characters/<名字>/` 或 `~/.dsh/qiyu/`,不在默认会话的 AGENTS.md 链上
|
|
28
|
+
4. 总开关默认关着,`install` 原来的交接语却写「设置里已经填好了,不用改」
|
|
29
|
+
|
|
30
|
+
0.15.7 补:
|
|
31
|
+
|
|
32
|
+
1. `AGENTS.md` / `README` 新增「第六步:给她一个会话」——建会话(工作目录选 `characters/<名字>`)、
|
|
33
|
+
桥设置里选目标会话、打开总开关;「交还给人做的四件事」改成五件
|
|
34
|
+
2. `install` 的交接语把这三件事写清楚,不再说「不用改」
|
|
35
|
+
3. `doctor` 新增:目标会话为空 → **fail**;目标会话没开着 → warn;总开关关着 / 试运行开着 → warn
|
|
36
|
+
4. `install` 的交接语给出**绝对路径**,并区分「部署文件夹」和「NapCat 数据目录」,
|
|
37
|
+
明确说会话工作目录要选部署文件夹里的 `characters/<名字>`(不是 `~/.dsh/qiyu`)
|
|
38
|
+
5. 新增 `qiyu handover`:在部署文件夹写 `交接.md`——位置 / QQ / 桥的目标会话 /
|
|
39
|
+
升级命令 / 回退方法 / 直接丢给新会话的开场白,方便归档和 GitHub 升级后接手
|
|
40
|
+
6. 设置面板新增「版本」页签:显示栈版本 / 安装时间 / 各运行时版本;
|
|
41
|
+
`install` 会把这份信息写进 `~/.dsh/qiyu/version.json`,面板通过
|
|
42
|
+
`/dsh-qiyu-panel/version` 读(没有记录就现场读各插件 package.json)
|
|
43
|
+
|
|
13
44
|
## 2026-10-23 第五轮:DSH 端口自动探测(v0.15.6)
|
|
14
45
|
|
|
15
46
|
`qiyu.config.json` 里的 `dsh.port` 是某一次写下的快照,而 DSH 的端口会变
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-qiyu-stack",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.7",
|
|
4
4
|
"description": "栖语栈:让一个智能体住进 QQ —— QQ 双向桥、长期记忆、心情、语音、作息,以及把 NapCat 部署这件事一次做对",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"tools",
|
|
26
26
|
"runtime",
|
|
27
27
|
"LICENSE",
|
|
28
|
-
"README.md"
|
|
28
|
+
"README.md",
|
|
29
|
+
"CHANGELOG.md"
|
|
29
30
|
],
|
|
30
31
|
"repository": {
|
|
31
32
|
"type": "git",
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// 栖语面板 · 网页半边
|
|
2
|
+
//
|
|
3
|
+
// 在 DSH 设置的左侧栏加一项「栖语」,里面是一个页签容器:
|
|
4
|
+
// 别家插件把设置页注册到 qiyu.tab,就变成这里的一个页签。
|
|
5
|
+
//
|
|
6
|
+
// 关键点(都是 DSH 的插槽契约里写死的):
|
|
7
|
+
// 1. renderSlot(key) 只允许渲染「自己 children 里声明过的槽位」,
|
|
8
|
+
// 所以这里用 children 声明 qiyu.tab,而不是去借用别家的槽位。
|
|
9
|
+
// 2. 页签列表通过 inject 提供的 hooks.tabs 拿到(跟内置「内置插件」页一个写法),
|
|
10
|
+
// 组件那边就拿到一个 useTabs(props)。
|
|
11
|
+
// 3. 改完这个文件不用重启 DSH,刷新页面即可 —— 客户端 bundle 的版本号按文件 mtime 算。
|
|
12
|
+
|
|
13
|
+
window.__ModuleLoader__.load({
|
|
14
|
+
id: 'dsh-qiyu-panel',
|
|
15
|
+
factory: (require) => {
|
|
16
|
+
var module = { exports: {} }
|
|
17
|
+
var exports = module.exports
|
|
18
|
+
|
|
19
|
+
const React = require('react')
|
|
20
|
+
const inject = ['slots']
|
|
21
|
+
const h = React.createElement
|
|
22
|
+
|
|
23
|
+
const 槽位 = 'qiyu.tab'
|
|
24
|
+
|
|
25
|
+
const S = {
|
|
26
|
+
wrap: { padding: '4px 2px 24px', fontSize: 13, lineHeight: 1.7 },
|
|
27
|
+
head: { fontSize: 15, fontWeight: 600, margin: '0 0 2px' },
|
|
28
|
+
intro: { opacity: 0.6, fontSize: 12, margin: '0 0 14px' },
|
|
29
|
+
tabs: {
|
|
30
|
+
display: 'flex',
|
|
31
|
+
gap: 4,
|
|
32
|
+
flexWrap: 'wrap',
|
|
33
|
+
borderBottom: '1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.10))',
|
|
34
|
+
marginBottom: 14,
|
|
35
|
+
// ⭐ 钉在顶部:某个页签里的配置很长时,往下滚页签条不会跟着跑掉
|
|
36
|
+
position: 'sticky',
|
|
37
|
+
top: 0,
|
|
38
|
+
zIndex: 5,
|
|
39
|
+
paddingTop: 6,
|
|
40
|
+
background: 'var(--dsw-alias-bg-layer-1, rgba(255,255,255,.94))',
|
|
41
|
+
backdropFilter: 'saturate(1.1) blur(6px)',
|
|
42
|
+
WebkitBackdropFilter: 'saturate(1.1) blur(6px)',
|
|
43
|
+
},
|
|
44
|
+
tab: {
|
|
45
|
+
border: 'none',
|
|
46
|
+
background: 'transparent',
|
|
47
|
+
color: 'inherit',
|
|
48
|
+
opacity: 0.6,
|
|
49
|
+
fontSize: 12.5,
|
|
50
|
+
padding: '7px 12px',
|
|
51
|
+
cursor: 'pointer',
|
|
52
|
+
borderBottom: '2px solid transparent',
|
|
53
|
+
marginBottom: -1,
|
|
54
|
+
},
|
|
55
|
+
tabOn: {
|
|
56
|
+
border: 'none',
|
|
57
|
+
background: 'transparent',
|
|
58
|
+
color: 'inherit',
|
|
59
|
+
fontSize: 12.5,
|
|
60
|
+
padding: '7px 12px',
|
|
61
|
+
cursor: 'pointer',
|
|
62
|
+
fontWeight: 600,
|
|
63
|
+
borderBottom: '2px solid var(--dsw-alias-state-business-primary, #4d6bfe)',
|
|
64
|
+
marginBottom: -1,
|
|
65
|
+
},
|
|
66
|
+
panel: { minHeight: 60 },
|
|
67
|
+
empty: { opacity: 0.6, fontSize: 12.5, padding: '18px 4px' },
|
|
68
|
+
badge: {
|
|
69
|
+
fontSize: 11,
|
|
70
|
+
opacity: 0.55,
|
|
71
|
+
marginLeft: 6,
|
|
72
|
+
},
|
|
73
|
+
row: {
|
|
74
|
+
display: 'flex',
|
|
75
|
+
gap: 12,
|
|
76
|
+
padding: '6px 0',
|
|
77
|
+
borderBottom: '1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.06))',
|
|
78
|
+
},
|
|
79
|
+
rowKey: { opacity: 0.6, minWidth: 96, flex: '0 0 auto' },
|
|
80
|
+
rowVal: { wordBreak: 'break-all' },
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function 取标签(原) {
|
|
84
|
+
try {
|
|
85
|
+
const v = typeof 原 === 'function' ? 原() : 原
|
|
86
|
+
return v == null ? '' : String(v)
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return ''
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function Panel(props) {
|
|
93
|
+
const renderSlot = props && props.renderSlot
|
|
94
|
+
const useTabs = props && props.useTabs
|
|
95
|
+
const rows = typeof useTabs === 'function' ? useTabs((v) => v) : []
|
|
96
|
+
const [active, setActive] = React.useState()
|
|
97
|
+
|
|
98
|
+
const 选中 = rows.find((r) => r.id === active) ? active : rows[0] && rows[0].id
|
|
99
|
+
|
|
100
|
+
return h(
|
|
101
|
+
'div',
|
|
102
|
+
{ style: S.wrap },
|
|
103
|
+
h('div', { style: S.head }, '栖语'),
|
|
104
|
+
h('div', { style: S.intro }, '住在这里的几件事:QQ、心情、记忆、主动搭话、刷 B 站。'),
|
|
105
|
+
|
|
106
|
+
rows.length === 0
|
|
107
|
+
? h('div', { style: S.empty }, '还没有插件注册页签到 qiyu.tab。')
|
|
108
|
+
: h(
|
|
109
|
+
React.Fragment,
|
|
110
|
+
null,
|
|
111
|
+
h(
|
|
112
|
+
'div',
|
|
113
|
+
{ style: S.tabs, role: 'tablist' },
|
|
114
|
+
rows.map((r) =>
|
|
115
|
+
h(
|
|
116
|
+
'button',
|
|
117
|
+
{
|
|
118
|
+
key: r.id,
|
|
119
|
+
type: 'button',
|
|
120
|
+
role: 'tab',
|
|
121
|
+
style: r.id === 选中 ? S.tabOn : S.tab,
|
|
122
|
+
'aria-selected': r.id === 选中,
|
|
123
|
+
onClick: () => setActive(r.id),
|
|
124
|
+
},
|
|
125
|
+
r.label || r.id,
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
h(
|
|
130
|
+
'div',
|
|
131
|
+
{ style: S.panel, role: 'tabpanel' },
|
|
132
|
+
typeof renderSlot === 'function' && 选中
|
|
133
|
+
? renderSlot(槽位, {}, { only: 选中 })
|
|
134
|
+
: h('div', { style: S.empty }, '这个页签还是空的。'),
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function VersionTab() {
|
|
141
|
+
const [数据, set数据] = React.useState(null)
|
|
142
|
+
const [错, set错] = React.useState('')
|
|
143
|
+
|
|
144
|
+
React.useEffect(() => {
|
|
145
|
+
let 活 = true
|
|
146
|
+
fetch('/dsh-qiyu-panel/version', { cache: 'no-store' })
|
|
147
|
+
.then((r) => r.json())
|
|
148
|
+
.then((j) => {
|
|
149
|
+
if (活) set数据(j)
|
|
150
|
+
})
|
|
151
|
+
.catch((e) => {
|
|
152
|
+
if (活) set错(String((e && e.message) || e))
|
|
153
|
+
})
|
|
154
|
+
return () => {
|
|
155
|
+
活 = false
|
|
156
|
+
}
|
|
157
|
+
}, [])
|
|
158
|
+
|
|
159
|
+
if (错) return h('div', { style: S.empty }, '读不到版本:' + 错)
|
|
160
|
+
if (!数据) return h('div', { style: S.empty }, '正在读版本…')
|
|
161
|
+
|
|
162
|
+
const 行 = (标签, 值) =>
|
|
163
|
+
h(
|
|
164
|
+
'div',
|
|
165
|
+
{ key: 标签, style: S.row },
|
|
166
|
+
h('span', { style: S.rowKey }, 标签),
|
|
167
|
+
h('span', { style: S.rowVal }, 值 || '—'),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const 模块 = 数据.模块 || {}
|
|
171
|
+
const 更新 = String(数据.更新内容 || '')
|
|
172
|
+
.split('\n')
|
|
173
|
+
.map((s) => s.replace(/^[-*]\s*/, '').trim())
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
|
|
176
|
+
return h(
|
|
177
|
+
'div',
|
|
178
|
+
{ style: S.wrap },
|
|
179
|
+
h('div', { style: S.head }, '当前版本'),
|
|
180
|
+
h('div', { style: S.intro }, '这一套装了什么、哪一版,以及这次更新改了什么。'),
|
|
181
|
+
行('栖语栈', 数据.栈),
|
|
182
|
+
行('安装时间', 数据.安装时间),
|
|
183
|
+
行('设置面板', 数据.面板),
|
|
184
|
+
更新.length
|
|
185
|
+
? h(
|
|
186
|
+
'div',
|
|
187
|
+
{ style: { marginTop: 14 } },
|
|
188
|
+
h('div', { style: { fontWeight: 600, marginBottom: 4 } }, '这次更新做了什么'),
|
|
189
|
+
更新.map((s, i) =>
|
|
190
|
+
h(
|
|
191
|
+
'div',
|
|
192
|
+
{ key: i, style: { display: 'flex', gap: 8, padding: '3px 0' } },
|
|
193
|
+
h('span', { style: { opacity: 0.45 } }, '•'),
|
|
194
|
+
h('span', { style: S.rowVal }, s),
|
|
195
|
+
),
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
: null,
|
|
199
|
+
Object.keys(模块).length
|
|
200
|
+
? h(
|
|
201
|
+
'div',
|
|
202
|
+
{ style: { marginTop: 14 } },
|
|
203
|
+
h('div', { style: { fontWeight: 600, marginBottom: 4 } }, '运行时'),
|
|
204
|
+
Object.keys(模块)
|
|
205
|
+
.sort()
|
|
206
|
+
.map((k) => 行(k, 模块[k])),
|
|
207
|
+
)
|
|
208
|
+
: null,
|
|
209
|
+
数据.提示 ? h('div', { style: { ...S.empty, padding: '10px 0 0' } }, 数据.提示) : null,
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function apply(ctx) {
|
|
214
|
+
const slots = ctx.get('slots')
|
|
215
|
+
if (!slots) return
|
|
216
|
+
|
|
217
|
+
// 页签列表:跟内置「内置插件」页同一套写法,靠 version 判断要不要重算
|
|
218
|
+
let 上次版本 = -1
|
|
219
|
+
let 缓存 = []
|
|
220
|
+
const 快照 = () => {
|
|
221
|
+
const 版本 = slots.getVersion(槽位)
|
|
222
|
+
if (版本 !== 上次版本) {
|
|
223
|
+
上次版本 = 版本
|
|
224
|
+
缓存 = slots.entries(槽位)
|
|
225
|
+
.map((entry) => ({
|
|
226
|
+
id: (entry.options && entry.options.id) || '',
|
|
227
|
+
order: (entry.options && entry.options.order) || 0,
|
|
228
|
+
label: 取标签(entry.options && entry.options.label),
|
|
229
|
+
}))
|
|
230
|
+
.sort((a, b) => a.order - b.order)
|
|
231
|
+
}
|
|
232
|
+
return 缓存
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const 注入 = () => ({
|
|
236
|
+
hooks: {
|
|
237
|
+
tabs: {
|
|
238
|
+
getSnapshot: 快照,
|
|
239
|
+
subscribe: (listener) => slots.subscribe(槽位, listener),
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
slots.inject('settings.section', () =>
|
|
245
|
+
slots.register(
|
|
246
|
+
{
|
|
247
|
+
name: 'settings.section',
|
|
248
|
+
id: 'qiyu',
|
|
249
|
+
order: 48,
|
|
250
|
+
label: () => '栖语',
|
|
251
|
+
inject: 注入,
|
|
252
|
+
// ⭐ 自己声明页签槽位 —— renderSlot 只认这个
|
|
253
|
+
children: { [槽位]: { kind: 'list', scope: 'root' } },
|
|
254
|
+
},
|
|
255
|
+
Panel,
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
// ⭐ 版本页签:面板自己注册进自己的 qiyu.tab,永远排在最后
|
|
260
|
+
slots.inject('qiyu.tab', () =>
|
|
261
|
+
slots.register(
|
|
262
|
+
{ name: 'qiyu.tab', id: 'version', order: 99, label: () => '版本' },
|
|
263
|
+
VersionTab,
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
exports.apply = apply
|
|
269
|
+
exports.inject = inject
|
|
270
|
+
return module.exports
|
|
271
|
+
},
|
|
272
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// 栖语面板 · 宿主半边
|
|
2
|
+
//
|
|
3
|
+
// 面板本身没有业务,只多做一件事:给「版本」页签提供一个只读接口
|
|
4
|
+
// GET /dsh-qiyu-panel/version
|
|
5
|
+
// 数据来自 qiyu install 写的 $DSH_HOME/qiyu/version.json(没有就退回现场读各插件 package.json)。
|
|
6
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
|
7
|
+
import { join, dirname } from 'node:path'
|
|
8
|
+
import { homedir } from 'node:os'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const 包根 = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
12
|
+
|
|
13
|
+
function 读JSON(路径) {
|
|
14
|
+
try {
|
|
15
|
+
if (!existsSync(路径)) return null
|
|
16
|
+
return JSON.parse(readFileSync(路径, 'utf8').replace(/^\uFEFF/, ''))
|
|
17
|
+
} catch {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 现场读已装插件的版本(version.json 缺字段时兜底) */
|
|
23
|
+
function 现场模块版本(profile的node_modules) {
|
|
24
|
+
const 出 = {}
|
|
25
|
+
for (const 名 of ['dsh-qq-bridge', 'dsh-fish-memory', 'dsh-feiyu-mood', 'dsh-speak-up']) {
|
|
26
|
+
const j = 读JSON(join(profile的node_modules, 名, 'package.json'))
|
|
27
|
+
if (j && j.version) 出[名] = j.version
|
|
28
|
+
}
|
|
29
|
+
return 出
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function 版本信息() {
|
|
33
|
+
const 家 = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
34
|
+
const 记录 = 读JSON(join(家, 'qiyu', 'version.json')) || {}
|
|
35
|
+
const 自己 = 读JSON(join(包根, 'package.json')) || {}
|
|
36
|
+
const profile = process.env.DSH_PROFILE_DIR || join(家, 'profiles', 'desktop')
|
|
37
|
+
|
|
38
|
+
let 模块 = 记录.模块 || {}
|
|
39
|
+
if (!Object.keys(模块).length) {
|
|
40
|
+
try {
|
|
41
|
+
模块 = 现场模块版本(join(profile, 'node_modules'))
|
|
42
|
+
} catch {
|
|
43
|
+
模块 = {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
栈: 记录.版本 || '',
|
|
50
|
+
安装时间: 记录.安装时间 || '',
|
|
51
|
+
更新内容: 记录.更新内容 || '',
|
|
52
|
+
面板: 自己.version || '',
|
|
53
|
+
模块,
|
|
54
|
+
提示: 记录.版本 ? '' : '还没有安装记录 —— 跑一次 npx dsh-qiyu-stack install 就会写上。',
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const inject = ['webServer']
|
|
59
|
+
|
|
60
|
+
export function apply(ctx) {
|
|
61
|
+
const 注册 = () =>
|
|
62
|
+
ctx.webServer.register({
|
|
63
|
+
kind: 'exact',
|
|
64
|
+
path: '/dsh-qiyu-panel/version',
|
|
65
|
+
handler: async (req, res) => {
|
|
66
|
+
const 体 = JSON.stringify(版本信息())
|
|
67
|
+
res.writeHead(200, {
|
|
68
|
+
'content-type': 'application/json; charset=utf-8',
|
|
69
|
+
'cache-control': 'no-store',
|
|
70
|
+
})
|
|
71
|
+
res.end(体)
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
if (typeof ctx.effect === 'function') ctx.effect(注册)
|
|
77
|
+
else 注册()
|
|
78
|
+
} catch {
|
|
79
|
+
// 拿不到 webServer 就只是没有版本接口;不影响设置页其他功能
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-qiyu-panel",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "栖语 · 设置总面板:把各插件的设置收进一个「栖语」页,按页签组织。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/host.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/host.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"dsh": {
|
|
20
|
+
"bundle": {
|
|
21
|
+
"patch": "./cordis.patch.yml"
|
|
22
|
+
},
|
|
23
|
+
"client": {
|
|
24
|
+
"platform": "web",
|
|
25
|
+
"inject": [
|
|
26
|
+
"slots"
|
|
27
|
+
],
|
|
28
|
+
"external": [
|
|
29
|
+
"react"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// 从 CHANGELOG.md 里取某个版本的「这段写了什么」。
|
|
2
|
+
//
|
|
3
|
+
// 用途:qiyu install 写进 ~/.dsh/qiyu/version.json,设置面板的「版本」页签显示;
|
|
4
|
+
// qiyu handover 也顺手把它写进交接文档。
|
|
5
|
+
import { readFileSync } from 'node:fs'
|
|
6
|
+
import { join, dirname } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const 包根 = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
10
|
+
|
|
11
|
+
export function 读更新内容(版本) {
|
|
12
|
+
try {
|
|
13
|
+
const 文 = readFileSync(join(包根, 'CHANGELOG.md'), 'utf8')
|
|
14
|
+
const 行们 = 文.split(/\r?\n/)
|
|
15
|
+
const 头 = new RegExp('^##\\s*' + String(版本).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?![\\d.])')
|
|
16
|
+
let 开始 = -1
|
|
17
|
+
for (let i = 0; i < 行们.length; i++) {
|
|
18
|
+
if (头.test(行们[i])) {
|
|
19
|
+
开始 = i + 1
|
|
20
|
+
break
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (开始 < 0) return ''
|
|
24
|
+
const 出 = []
|
|
25
|
+
for (let i = 开始; i < 行们.length; i++) {
|
|
26
|
+
if (/^##\s/.test(行们[i])) break
|
|
27
|
+
出.push(行们[i])
|
|
28
|
+
}
|
|
29
|
+
return 出.join('\n').trim()
|
|
30
|
+
} catch {
|
|
31
|
+
return ''
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/doctor.mjs
CHANGED
|
@@ -200,11 +200,36 @@ export async function doctor({ 只输出JSON = false, 跳过WebSocket = false }
|
|
|
200
200
|
} else {
|
|
201
201
|
try {
|
|
202
202
|
const r = await 请求(`http://127.0.0.1:${探测.端口}/dsh-qq-bridge/state`)
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
const j = r.json
|
|
204
|
+
if (j && j.配置) {
|
|
205
|
+
const 状 = j.NapCat状态
|
|
205
206
|
const 注 = 探测.来源 === '自动探测' ? `DSH 端口 ${探测.端口} 是自动探测到的(配置里写的是 ${配置.dsh.port || '空'})` : ''
|
|
206
|
-
if (状.用户) 记('ok', `DSH 桥认到:${状.昵称}(${状.用户})`, 注)
|
|
207
|
-
else 记('warn', 'DSH 桥连不上 NapCat', JSON.stringify(状).slice(0, 160), '桥的 NapCat 地址/令牌和 qiyu.config.json 要一致')
|
|
207
|
+
if (状 && 状.用户) 记('ok', `DSH 桥认到:${状.昵称}(${状.用户})`, 注)
|
|
208
|
+
else if (状) 记('warn', 'DSH 桥连不上 NapCat', JSON.stringify(状).slice(0, 160), '桥的 NapCat 地址/令牌和 qiyu.config.json 要一致')
|
|
209
|
+
else 记('warn', 'DSH 桥没有 NapCat 状态', '', '桥的 NapCat 地址/令牌和 qiyu.config.json 要一致')
|
|
210
|
+
|
|
211
|
+
// ⭐ 桥连上 NapCat ≠ 能把消息送进会话。
|
|
212
|
+
// 目标会话为空时,桥收到 QQ 消息只会记一句「还没填目标会话,这轮不转」,直接丢掉。
|
|
213
|
+
if (!j.配置.目标会话) {
|
|
214
|
+
记(
|
|
215
|
+
'fail',
|
|
216
|
+
'桥还没选「目标会话」',
|
|
217
|
+
'收到 QQ 消息也不知道该送进哪个会话,会直接丢掉',
|
|
218
|
+
'设置 → 通到 QQ → 目标会话:选她的会话(工作目录是 characters/<名字> 的那个)'
|
|
219
|
+
)
|
|
220
|
+
} else if (!Array.isArray(j.目标情况) || !j.目标情况.length) {
|
|
221
|
+
记(
|
|
222
|
+
'warn',
|
|
223
|
+
'「目标会话」现在没开着',
|
|
224
|
+
'可能归档了,或者 DSH 刚重启还没恢复',
|
|
225
|
+
'在 DSH 里打开那个会话;归档了就取消归档,或重新选一个'
|
|
226
|
+
)
|
|
227
|
+
} else {
|
|
228
|
+
记('ok', `目标会话:${j.目标情况[0].标题 || j.配置.目标会话}`)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (j.配置.总开关 !== true) 记('warn', '总开关关着:她收不到也发不出', '消息进不了、也出不去', '设置 → 通到 QQ → 打开「总开关」')
|
|
232
|
+
if (j.配置.试运行 === true) 记('warn', '试运行开着:不会真的发到 QQ', '', '确认没问题后,在设置里把「试运行」关掉')
|
|
208
233
|
} else {
|
|
209
234
|
记('warn', `DSH 在 ${探测.端口},但没看到 dsh-qq-bridge`, '', '确认 DSH 里装了 dsh-qq-bridge 插件')
|
|
210
235
|
}
|
|
@@ -237,6 +262,7 @@ export async function doctor({ 只输出JSON = false, 跳过WebSocket = false }
|
|
|
237
262
|
['memory', 'dsh-fish-memory'],
|
|
238
263
|
['mood', 'dsh-feiyu-mood'],
|
|
239
264
|
['rhythm', 'dsh-speak-up'],
|
|
265
|
+
['panel', 'dsh-qiyu-panel'],
|
|
240
266
|
]
|
|
241
267
|
const 问题 = []
|
|
242
268
|
for (const [目录名, 包名] of 模块们) {
|
package/src/handover.mjs
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// qiyu handover —— 在部署文件夹里生成「交接.md」。
|
|
2
|
+
//
|
|
3
|
+
// 为什么要有这个:部署完那一刻,负责装的 agent 最清楚所有路径和状态;
|
|
4
|
+
// 过几天用户想升级 / 换会话 / 回退,新会话只能从一份文档开始。
|
|
5
|
+
// 这份文档就是那次「交接」。
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, readdirSync, statSync } from 'node:fs'
|
|
7
|
+
import { join, resolve, dirname } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { 读配置, 配置文件路径 } from './config.mjs'
|
|
10
|
+
import { 探测DSH } from './dsh-port.mjs'
|
|
11
|
+
import { 读更新内容 } from './changelog.mjs'
|
|
12
|
+
|
|
13
|
+
const 包根 = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
14
|
+
|
|
15
|
+
function 版本() {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(join(包根, 'package.json'), 'utf8')).version || '?'
|
|
18
|
+
} catch {
|
|
19
|
+
return '?'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function 时间() {
|
|
24
|
+
const d = new Date()
|
|
25
|
+
const p = (n) => String(n).padStart(2, '0')
|
|
26
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 角色名:参数 > 配置 > characters/ 里唯一那个 */
|
|
30
|
+
function 找角色(配置, 指定) {
|
|
31
|
+
if (指定) return 指定
|
|
32
|
+
if (配置.角色) return 配置.角色
|
|
33
|
+
try {
|
|
34
|
+
const 根 = resolve(process.cwd(), 配置.character || 'characters')
|
|
35
|
+
if (!existsSync(根)) return ''
|
|
36
|
+
const 候选 = readdirSync(根).filter((n) => {
|
|
37
|
+
try {
|
|
38
|
+
return statSync(join(根, n)).isDirectory()
|
|
39
|
+
} catch {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
return 候选.length === 1 ? 候选[0] : ''
|
|
44
|
+
} catch {
|
|
45
|
+
return ''
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 从角色包里读 QQ(桥没跑的时候兜底) */
|
|
50
|
+
function 角色里的QQ(角色目录) {
|
|
51
|
+
try {
|
|
52
|
+
const j = JSON.parse(readFileSync(join(角色目录, 'character.json'), 'utf8').replace(/^\uFEFF/, ''))
|
|
53
|
+
return { qq: String(j.qq || j.QQ || ''), 称呼: j.称呼 || '' }
|
|
54
|
+
} catch {
|
|
55
|
+
return { qq: '', 称呼: '' }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function 桥状态(配置) {
|
|
60
|
+
try {
|
|
61
|
+
const 探测 = await 探测DSH({ 已知端口: 配置.dsh.port })
|
|
62
|
+
if (!探测 || !探测.有桥) return null
|
|
63
|
+
const r = await fetch(`http://127.0.0.1:${探测.端口}/dsh-qq-bridge/state`, { signal: AbortSignal.timeout(3000) })
|
|
64
|
+
if (!r.ok) return null
|
|
65
|
+
return { 端口: 探测.端口, 状态: await r.json() }
|
|
66
|
+
} catch {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function 开关(v) {
|
|
72
|
+
return v === true ? '开' : v === false ? '关' : '(未知)'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function handover({ 角色: 指定角色 = '', 输出 = '交接.md' } = {}) {
|
|
76
|
+
const cwd = process.cwd()
|
|
77
|
+
const 配置 = 读配置()
|
|
78
|
+
const 名字 = 找角色(配置, 指定角色)
|
|
79
|
+
if (!名字) {
|
|
80
|
+
console.error('✗ 没找到角色。指定一下:qiyu handover --character <名字>')
|
|
81
|
+
console.error(' (或者在部署文件夹里跑,characters/ 下只有一个角色时也能自动认)')
|
|
82
|
+
return 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const 角色目录 = resolve(cwd, 配置.character || 'characters', 名字)
|
|
86
|
+
const 有角色 = existsSync(角色目录)
|
|
87
|
+
const 有AGENTS = existsSync(join(角色目录, 'AGENTS.md'))
|
|
88
|
+
const 角色信息 = 有角色 ? 角色里的QQ(角色目录) : { qq: '', 称呼: '' }
|
|
89
|
+
const 桥 = await 桥状态(配置)
|
|
90
|
+
const 桥配置 = (桥 && 桥.状态 && 桥.状态.配置) || {}
|
|
91
|
+
const NapCat = (桥 && 桥.状态 && 桥.状态.NapCat状态) || null
|
|
92
|
+
const qq = (NapCat && NapCat.用户) || 角色信息.qq || '(还没填)'
|
|
93
|
+
const 昵称 = (NapCat && NapCat.昵称) || 名字
|
|
94
|
+
const 目标会话 = 桥配置.目标会话 || ''
|
|
95
|
+
const 目标标题 =
|
|
96
|
+
(桥 && Array.isArray(桥.状态.目标情况) && 桥.状态.目标情况[0] && 桥.状态.目标情况[0].标题) || ''
|
|
97
|
+
const 数据目录 = (配置.napcat && 配置.napcat.dataDir) || ''
|
|
98
|
+
const 数据目录显示 = 数据目录 ? '`' + 数据目录 + '`' : '(没记录,deploy --dir 时指定的那个)'
|
|
99
|
+
const 容器 = (配置.napcat && 配置.napcat.container) || 'napcat-qq'
|
|
100
|
+
|
|
101
|
+
const 路径 = resolve(cwd, 输出)
|
|
102
|
+
if (existsSync(路径)) {
|
|
103
|
+
const 备份 = `${路径}.bak-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`
|
|
104
|
+
try {
|
|
105
|
+
renameSync(路径, 备份)
|
|
106
|
+
} catch {
|
|
107
|
+
// 备份不了就直接覆盖
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const 内容 = `# 栖语部署交接
|
|
112
|
+
|
|
113
|
+
> 生成时间:${时间()}
|
|
114
|
+
> 这份是给「以后接手的人 / 新会话」看的:升级、迁移、回退都从这份开始。
|
|
115
|
+
> ⚠️ 里面有 QQ 号和本机路径,**别公开、别提交到公开仓库**。
|
|
116
|
+
|
|
117
|
+
## 一句话
|
|
118
|
+
|
|
119
|
+
「${名字}」跑在 \`${cwd}\`,QQ ${qq}(${昵称})${目标会话 ? `,桥盯的会话是 \`${目标会话}\`` : ',**桥还没选目标会话**'}。
|
|
120
|
+
|
|
121
|
+
## 部署位置
|
|
122
|
+
|
|
123
|
+
| 东西 | 路径 |
|
|
124
|
+
|---|---|
|
|
125
|
+
| 部署文件夹 | \`${cwd}\` |
|
|
126
|
+
| 她的角色包 | \`${角色目录}\`${有角色 ? '' : '(**不存在**,先 qiyu init)'} |
|
|
127
|
+
| 她的 AGENTS.md | \`${join(角色目录, 'AGENTS.md')}\`${有AGENTS ? '' : '(**不存在**,她会没有性格)'} |
|
|
128
|
+
| NapCat 数据 | ${数据目录显示} |
|
|
129
|
+
| qiyu 配置 | \`${配置文件路径()}\`(**含令牌,别外传**) |
|
|
130
|
+
|
|
131
|
+
## 现在怎么跑
|
|
132
|
+
|
|
133
|
+
- 容器:\`${容器}\`(Docker 里的 Linux QQ + NapCat)
|
|
134
|
+
- 端口:3000 OneBot HTTP / 3001 WS / 6099 WebUI
|
|
135
|
+
- 桥:目标会话 ${目标标题 ? `「${目标标题}」` : '(没选)'}${目标会话 ? ` \`${目标会话}\`` : ''}|总开关:${开关(桥配置.总开关)}|试运行:${开关(桥配置.试运行)}
|
|
136
|
+
- 自检:\`npx dsh-qiyu-stack doctor --json\`
|
|
137
|
+
|
|
138
|
+
## 她的会话(重建时看这里)
|
|
139
|
+
|
|
140
|
+
会话的工作目录必须是**她的角色包**:\`${角色目录}\`。
|
|
141
|
+
|
|
142
|
+
> DSH 只会自动读工作目录根上的 AGENTS.md;选错地方,她会变成没有性格的默认助手。
|
|
143
|
+
|
|
144
|
+
重建步骤:DSH 新建会话 → 工作目录选上面那个路径 → 设置 → 通到 QQ →
|
|
145
|
+
「目标会话」选这个会话 → 打开「总开关」。
|
|
146
|
+
|
|
147
|
+
## 这次版本更新了什么(${版本()})
|
|
148
|
+
|
|
149
|
+
${读更新内容(版本()) || '(这次没写更新记录)'}
|
|
150
|
+
|
|
151
|
+
## 以后升级到新版本(GitHub / npm)
|
|
152
|
+
|
|
153
|
+
在部署文件夹里:
|
|
154
|
+
|
|
155
|
+
\`\`\`bash
|
|
156
|
+
npx dsh-qiyu-stack@latest preflight --json
|
|
157
|
+
npx dsh-qiyu-stack@latest install --character ${名字} --yes
|
|
158
|
+
\`\`\`
|
|
159
|
+
|
|
160
|
+
然后**重启 DSH**(关掉窗口再开,不用重启电脑),再:
|
|
161
|
+
|
|
162
|
+
\`\`\`bash
|
|
163
|
+
npx dsh-qiyu-stack@latest doctor --json
|
|
164
|
+
\`\`\`
|
|
165
|
+
|
|
166
|
+
\`install\` 会按内容替换运行时代码,**保留她的角色包和个人文件**;
|
|
167
|
+
改 \`package.json\` 前会留 \`.bak-qiyuinstall-<时间>\` 备份。
|
|
168
|
+
|
|
169
|
+
## 回退
|
|
170
|
+
|
|
171
|
+
- 代码回退:把 profile 里 \`package.json.bak-qiyuinstall-*\` 拷回原名,
|
|
172
|
+
再把 \`node_modules/*.bak-qiyuinstall-*\` 挪回原名,重启 DSH
|
|
173
|
+
- NapCat 不用动:容器、登录态、数据目录都在 ${数据目录显示}
|
|
174
|
+
|
|
175
|
+
## 新会话开场白(直接复制给它)
|
|
176
|
+
|
|
177
|
+
> 你接手一个已经部署好的栖语实例,部署文件夹是 \`${cwd}\`。
|
|
178
|
+
> 先读 \`${join(cwd, '交接.md')}\`,再跑 \`npx dsh-qiyu-stack doctor --json\`,
|
|
179
|
+
> 把结果和你要做的事告诉我。要升级就先跑
|
|
180
|
+
> \`npx dsh-qiyu-stack@latest install --character ${名字} --yes\`,然后提醒我重启 DSH。
|
|
181
|
+
|
|
182
|
+
## 用户自己补
|
|
183
|
+
|
|
184
|
+
- [ ] 这份文档之外,还有没有别的改动(改过端口 / 换过号 / 装过别的插件)?
|
|
185
|
+
- [ ] 备份放哪:______________________
|
|
186
|
+
- [ ] 有哪些已知问题:______________________
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
由 \`dsh-qiyu-stack@${版本()}\` 生成。
|
|
191
|
+
`
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
writeFileSync(路径, 内容, 'utf8')
|
|
195
|
+
} catch (错) {
|
|
196
|
+
console.error('✗ 写不了交接文档:' + String(错.message || 错))
|
|
197
|
+
return 1
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log('✅ 交接文档写好了:' + 路径)
|
|
201
|
+
console.log('')
|
|
202
|
+
console.log(' 里面有:部署位置、QQ、容器、桥的目标会话、升级命令、回退方法、给新会话的开场白。')
|
|
203
|
+
if (!有AGENTS) console.log(' ⚠️ 注意:她的 AGENTS.md 不存在,先 qiyu init 或把人设补上。')
|
|
204
|
+
if (!目标会话) console.log(' ⚠️ 桥还没选「目标会话」——去 设置 → 通到 QQ 选一个,再跑一次 qiyu handover。')
|
|
205
|
+
console.log(' 提醒用户:这份含 QQ 号,别公开。')
|
|
206
|
+
return 0
|
|
207
|
+
}
|
package/src/install.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { execFileSync } from 'node:child_process'
|
|
12
12
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, copyFileSync, rmSync, renameSync } from 'node:fs'
|
|
13
13
|
import { 读配置 } from './config.mjs'
|
|
14
|
+
import { 读更新内容 } from './changelog.mjs'
|
|
14
15
|
import { join, resolve, dirname } from 'node:path'
|
|
15
16
|
import { homedir } from 'node:os'
|
|
16
17
|
import { fileURLToPath } from 'node:url'
|
|
@@ -25,7 +26,7 @@ const 红 = (s) => `\x1b[31m${s}\x1b[0m`
|
|
|
25
26
|
const 灰 = (s) => `\x1b[90m${s}\x1b[0m`
|
|
26
27
|
|
|
27
28
|
// runtime 下这几个目录就是要装的块(voice 目前只是脚本,不当插件装)
|
|
28
|
-
const 模块目录 = ['bridge', 'memory', 'mood', 'rhythm']
|
|
29
|
+
const 模块目录 = ['bridge', 'memory', 'mood', 'rhythm', 'panel']
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* 一个目录的"指纹":相对路径 → 内容哈希。
|
|
@@ -283,6 +284,27 @@ export async function install(选项 = {}) {
|
|
|
283
284
|
// ---------- 4.5 语音转写脚本 + 活动角色,落到 $DSH_HOME/qiyu ----------
|
|
284
285
|
const 家 = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
285
286
|
const qiyu家 = join(家, 'qiyu')
|
|
287
|
+
|
|
288
|
+
// ---------- 4.4 版本记录:设置面板的「版本」页签读它 ----------
|
|
289
|
+
try {
|
|
290
|
+
const 栈版本 = JSON.parse(readFileSync(join(包根, 'package.json'), 'utf8')).version || '?'
|
|
291
|
+
const 模块版本 = {}
|
|
292
|
+
for (const m of 模块们) 模块版本[m.名字] = m.版本
|
|
293
|
+
mkdirSync(qiyu家, { recursive: true })
|
|
294
|
+
writeFileSync(
|
|
295
|
+
join(qiyu家, 'version.json'),
|
|
296
|
+
JSON.stringify(
|
|
297
|
+
{ 版本: 栈版本, 安装时间: new Date().toISOString(), 更新内容: 读更新内容(栈版本), 模块: 模块版本 },
|
|
298
|
+
null,
|
|
299
|
+
2,
|
|
300
|
+
) + '\n',
|
|
301
|
+
'utf8',
|
|
302
|
+
)
|
|
303
|
+
console.log(绿('✓') + ` 版本记录 → ${join(qiyu家, 'version.json')}`)
|
|
304
|
+
} catch (错) {
|
|
305
|
+
console.log(黄('· 版本记录没写成(不影响使用):') + String(错.message || 错))
|
|
306
|
+
}
|
|
307
|
+
|
|
286
308
|
try {
|
|
287
309
|
const 源 = join(包根, 'runtime', 'voice', '语音转写.py')
|
|
288
310
|
if (existsSync(源)) {
|
|
@@ -404,11 +426,24 @@ export async function install(选项 = {}) {
|
|
|
404
426
|
console.log(' 2. 重启后:回到这个会话,或者开一个新会话,先跑一遍')
|
|
405
427
|
console.log(' npx dsh-qiyu-stack doctor --json')
|
|
406
428
|
console.log(' 把结果贴回来 —— 东西已经装好了,这一步只是确认,不是重新开始')
|
|
407
|
-
|
|
429
|
+
const 部署夹 = process.cwd()
|
|
430
|
+
const 角色夹 = 角色名 ? resolve(部署夹, 'characters', 角色名) : join(部署夹, 'characters', '<名字>')
|
|
431
|
+
const 本机配置 = 读配置()
|
|
432
|
+
console.log(' 3. **给她一个会话**(这一步命令装不出来,得人来做):')
|
|
433
|
+
console.log(' · 部署文件夹(qiyu.config.json 就在这):' + 部署夹)
|
|
434
|
+
console.log(' · 新建会话,工作目录选:' + 角色夹)
|
|
435
|
+
console.log(' (她的 AGENTS.md 在这一层,DSH 才会每一轮读到;不是 ~/.dsh/qiyu,也不是 DSH 默认工作区)')
|
|
436
|
+
if (本机配置.napcat && 本机配置.napcat.dataDir && resolve(本机配置.napcat.dataDir) !== 部署夹) {
|
|
437
|
+
console.log(' (NapCat 的数据在 ' + 本机配置.napcat.dataDir + ',那是另一处,会话不要选它)')
|
|
438
|
+
}
|
|
439
|
+
console.log(' · 设置 → 通到 QQ → 「目标会话」选这个会话,再打开「总开关」')
|
|
440
|
+
console.log(' · 再跑一次 doctor:目标会话应该 ok,总开关应该不再 warn')
|
|
441
|
+
console.log(' 4. 写交接文档:npx dsh-qiyu-stack handover')
|
|
442
|
+
console.log(' 它会写 交接.md(部署位置 / 升级命令 / 回退方法 / 给新会话的开场白)')
|
|
408
443
|
console.log('')
|
|
409
444
|
console.log(灰(' 注:DSH 启动时可能会自己弹一个浏览器窗口 —— 那是 DSH 自己的界面,不是报错。'))
|
|
410
|
-
console.log(灰('
|
|
411
|
-
console.log(灰('
|
|
445
|
+
console.log(灰(' 桥的地址和令牌已经填好了;但「目标会话」和「总开关」是给你的,不会替你选。'))
|
|
446
|
+
console.log(灰(' 只有换机器/换端口时才需要改地址,那时令牌在 qiyu.config.json 的 napcat.token。'))
|
|
412
447
|
console.log('')
|
|
413
448
|
console.log(灰(`要退回去:把 ${pkg备份.split(/[\\/]/).pop()} 拷回 package.json,`))
|
|
414
449
|
console.log(灰('再把 node_modules 里 *.bak-qiyuinstall-* 挪回原名。'))
|