dsh-qiyu-stack 0.15.3 → 0.15.4
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 +101 -0
- package/README.md +5 -2
- package/docs/status.md +13 -2
- package/package.json +2 -1
- package/src/deploy.mjs +1 -9
- package/src/docker.mjs +1 -7
- package/src/doctor.mjs +1 -9
- package/src/preflight.mjs +25 -9
- package/src/run.mjs +81 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# 给「部署助手」看的,不是给角色看的
|
|
2
|
+
|
|
3
|
+
> **如果你是被部署出来的那个角色**(你有人格、有作息、正在跟人聊天),
|
|
4
|
+
> **忽略本文件全部内容**,回去读你自己的 `AGENTS.md`。
|
|
5
|
+
> 只有当用户让你「把栖语装到这台机器上 / 帮我部署一个 QQ 智能体」时,这份文件才适用。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
你面对的是一个 **DSH 插件栈**:`dsh-qiyu-stack`。它让一个智能体住进 QQ(NapCat + OneBot)。
|
|
10
|
+
|
|
11
|
+
## 你要做的事,只有五条命令
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx dsh-qiyu-stack preflight --json # 1. 环境体检,先看这台机器行不行
|
|
15
|
+
npx dsh-qiyu-stack init # 2. 建角色(会问:名字、QQ、称呼、作息、性格、能否自改)
|
|
16
|
+
npx dsh-qiyu-stack deploy --qq <号> # 3. 起 NapCat(会弹二维码,用户手机扫)
|
|
17
|
+
npx dsh-qiyu-stack install --character <名字> --yes # 4. 装进 DSH
|
|
18
|
+
npx dsh-qiyu-stack doctor --json # 5. 验收
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**不要 `git clone` 仓库**——`npx` 就够了,clone 反而会让人以为要改源码。
|
|
22
|
+
|
|
23
|
+
## 必须先问用户,不许猜
|
|
24
|
+
|
|
25
|
+
| 要问什么 | 为什么 |
|
|
26
|
+
|---|---|
|
|
27
|
+
| **QQ 号**(她要登录的那个号) | 猜不了,也不该猜。用户可能有好几个号 |
|
|
28
|
+
| **角色名字** | 这是人格的根,不能替他起 |
|
|
29
|
+
| 部署到哪个盘 | 默认会挑非系统盘;C 盘空间紧的话要说明 |
|
|
30
|
+
|
|
31
|
+
## 沙箱和提权:这是**默认路径**,不是意外
|
|
32
|
+
|
|
33
|
+
DSH 默认给的是 `workspace-write`。下面这些**注定会被拒**,你要提前预判,别把它当成"npm 坏了":
|
|
34
|
+
|
|
35
|
+
| 会失败的动作 | 正确做法 |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `npx` 写 `%LOCALAPPDATA%\npm-cache` | 加 `--cache ./.npm-cache`(指到工作区),或申请提升权限 |
|
|
38
|
+
| `install` 写 `~/.dsh/profiles/...` | **申请提权**(这是正常操作,不是异常);被拒就请用户自己在终端跑这一条 |
|
|
39
|
+
| 写 `~/.dsh/qiyu/`、`~/.dsh/*.json` | 同上 |
|
|
40
|
+
|
|
41
|
+
**申请提权时说清楚要写哪里、为什么**,别默默重试。
|
|
42
|
+
|
|
43
|
+
### ⚠️ 一个会骗你的坑:沙箱里读不到子进程输出
|
|
44
|
+
|
|
45
|
+
受限沙箱不允许程序开命名管道,所以任何 `execFile/spawn` **抓输出**(`stdio: 'pipe'`)都会 `EPERM`。
|
|
46
|
+
表现是:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
preflight --json → { "ok": false, "不通": 1, "标题": "这台机器上没有 Docker" }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
而机器上 Docker 明明装着、容器还在跑。
|
|
53
|
+
|
|
54
|
+
v0.15.4 起 `qiyu` 会自动兜底(把输出重定向到临时文件再读),仍然读不到的,会**把结论降级成 `warn`
|
|
55
|
+
并在标题前面加「沙箱限制,这条不可信」**。看到这几个字,别让用户去装东西,改成:
|
|
56
|
+
|
|
57
|
+
1. 让用户**在他自己的终端**里跑一遍同样的命令;或
|
|
58
|
+
2. 把这次会话提到 `danger-full-access` 再验。
|
|
59
|
+
|
|
60
|
+
**永远不要**在拿到「沙箱限制」标记时,让用户重装 Docker / 改配置。
|
|
61
|
+
|
|
62
|
+
## 必须交还给人做的四件事
|
|
63
|
+
|
|
64
|
+
这四件 agent **做不了,也不该做**,直接告诉用户:
|
|
65
|
+
|
|
66
|
+
1. **装 Docker Desktop** —— 要管理员权限 + 重启电脑。给他链接和步骤,等他做完再回来
|
|
67
|
+
2. **重启 DSH** —— 装完之后必须重启。**重启会结束当前会话**,所以先把状态写下来(见下)
|
|
68
|
+
3. **手机扫码** —— `deploy` 会自动打开二维码图片,但"用哪个号扫"只有人知道
|
|
69
|
+
4. **在手机确认页勾「下次登录无需确认」** —— 不勾的话每次重启容器都要重新验证
|
|
70
|
+
|
|
71
|
+
## 千万别碰的东西
|
|
72
|
+
|
|
73
|
+
- `characters/<名字>/AGENTS.md`、`character.json`、`schedule.json` —— **用户的角色,只能创建,不能替换已有内容**
|
|
74
|
+
- `~/.dsh/AGENTS.md` —— 全局用户配置,改了会影响这台机器上**所有**会话(包括别人家已经养着的角色)
|
|
75
|
+
- 任何模板里的"默认值" —— 那些刻意留空是有原因的(见 `docs/conventions.md`)
|
|
76
|
+
|
|
77
|
+
## 重启前要做的事
|
|
78
|
+
|
|
79
|
+
`install` 跑完会打印交接语。你要做的是:
|
|
80
|
+
|
|
81
|
+
1. 把 `qiyu doctor --json` 的**预期结果**告诉用户("重启后应该看到 0 不通")
|
|
82
|
+
2. 提醒他:**重启后回到这个会话**(如果 DSH 能恢复),或者开新会话后先跑一次 `npx dsh-qiyu-stack doctor --json`,把结果贴回来
|
|
83
|
+
3. **不要说"重新开始"** —— 东西已经装好了,只是要验证
|
|
84
|
+
|
|
85
|
+
## 验收标准
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx dsh-qiyu-stack doctor --json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `不通`(fail)必须为 **0**
|
|
92
|
+
- `待看`(warn)可以有——通常是"Windows 上别装 Windows 版 NapCat"这类提示
|
|
93
|
+
- 模板体检必须是 `ok`(说明没有别人的私人值混进来)
|
|
94
|
+
|
|
95
|
+
过了这关,才算装完。
|
|
96
|
+
|
|
97
|
+
## 出问题去哪查
|
|
98
|
+
|
|
99
|
+
- 部署细节和踩过的坑:`docs/deployment.md`
|
|
100
|
+
- 默认值红线:`docs/conventions.md`
|
|
101
|
+
- 当前状态与已知未做:`docs/status.md`
|
package/README.md
CHANGED
|
@@ -133,13 +133,16 @@ characters/小满/ 你的角色包(不进版本库)
|
|
|
133
133
|
**已发布**:[npm](https://www.npmjs.com/package/dsh-qiyu-stack) · [GitHub](https://github.com/aesfls/dsh-qiyu-stack)
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
|
-
npx dsh-qiyu-stack preflight #
|
|
136
|
+
npx dsh-qiyu-stack preflight # 环境体检(加 --json 给 agent 用)
|
|
137
137
|
npx dsh-qiyu-stack init # 建角色(向导)
|
|
138
138
|
npx dsh-qiyu-stack deploy --qq <号> # 起 NapCat + 出码
|
|
139
139
|
npx dsh-qiyu-stack install --character <名字> --yes # 装进 DSH
|
|
140
|
-
npx dsh-qiyu-stack doctor #
|
|
140
|
+
npx dsh-qiyu-stack doctor # 自检(加 --json 给 agent 用)
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
> 让 AI 帮你装?把这个仓库地址丢给它就行 —— 根目录的 [AGENTS.md](AGENTS.md) 是给 agent 看的说明书。
|
|
144
|
+
> 它知道自己能做什么、哪些必须交还给人(装 Docker、重启 DSH、手机扫码)。
|
|
145
|
+
|
|
143
146
|
已经能用:
|
|
144
147
|
|
|
145
148
|
- [x] **`qiyu init` 向导**:名字 / QQ / 称呼 / 作息 / 性格 /「能不能自己改性格」;
|
package/docs/status.md
CHANGED
|
@@ -4,12 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
## 一句话状态
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**能用了,也发出去了,但还没被陌生人在干净机器上验证过。**
|
|
8
8
|
|
|
9
|
-
- npm:`dsh-qiyu-stack@0.15.
|
|
9
|
+
- npm:`dsh-qiyu-stack@0.15.3`(`latest`),`npx dsh-qiyu-stack preflight --json` 实测可跑
|
|
10
10
|
- GitHub:<https://github.com/aesfls/dsh-qiyu-stack>(公开,main)
|
|
11
11
|
- 千语(爸爸本人的实例):正常运行中,桥、NapCat、Docker 都通
|
|
12
12
|
|
|
13
|
+
## 2026-10-23 第二轮补的(agent 入口)
|
|
14
|
+
|
|
15
|
+
一个陌生人会话实测后提的问题,采纳了三条:
|
|
16
|
+
|
|
17
|
+
1. **仓库根 `AGENTS.md`**:给部署助手看的说明书——五条命令、必须先问的(QQ 号/角色名)、
|
|
18
|
+
沙箱和提权是**默认路径**不是意外、必须交还给人做的四件事(装 Docker / 重启 DSH / 扫码 / 勾确认框)、
|
|
19
|
+
千万别碰的东西(角色包、`~/.dsh/AGENTS.md`)、验收标准
|
|
20
|
+
2. **`preflight --json` / `doctor --json`**:让 agent 机器判断卡在哪,别去正则扒中文
|
|
21
|
+
3. **`install` 交接语**:改成「重启后回这个会话,或开新会话跑 doctor --json 贴回来」,
|
|
22
|
+
并写明**「不是重新开始」**
|
|
23
|
+
|
|
13
24
|
## 今天做了什么
|
|
14
25
|
|
|
15
26
|
1. **NapCat 迁移落地**:Windows 版 NapCat 被 QQ 的文件校验堵死,改走 Docker + Linux QQ,跑通
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-qiyu-stack",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.4",
|
|
4
4
|
"description": "栖语栈:让一个智能体住进 QQ —— QQ 双向桥、长期记忆、心情、语音、作息,以及把 NapCat 部署这件事一次做对",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"files": [
|
|
18
|
+
"AGENTS.md",
|
|
18
19
|
"bin",
|
|
19
20
|
"src",
|
|
20
21
|
"templates",
|
package/src/deploy.mjs
CHANGED
|
@@ -13,21 +13,13 @@ import { createInterface } from 'node:readline'
|
|
|
13
13
|
import { 读配置, 配置文件路径 } from './config.mjs'
|
|
14
14
|
import { 打开文件 } from './qr.mjs'
|
|
15
15
|
import { 确保引擎, 引擎版本 } from './docker.mjs'
|
|
16
|
+
import { 跑 } from './run.mjs'
|
|
16
17
|
|
|
17
18
|
const 绿 = (s) => `\x1b[32m${s}\x1b[0m`
|
|
18
19
|
const 黄 = (s) => `\x1b[33m${s}\x1b[0m`
|
|
19
20
|
const 红 = (s) => `\x1b[31m${s}\x1b[0m`
|
|
20
21
|
const 灰 = (s) => `\x1b[90m${s}\x1b[0m`
|
|
21
22
|
|
|
22
|
-
function 跑(命令, 参数, 超时 = 60000) {
|
|
23
|
-
try {
|
|
24
|
-
const 出 = execFileSync(命令, 参数, { timeout: 超时, encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
25
|
-
return { ok: true, 出: String(出).trim() }
|
|
26
|
-
} catch (错) {
|
|
27
|
-
return { ok: false, 错: String((错 && (错.stderr || 错.message)) || 错).trim() }
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
23
|
async function 端口通(端口, 超时 = 1200) {
|
|
32
24
|
const net = await import('node:net')
|
|
33
25
|
return new Promise((解决) => {
|
package/src/docker.mjs
CHANGED
|
@@ -8,18 +8,12 @@
|
|
|
8
8
|
import { execFile, spawn } from 'node:child_process'
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
10
10
|
import { join } from 'node:path'
|
|
11
|
+
import { 跑 } from './run.mjs'
|
|
11
12
|
|
|
12
13
|
const 绿 = (s) => `\x1b[32m${s}\x1b[0m`
|
|
13
14
|
const 黄 = (s) => `\x1b[33m${s}\x1b[0m`
|
|
14
15
|
const 灰 = (s) => `\x1b[90m${s}\x1b[0m`
|
|
15
16
|
|
|
16
|
-
export function 跑(命令, 参数, 超时 = 15000) {
|
|
17
|
-
return new Promise((解决) => {
|
|
18
|
-
execFile(命令, 参数, { timeout: 超时, windowsHide: true, encoding: 'utf8' }, (错, 出, 错出) => {
|
|
19
|
-
解决({ ok: !错, 出: String(出 || '').trim(), 错: String(错出 || (错 && 错.message) || '').trim() })
|
|
20
|
-
})
|
|
21
|
-
})
|
|
22
|
-
}
|
|
23
17
|
|
|
24
18
|
export async function 引擎版本() {
|
|
25
19
|
const r = await 跑('docker', ['version', '--format', '{{.Server.Version}}'], 10000)
|
package/src/doctor.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
|
9
9
|
import { join, dirname, resolve } from 'node:path'
|
|
10
10
|
import { fileURLToPath } from 'node:url'
|
|
11
11
|
import { 读配置 } from './config.mjs'
|
|
12
|
+
import { 跑 } from './run.mjs'
|
|
12
13
|
|
|
13
14
|
const 包根 = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
14
15
|
|
|
@@ -17,15 +18,6 @@ const 黄 = (s) => `\x1b[33m${s}\x1b[0m`
|
|
|
17
18
|
const 红 = (s) => `\x1b[31m${s}\x1b[0m`
|
|
18
19
|
const 灰 = (s) => `\x1b[90m${s}\x1b[0m`
|
|
19
20
|
|
|
20
|
-
function 跑(命令, 参数, 超时 = 8000) {
|
|
21
|
-
try {
|
|
22
|
-
const 出 = execFileSync(命令, 参数, { timeout: 超时, encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
23
|
-
return { ok: true, 出: String(出).trim() }
|
|
24
|
-
} catch (错) {
|
|
25
|
-
return { ok: false, 错: String((错 && (错.stderr || 错.message)) || 错).trim() }
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
21
|
function 端口通(主机, 端口, 超时 = 1500) {
|
|
30
22
|
return new Promise((解决) => {
|
|
31
23
|
const s = net.connect({ host: 主机, port: 端口 })
|
package/src/preflight.mjs
CHANGED
|
@@ -11,21 +11,13 @@ import { existsSync, statfsSync, readFileSync } from 'node:fs'
|
|
|
11
11
|
import { join } from 'node:path'
|
|
12
12
|
import { homedir, release } from 'node:os'
|
|
13
13
|
import { createInterface } from 'node:readline'
|
|
14
|
+
import { 跑, 沙箱受限过 } from './run.mjs'
|
|
14
15
|
|
|
15
16
|
const 绿 = (s) => `\x1b[32m${s}\x1b[0m`
|
|
16
17
|
const 黄 = (s) => `\x1b[33m${s}\x1b[0m`
|
|
17
18
|
const 红 = (s) => `\x1b[31m${s}\x1b[0m`
|
|
18
19
|
const 灰 = (s) => `\x1b[90m${s}\x1b[0m`
|
|
19
20
|
|
|
20
|
-
function 跑(命令, 参数, 超时 = 8000) {
|
|
21
|
-
try {
|
|
22
|
-
const 出 = execFileSync(命令, 参数, { timeout: 超时, encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
23
|
-
return { ok: true, 出: String(出).trim() }
|
|
24
|
-
} catch (错) {
|
|
25
|
-
return { ok: false, 错: String((错 && (错.stderr || 错.message)) || 错).trim() }
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
21
|
function 端口通(端口, 超时 = 1500) {
|
|
30
22
|
return new Promise((解决) => {
|
|
31
23
|
const s = net.connect({ host: '127.0.0.1', port: 端口 })
|
|
@@ -357,6 +349,30 @@ export async function preflight(选项 = {}) {
|
|
|
357
349
|
}
|
|
358
350
|
}
|
|
359
351
|
|
|
352
|
+
// ---------- 沙箱兜底:读不到命令输出时,别把「没装」当成事实 ----------
|
|
353
|
+
//
|
|
354
|
+
// 受限沙箱不允许程序开管道,所有"跑条命令看输出"的判断都可能拿到空结果。
|
|
355
|
+
// 这时把相关 fail 降级成 warn 并说清楚——否则 agent 会去让用户装一个已经装好的 Docker。
|
|
356
|
+
if (沙箱受限过()) {
|
|
357
|
+
let 降级了 = 0
|
|
358
|
+
for (const x of 结果) {
|
|
359
|
+
if (x.级别 !== 'fail') continue
|
|
360
|
+
if (!/Docker|容器|端口|Python|WSL|虚拟化/.test(x.标题)) continue
|
|
361
|
+
x.级别 = 'warn'
|
|
362
|
+
x.标题 = '沙箱限制,这条不可信:' + x.标题
|
|
363
|
+
x.怎么修 = '在用户自己的终端里跑 qiyu preflight 再确认一次;或者把这次会话提到 danger-full-access'
|
|
364
|
+
降级了++
|
|
365
|
+
}
|
|
366
|
+
结果.push({
|
|
367
|
+
级别: 'warn',
|
|
368
|
+
标题: '当前沙箱读不到子进程输出',
|
|
369
|
+
依据:
|
|
370
|
+
'受限沙箱不允许程序开管道(stdio: pipe 会 EPERM)。qiyu 已改用"重定向到文件再读"兜底,' +
|
|
371
|
+
'但个别命令仍可能失败,' + (降级了 ? `上面 ${降级了} 条结论已降级` : '') + '。',
|
|
372
|
+
怎么修: '这类结论不能当真:让用户在自己终端跑一遍,或把会话权限提到 danger-full-access',
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
360
376
|
// ---------- 输出 ----------
|
|
361
377
|
const 坏 = 结果.filter((x) => x.级别 === 'fail').length
|
|
362
378
|
const 疑 = 结果.filter((x) => x.级别 === 'warn').length
|
package/src/run.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// 跑外部命令,并且**在受限沙箱里也能活**。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要这个文件(2026-10-23 实测):
|
|
4
|
+
// DSH 默认沙箱(workspace-write / read-only)不允许程序开命名管道,
|
|
5
|
+
// 所以 `execFileSync(cmd, args, { stdio: ['ignore','pipe','pipe'] })` 会直接 EPERM。
|
|
6
|
+
// 而 qiyu 里几乎所有判断都靠"跑条命令看输出"——一旦 EPERM,
|
|
7
|
+
// 就会变成「没装 Docker」「端口查不出来」「容器不存在」这类**假阴性**,
|
|
8
|
+
// 把用户和 agent 一起带沟里。
|
|
9
|
+
//
|
|
10
|
+
// 兜底办法:抓不到管道,就把输出重定向到临时文件再读(沙箱允许写工作区)。
|
|
11
|
+
import { execFileSync } from 'node:child_process'
|
|
12
|
+
import { mkdtempSync, readFileSync, rmSync, existsSync } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import { tmpdir } from 'node:os'
|
|
15
|
+
|
|
16
|
+
let 遇到过沙箱限制 = false
|
|
17
|
+
|
|
18
|
+
/** 这次运行里,有没有因为沙箱读不到命令输出(结果可能不准) */
|
|
19
|
+
export function 沙箱受限过() {
|
|
20
|
+
return 遇到过沙箱限制
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function 引号(s) {
|
|
24
|
+
const t = String(s)
|
|
25
|
+
return /[\s"&|<>^]/.test(t) ? '"' + t.replace(/"/g, '\\"') + '"' : t
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 把命令的输出重定向到文件,再读回来(不依赖管道)。 */
|
|
29
|
+
function 落盘跑(命令, 参数, 超时) {
|
|
30
|
+
let 目录 = ''
|
|
31
|
+
try {
|
|
32
|
+
目录 = mkdtempSync(join(tmpdir(), 'qiyu-cmd-'))
|
|
33
|
+
const 输出文件 = join(目录, 'out.txt')
|
|
34
|
+
const 整行 = [命令, ...参数].map(引号).join(' ') + ' > ' + 引号(输出文件) + ' 2>&1'
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
execFileSync('cmd.exe', ['/c', 整行], { timeout: 超时, windowsHide: true, stdio: ['ignore', 'ignore', 'ignore'] })
|
|
37
|
+
} else {
|
|
38
|
+
execFileSync('sh', ['-c', 整行], { timeout: 超时, stdio: ['ignore', 'ignore', 'ignore'] })
|
|
39
|
+
}
|
|
40
|
+
const 出 = existsSync(输出文件) ? readFileSync(输出文件, 'utf8').trim() : ''
|
|
41
|
+
return { ok: true, 出, 错: '' }
|
|
42
|
+
} catch (错) {
|
|
43
|
+
// 命令本身失败(退出码非 0)时,输出仍可能在文件里——尽量把它带回去
|
|
44
|
+
let 出 = ''
|
|
45
|
+
try {
|
|
46
|
+
const 文件 = join(目录, 'out.txt')
|
|
47
|
+
if (目录 && existsSync(文件)) 出 = readFileSync(文件, 'utf8').trim()
|
|
48
|
+
} catch {}
|
|
49
|
+
return { ok: false, 出, 错: 出 || String((错 && (错.stderr || 错.message)) || 错).trim() }
|
|
50
|
+
} finally {
|
|
51
|
+
try {
|
|
52
|
+
if (目录) rmSync(目录, { recursive: true, force: true })
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 跑一条命令。
|
|
59
|
+
* @returns {{ok:boolean, 出:string, 错:string, 沙箱?:boolean}}
|
|
60
|
+
*/
|
|
61
|
+
export function 跑(命令, 参数 = [], 超时 = 8000) {
|
|
62
|
+
try {
|
|
63
|
+
const 出 = execFileSync(命令, 参数, {
|
|
64
|
+
timeout: 超时,
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
windowsHide: true,
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
+
})
|
|
69
|
+
return { ok: true, 出: String(出).trim(), 错: '' }
|
|
70
|
+
} catch (错) {
|
|
71
|
+
const 码 = String((错 && 错.code) || '')
|
|
72
|
+
const 文 = String((错 && (错.stderr || 错.message)) || 错)
|
|
73
|
+
// 被沙箱拦了:换一条不靠管道的路
|
|
74
|
+
if (码 === 'EPERM' || 码 === 'EACCES' || 码 === 'EPIPE' || /EPERM|EACCES|spawn .*pipe/i.test(文)) {
|
|
75
|
+
遇到过沙箱限制 = true
|
|
76
|
+
const 结果 = 落盘跑(命令, 参数, 超时)
|
|
77
|
+
return { ...结果, 沙箱: true }
|
|
78
|
+
}
|
|
79
|
+
return { ok: false, 出: String((错 && 错.stdout) || '').trim(), 错: 文.trim() }
|
|
80
|
+
}
|
|
81
|
+
}
|