@kira905/dsh-butler-panel 0.9.1 → 0.9.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.
- package/README.md +27 -4
- package/lib/advice-hook.mjs +3 -4
- package/lib/butler-root.mjs +130 -29
- package/lib/engine-search.mjs +138 -0
- package/lib/metrics.mjs +5 -1
- package/lib/modules-route.mjs +6 -7
- package/lib/p4-learnings.mjs +2 -2
- package/lib/shards.mjs +3 -4
- package/lib/state.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,23 @@
|
|
|
30
30
|
|
|
31
31
|
## 接上你自己的大管家
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
面板找库**分四路,前命中即用**:
|
|
34
|
+
|
|
35
|
+
| 路 | 来源 | 说明 |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| ① | `BUTLER_ROOT` / `DSH_BUTLER_LIB_DIR`(还认 `BUTLER_LIB_DIR`) | 你显式指了就以你指的为准,优先级最高 |
|
|
38
|
+
| ② | `<工作区>/tools/dsh-butler/lib` | 工作区约定位置(`DSH_WORKSPACE` 不设时 = `<用户主目录>/DSH`) |
|
|
39
|
+
| ③ | **已安装的 `@kira905/dsh-butler-engine` 包** | 依次试包内 `lib/` → `src/dsh-butler/lib/` → `src/lib/` → `jobs/lib/`。定位包根按「显式 env `DSH_BUTLER_ENGINE_ROOT` → Node 模块解析 → 全局 npm 前缀」三档走 |
|
|
40
|
+
| ④ | 都落空 | 占位 + 一行**可照做**的指引(见下) |
|
|
41
|
+
|
|
42
|
+
为什么要第 ③ 路:不是每台机器都有「大管家工具目录」——那本来是自己工作区里的东西。
|
|
43
|
+
但 `@kira905/dsh-butler-engine` 是**能装的包**,装上就有同一批库。于是最省事的接法变成:
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
npm i -g @kira905/dsh-butler-engine
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
环境变量仍然有用(放在最前面,压过包):
|
|
34
50
|
|
|
35
51
|
| 变量 | 含义 | 不设时 |
|
|
36
52
|
|---|---|---|
|
|
@@ -38,15 +54,22 @@
|
|
|
38
54
|
| `DSH_WORKSPACE` | DSH 工作区根(`tools/` 与 `docs/` 所在那层) | `<用户主目录>/DSH` |
|
|
39
55
|
| `DSH_HOME` / `BUTLER_DSH_HOME` | DSH 数据根(会话、看板账本、状态文件) | `~/.dsh` |
|
|
40
56
|
| `DSH_BUTLER_LIB_DIR` | 只覆盖"共享库目录"这一项 | 跟随 `BUTLER_ROOT` |
|
|
57
|
+
| `DSH_BUTLER_ENGINE_ROOT` | 直接点名 engine 包根(装了但找不到时的逃生口) | 自动探测 |
|
|
41
58
|
|
|
42
59
|
```powershell
|
|
43
60
|
# 例:工具目录放在你自己的某个工作区下
|
|
44
61
|
$env:DSH_WORKSPACE = '<你的工作区>'
|
|
45
62
|
```
|
|
46
63
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
**接不上会怎样**:面板不会崩。缺的那几段退化成「不可用」,并在控制台留一行**告诉你怎么补**:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
[butler-panel] 缺 store.mjs:装 npm i -g @kira905/dsh-butler-engine@^0.5.0,或设 BUTLER_ROOT=<你的大管家工具目录>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
(那行里的版本号是读**装在盘上的包**得来的,不是写死的;包完全没装时就不带版本号。)
|
|
71
|
+
这样设计是因为插件是整棵加载的 —— 任一模块顶层 `import` 失败会把整个插件树拖死、
|
|
72
|
+
连带 `dsh web` 起不来,那比"少显示一段"严重得多。
|
|
50
73
|
|
|
51
74
|
## 它挂在哪里
|
|
52
75
|
|
package/lib/advice-hook.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import os from 'node:os';
|
|
25
25
|
import { pathToFileURL } from 'node:url';
|
|
26
|
-
import {
|
|
26
|
+
import { butlerLibPath } from './butler-root.mjs';
|
|
27
27
|
|
|
28
28
|
/** 本钩子版本(≤ 产品版本,见 lib/version.mjs 的约定)。 */
|
|
29
29
|
export const ADVICE_HOOK_VERSION = '0.1.0';
|
|
@@ -35,7 +35,6 @@ export const NO_ADVICE_FLAG = '--no-advice';
|
|
|
35
35
|
export const ADVICE_LOCK_NAME = 'p5-advice';
|
|
36
36
|
/** 锁 TTL(与 report-advice.mjs 一致)。 */
|
|
37
37
|
export const ADVICE_LOCK_TTL_MS = 180_000;
|
|
38
|
-
const BUTLER_LIB_DIR = butlerLib();
|
|
39
38
|
|
|
40
39
|
/** 本次运行是否显式关闭 P2.5(argv / env 任一命中即关)。 */
|
|
41
40
|
export function isAdviceDisabled({ argv = process.argv.slice(2), env = process.env } = {}) {
|
|
@@ -53,8 +52,8 @@ function resolveHome(env = process.env) {
|
|
|
53
52
|
|
|
54
53
|
/** 默认加载器:动态 import butler lib(Windows 下 ESM 不吃绝对路径 → 必须 pathToFileURL)。 */
|
|
55
54
|
async function defaultLoader() {
|
|
56
|
-
const advice = await import(pathToFileURL(
|
|
57
|
-
const store = await import(pathToFileURL(
|
|
55
|
+
const advice = await import(pathToFileURL(butlerLibPath('advice.mjs')).href);
|
|
56
|
+
const store = await import(pathToFileURL(butlerLibPath('store.mjs')).href);
|
|
58
57
|
return { advice, store };
|
|
59
58
|
}
|
|
60
59
|
|
package/lib/butler-root.mjs
CHANGED
|
@@ -5,20 +5,31 @@
|
|
|
5
5
|
// 为什么需要它:插件部署后落在 `~/.dsh/profiles/web/node_modules/<pkg>/lib/`,
|
|
6
6
|
// 相对路径会解析到 `node_modules/…` 而那里没有大管家工具目录 ⇒ 静态 import 失败会
|
|
7
7
|
// 带崩整个 plugin tree。历史做法是写死 `file:///<绝对路径>` 形式的 URL,能用但把本机目录写进了源码。
|
|
8
|
-
//
|
|
8
|
+
// 本模块把那个绝对前缀换成环境变量,默认值退化为「用户主目录下的约定位置」。
|
|
9
9
|
//
|
|
10
|
-
//
|
|
10
|
+
// ── 解析顺序(**每一路都要能单独说清**)──────────────────────────────────────
|
|
11
11
|
//
|
|
12
|
-
// BUTLER_ROOT
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// 1. 显式指定 `DSH_BUTLER_LIB_DIR` / `BUTLER_ROOT`(`BUTLER_LIB_DIR` 亦认)
|
|
13
|
+
// —— 用户明确指了就以他指的为准,优先级最高,行为不变
|
|
14
|
+
// 2. 工作区约定 `<工作区>/tools/dsh-butler/lib`(工程内既有缺省,保留)
|
|
15
|
+
// 3. npm 包回落(本版新增) `@kira905/dsh-butler-engine` 包内依次试
|
|
16
|
+
// `lib/` → `src/dsh-butler/lib/` → `src/lib/` → `jobs/lib/`
|
|
17
|
+
// (定位包根见 ./engine-search.mjs:显式 env → 模块解析 → 全局前缀)
|
|
18
|
+
// 4. 全落空 占位 + **可照做**的一句指引(见 guidanceFor())
|
|
16
19
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
20
|
+
// 为什么要第 3 路:全新机器上不一定有「大管家工具目录」——那本来是个人工作区里的东西。
|
|
21
|
+
// 但 `@kira905/dsh-butler-engine` 是**可安装的包**,装上就有同样的库。没有它时面板只能整片
|
|
22
|
+
// 显示「不可用」;有了它,装个包面板就能出真数据(A-8 第三关)。
|
|
23
|
+
//
|
|
24
|
+
// 🔴 三件不许做的事
|
|
25
|
+
// · 不许把库代码复制进本包(面板侧不内联一份"最小实现")—— 只**引用**,不**复制**。
|
|
26
|
+
// · 不许把「找不到」伪装成成功:每一路都只认**真实存在的文件**。
|
|
27
|
+
// · 不许跨进程缓存:ESM 有模块缓存,换 env 必须换进程才叫验证(否则是假绿)。
|
|
19
28
|
import os from 'node:os'
|
|
29
|
+
import fs from 'node:fs'
|
|
20
30
|
import path from 'node:path'
|
|
21
31
|
import { pathToFileURL } from 'node:url'
|
|
32
|
+
import { engineLibDirs, engineVersion, engineRoots, ENGINE_PKG } from './engine-search.mjs'
|
|
22
33
|
|
|
23
34
|
/** DSH 数据根:BUTLER_DSH_HOME > DSH_HOME > ~/.dsh。 */
|
|
24
35
|
export function dshHome() {
|
|
@@ -51,7 +62,7 @@ export function butlerRoot() {
|
|
|
51
62
|
|
|
52
63
|
/** 大管家共享库目录下的路径。 */
|
|
53
64
|
export function butlerLib(...seg) {
|
|
54
|
-
return path.join(butlerRoot(), 'lib', ...seg)
|
|
65
|
+
return path.join(butlerLibSearchDirs()[0] ?? path.join(butlerRoot(), 'lib'), ...seg)
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
/** 工作区 `tools/` 下的路径(跨工具共享件,例如 dsh-host-rpc / panel-tokens)。 */
|
|
@@ -79,41 +90,131 @@ export function asUrl(p) {
|
|
|
79
90
|
return pathToFileURL(p).href
|
|
80
91
|
}
|
|
81
92
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
// ── 搜索目录(纯路径,不读文件)────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function dedupe(dirs) {
|
|
96
|
+
const out = []
|
|
97
|
+
const seen = new Set()
|
|
98
|
+
for (const d of dirs) {
|
|
99
|
+
if (!d) continue
|
|
100
|
+
const key = d.toLowerCase()
|
|
101
|
+
if (seen.has(key)) continue
|
|
102
|
+
seen.add(key)
|
|
103
|
+
out.push(d)
|
|
104
|
+
}
|
|
105
|
+
return out
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `<butlerRoot>/lib` 的候选目录,按优先级。第一项当「主目录」用(兼容 no-arg 调用)。 */
|
|
109
|
+
export function butlerLibSearchDirs() {
|
|
110
|
+
const dirs = []
|
|
111
|
+
// ① 显式指定 —— 优先级最高,与旧版逐字一致
|
|
112
|
+
const libDir = (process.env.DSH_BUTLER_LIB_DIR ?? '').trim()
|
|
113
|
+
const legacyLibDir = (process.env.BUTLER_LIB_DIR ?? '').trim()
|
|
114
|
+
if (libDir) dirs.push(path.resolve(libDir))
|
|
115
|
+
if (legacyLibDir) dirs.push(path.resolve(legacyLibDir))
|
|
116
|
+
if ((process.env.BUTLER_ROOT ?? '').trim()) dirs.push(path.join(path.resolve(process.env.BUTLER_ROOT.trim()), 'lib'))
|
|
117
|
+
// ② 工作区约定(旧版缺省)
|
|
118
|
+
dirs.push(path.join(workspaceRoot(), 'tools', 'dsh-butler', 'lib'))
|
|
119
|
+
// ③ npm 包回落
|
|
120
|
+
dirs.push(...engineLibDirs('butler'))
|
|
121
|
+
return dedupe(dirs)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `<工作区>/tools/lib` 的候选目录。 */
|
|
125
|
+
export function toolLibSearchDirs() {
|
|
126
|
+
return dedupe([path.join(workspaceRoot(), 'tools', 'lib'), ...engineLibDirs('tool')])
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 大管家工具根顶层(CLI 脚本)的候选目录。 */
|
|
130
|
+
export function butlerFileSearchDirs() {
|
|
131
|
+
return dedupe([butlerRoot(), ...engineLibDirs('butlerFile')])
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function firstExistingFile(dirs, name) {
|
|
135
|
+
for (const dir of dirs) {
|
|
136
|
+
const p = path.join(dir, name)
|
|
137
|
+
try {
|
|
138
|
+
if (fs.statSync(p).isFile()) return p
|
|
139
|
+
} catch { /* 这一档没有 */ }
|
|
140
|
+
}
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 某个库文件的真实路径:按 butlerLib 的候选目录顺序找,找不到返回旧版缺省路径
|
|
146
|
+
* (调用方据此拼错误信息,用户在提示里看到的仍是"你以为该在的地方")。
|
|
147
|
+
*/
|
|
148
|
+
export function butlerLibPath(name) {
|
|
149
|
+
return firstExistingFile(butlerLibSearchDirs(), name) ?? path.join(butlerRoot(), 'lib', name)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `<工作区>/tools/lib` 版。 */
|
|
153
|
+
export function toolLibPath(name) {
|
|
154
|
+
return firstExistingFile(toolLibSearchDirs(), name) ?? toolLibDir(name)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 大管家工具根顶层版(`butlerFile` 的加载用)。 */
|
|
158
|
+
export function butlerFilePath(name) {
|
|
159
|
+
return firstExistingFile(butlerFileSearchDirs(), name) ?? butlerFile(name)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 诊断用:某文件命中了哪一路(找不到返回 null)。 */
|
|
163
|
+
export function butlerLibHit(name) {
|
|
164
|
+
return firstExistingFile(butlerLibSearchDirs(), name)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── 占位与加载 ─────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 降级话术:**可照做**,不是「不可用」三个字。
|
|
171
|
+
* 版本号取 engine 包的**实际版本**(读不到就退成不带版本的安装命令——不写死一个会过期的号)。
|
|
172
|
+
*/
|
|
173
|
+
export function guidanceFor(name) {
|
|
174
|
+
const v = engineVersion()
|
|
175
|
+
const install = v ? `npm i -g ${ENGINE_PKG}@^${v}` : `npm i -g ${ENGINE_PKG}`
|
|
176
|
+
return `缺 ${name}:装 ${install},或设 BUTLER_ROOT=<你的大管家工具目录>`
|
|
177
|
+
}
|
|
178
|
+
|
|
90
179
|
function unavailableStub(name) {
|
|
91
|
-
const msg =
|
|
92
|
-
const stub = function () { throw new Error(msg) }
|
|
180
|
+
const msg = guidanceFor(name)
|
|
181
|
+
const stub = function () { throw new Error(msg) }
|
|
93
182
|
// 让调用方能在"不触发调用"的前提下认出这是占位(例如 `X = mod.Foo ?? null` 之后再 `new X()`)。
|
|
94
|
-
Object.defineProperty(stub, 'butlerUnavailable', { value: true, enumerable: false })
|
|
183
|
+
Object.defineProperty(stub, 'butlerUnavailable', { value: true, enumerable: false })
|
|
184
|
+
// 排障用:到底找过哪些目录、装没装包(不进错误文本,避免刷屏)
|
|
185
|
+
Object.defineProperty(stub, 'butlerSearched', {
|
|
186
|
+
value: { dirs: butlerLibSearchDirs(), engineRoots: engineRoots() },
|
|
187
|
+
enumerable: false,
|
|
188
|
+
})
|
|
95
189
|
// `then` 必须显式排除:否则这个对象会被当成 thenable,await / Promise.resolve 会挂住。
|
|
96
|
-
return new Proxy({}, { get: (_t, k) => (k === 'then' ? undefined : stub) })
|
|
190
|
+
return new Proxy({}, { get: (_t, k) => (k === 'then' ? undefined : stub) })
|
|
97
191
|
}
|
|
98
192
|
|
|
99
|
-
const MISSING_CODES = new Set(['ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND', 'ENOENT'])
|
|
193
|
+
const MISSING_CODES = new Set(['ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND', 'ENOENT'])
|
|
100
194
|
|
|
101
|
-
async function loadOrStub(
|
|
195
|
+
async function loadOrStub(resolvePath, name) {
|
|
196
|
+
const target = resolvePath(name)
|
|
197
|
+
let exists = false
|
|
198
|
+
try { exists = fs.statSync(target).isFile() } catch { /* 不存在 */ }
|
|
199
|
+
if (!exists) {
|
|
200
|
+
try { console.warn(`[butler-panel] ${guidanceFor(name)}`) } catch { /* 控制台不可用就算了 */ }
|
|
201
|
+
return unavailableStub(name)
|
|
202
|
+
}
|
|
102
203
|
try {
|
|
103
|
-
return await import(
|
|
204
|
+
return await import(asUrl(target))
|
|
104
205
|
} catch (e) {
|
|
206
|
+
// 文件在、但它自己的依赖缺(例如只搬了半个闭包)——同样降级,不让面板整棵挂掉。
|
|
105
207
|
if (MISSING_CODES.has(e?.code)) {
|
|
106
|
-
try { console.warn(`[butler-panel] ${
|
|
208
|
+
try { console.warn(`[butler-panel] ${guidanceFor(name)}(依赖不全:${String(e?.message ?? e).split('\n')[0]})`) } catch { /* 同上 */ }
|
|
107
209
|
return unavailableStub(name)
|
|
108
210
|
}
|
|
109
211
|
throw e
|
|
110
212
|
}
|
|
111
213
|
}
|
|
112
|
-
function msgOf(name) { return `大管家库缺失(${name})⇒ 该功能降级为「不可用」` }
|
|
113
214
|
|
|
114
|
-
export function importButlerLib(name) { return loadOrStub(
|
|
115
|
-
export function importToolLib(name) { return loadOrStub(
|
|
116
|
-
export function importButlerFile(name) { return loadOrStub(
|
|
215
|
+
export function importButlerLib(name) { return loadOrStub(butlerLibPath, name) }
|
|
216
|
+
export function importToolLib(name) { return loadOrStub(toolLibPath, name) }
|
|
217
|
+
export function importButlerFile(name) { return loadOrStub(butlerFilePath, name) }
|
|
117
218
|
|
|
118
219
|
/**
|
|
119
220
|
* 状态文件路径解析器:优先用大管家 lib 的 `resolveStatePath`;库不可用时退到**等价的最简形态**
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// dsh-butler-panel / lib/engine-search.mjs
|
|
2
|
+
//
|
|
3
|
+
// 只做一件事:**定位 @kira905/dsh-butler-engine 这个包在哪、它里面有哪些库目录**。
|
|
4
|
+
//
|
|
5
|
+
// 为什么要单独一件:面板里不止一处需要"找库"(butler-root 的加载器、metrics 的环境探测),
|
|
6
|
+
// 而 metrics.mjs 明确承诺自己不 import 面板的业务模块。把这段纯路径逻辑抽成无副作用的
|
|
7
|
+
// 小件,两边都能用,又不把业务耦合带进去。
|
|
8
|
+
//
|
|
9
|
+
// 🔴 为什么不只靠 createRequire:engine 在安装清单里是 **CLI 类型**(`npm i -g`),
|
|
10
|
+
// 落在全局前缀(Windows 是 `%APPDATA%\npm\node_modules`),而面板落在
|
|
11
|
+
// `~/.dsh/profiles/web/node_modules/`。Node 的模块解析只会从面板所在目录**向上**爬,
|
|
12
|
+
// 爬不到全局前缀 ⇒ 单靠 createRequire 在真实装载形态下必然找不到。
|
|
13
|
+
// 所以这里按「显式指定 → 模块解析 → 全局前缀常见位置」三档走,每档都只读、失败不抛。
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import os from 'node:os'
|
|
16
|
+
import path from 'node:path'
|
|
17
|
+
import { createRequire } from 'node:module'
|
|
18
|
+
|
|
19
|
+
export const ENGINE_PKG = '@kira905/dsh-butler-engine'
|
|
20
|
+
|
|
21
|
+
/** 各调用方要的"库目录"在包内的候选位置(顺序 = 优先级)。 */
|
|
22
|
+
export const ENGINE_SUBDIRS = {
|
|
23
|
+
// <butlerRoot>/lib 的镜像:engine 把生产 tools/dsh-butler/lib 放在 src/dsh-butler/lib
|
|
24
|
+
butler: ['lib', 'src/dsh-butler/lib', 'src/lib', 'jobs/lib'],
|
|
25
|
+
// <workspace>/tools/lib 的镜像:engine 放在 src/lib
|
|
26
|
+
tool: ['src/lib', 'lib', 'src/dsh-butler/lib', 'jobs/lib'],
|
|
27
|
+
// <butlerRoot> 顶层的镜像(CLI 脚本在 src/dsh-butler/,作业脚本在 jobs/)
|
|
28
|
+
butlerFile: ['src/dsh-butler', 'jobs', 'lib', 'src/dsh-butler/lib'],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const require_ = createRequire(import.meta.url)
|
|
32
|
+
|
|
33
|
+
/** 从某个入口文件向上爬,找到 name 匹配的 package.json 所在目录。 */
|
|
34
|
+
function pkgRootOf(entry) {
|
|
35
|
+
if (!entry) return null
|
|
36
|
+
let dir = path.dirname(entry)
|
|
37
|
+
for (let i = 0; i < 10; i++) {
|
|
38
|
+
try {
|
|
39
|
+
const j = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
|
40
|
+
if (j && j.name === ENGINE_PKG) return dir
|
|
41
|
+
} catch { /* 这一层没有或坏了,继续上溯 */ }
|
|
42
|
+
const up = path.dirname(dir)
|
|
43
|
+
if (up === dir) return null
|
|
44
|
+
dir = up
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 全局 npm 前缀下包可能出现的位置(不 spawn npm,只按约定路径猜)。 */
|
|
50
|
+
function globalNodeModules() {
|
|
51
|
+
const out = []
|
|
52
|
+
const add = (p) => { if (p && String(p).trim()) out.push(path.resolve(String(p).trim())) }
|
|
53
|
+
|
|
54
|
+
const prefix = process.env.npm_config_prefix || process.env.NPM_CONFIG_PREFIX
|
|
55
|
+
if (prefix) {
|
|
56
|
+
add(path.join(prefix, 'node_modules'))
|
|
57
|
+
add(path.join(prefix, 'lib', 'node_modules'))
|
|
58
|
+
}
|
|
59
|
+
if (process.env.APPDATA) add(path.join(process.env.APPDATA, 'npm', 'node_modules'))
|
|
60
|
+
if (process.env.LOCALAPPDATA) add(path.join(process.env.LOCALAPPDATA, 'npm', 'node_modules'))
|
|
61
|
+
|
|
62
|
+
// node 自己就在前缀根里时(自定义安装、绿色版、nvm 等)
|
|
63
|
+
const exeDir = path.dirname(process.execPath)
|
|
64
|
+
add(path.join(exeDir, 'node_modules'))
|
|
65
|
+
add(path.join(exeDir, '..', 'lib', 'node_modules'))
|
|
66
|
+
|
|
67
|
+
add(path.join(os.homedir(), '.npm-global', 'lib', 'node_modules'))
|
|
68
|
+
if (process.platform !== 'win32') {
|
|
69
|
+
add('/usr/local/lib/node_modules')
|
|
70
|
+
add('/usr/lib/node_modules')
|
|
71
|
+
}
|
|
72
|
+
return out
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let cachedRoots = null
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* engine 包根候选(按优先级,已去重、已过滤不存在的)。
|
|
79
|
+
* 返回 [] = 这台机器上压根没装 engine。
|
|
80
|
+
*/
|
|
81
|
+
export function engineRoots() {
|
|
82
|
+
if (cachedRoots) return cachedRoots
|
|
83
|
+
const roots = []
|
|
84
|
+
const push = (p) => {
|
|
85
|
+
if (!p) return
|
|
86
|
+
const abs = path.resolve(p)
|
|
87
|
+
if (roots.some((x) => x.toLowerCase() === abs.toLowerCase())) return
|
|
88
|
+
roots.push(abs)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ① 显式指定(用户/测试可钉死;装了但找不到时这也是逃生口)
|
|
92
|
+
const explicit = (process.env.DSH_BUTLER_ENGINE_ROOT ?? '').trim()
|
|
93
|
+
if (explicit) push(explicit)
|
|
94
|
+
|
|
95
|
+
// ② Node 自己的模块解析(面板与 engine 同树 / 被提升到同一 node_modules 时命中)
|
|
96
|
+
for (const spec of [`${ENGINE_PKG}/package.json`, ENGINE_PKG]) {
|
|
97
|
+
try { push(pkgRootOf(require_.resolve(spec))) } catch { /* 这一路走不通就下一路 */ }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ③ 全局前缀(真实装载形态:engine 是 CLI,装到全局)
|
|
101
|
+
for (const nm of globalNodeModules()) {
|
|
102
|
+
const cand = path.join(nm, ...ENGINE_PKG.split('/'))
|
|
103
|
+
try {
|
|
104
|
+
const j = JSON.parse(fs.readFileSync(path.join(cand, 'package.json'), 'utf8'))
|
|
105
|
+
if (j && j.name === ENGINE_PKG) push(cand)
|
|
106
|
+
} catch { /* 不在这一档,继续 */ }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
cachedRoots = roots.filter((r) => fs.existsSync(path.join(r, 'package.json')))
|
|
110
|
+
return cachedRoots
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 某个用途下**真实存在**的库目录(绝对路径,按优先级)。 */
|
|
114
|
+
export function engineLibDirs(kind = 'butler') {
|
|
115
|
+
const subdirs = ENGINE_SUBDIRS[kind] ?? ENGINE_SUBDIRS.butler
|
|
116
|
+
const out = []
|
|
117
|
+
for (const root of engineRoots()) {
|
|
118
|
+
for (const sub of subdirs) {
|
|
119
|
+
const dir = path.join(root, sub)
|
|
120
|
+
try { if (fs.statSync(dir).isDirectory()) out.push(dir) } catch { /* 没有这层 */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** engine 的实际版本号(用于降级话术里给可照做的安装命令);读不到返回 null。 */
|
|
127
|
+
export function engineVersion() {
|
|
128
|
+
for (const root of engineRoots()) {
|
|
129
|
+
try {
|
|
130
|
+
const j = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
131
|
+
if (j?.version) return String(j.version)
|
|
132
|
+
} catch { /* 下一处 */ }
|
|
133
|
+
}
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 测试用:清掉缓存(换过 env 必须换进程,否则是假绿)。 */
|
|
138
|
+
export function resetEngineCache() { cachedRoots = null }
|
package/lib/metrics.mjs
CHANGED
|
@@ -71,7 +71,8 @@
|
|
|
71
71
|
|
|
72
72
|
import path from 'node:path';
|
|
73
73
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
74
|
-
import { createHash } from 'node:crypto';
|
|
74
|
+
import { createHash } from 'node:crypto';
|
|
75
|
+
import { engineLibDirs } from './engine-search.mjs';
|
|
75
76
|
|
|
76
77
|
/** 指标模块版本(唯一入口;接口字段名自 A1 冻结后不得改)。 */
|
|
77
78
|
// 0.2.1(2026-09-18 · 施工卡 ⑥-2「面板额度字段」):`autoActionsToday` **只追加** `quotaGate`
|
|
@@ -225,6 +226,9 @@ const BUTLER_LIB_DIRS = [
|
|
|
225
226
|
(process.env.BUTLER_ROOT ?? '').trim() ? path.join(process.env.BUTLER_ROOT.trim(), 'lib') : null,
|
|
226
227
|
(process.env.DSH_WORKSPACE ?? '').trim() ? path.join(process.env.DSH_WORKSPACE.trim(), 'tools', 'dsh-butler', 'lib') : null,
|
|
227
228
|
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'dsh-butler', 'lib'),
|
|
229
|
+
// 全新机器上没有「大管家工具目录」,但可能有已安装的 npm 包(@kira905/dsh-butler-engine)。
|
|
230
|
+
// 这里只借 engine-search 的**纯路径**能力,不碰任何面板业务模块,本文件的零依赖性质不变。
|
|
231
|
+
...engineLibDirs('butler'),
|
|
228
232
|
].filter(Boolean);
|
|
229
233
|
|
|
230
234
|
async function tryImport(url) {
|
package/lib/modules-route.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { pathToFileURL } from 'node:url';
|
|
14
|
-
import {
|
|
14
|
+
import { butlerLibPath } from './butler-root.mjs';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* 默认 home(DSH 数据根)解析:口径 BUTLER_DSH_HOME > DSH_HOME > ~/.dsh
|
|
@@ -27,7 +27,6 @@ export function dshHome() {
|
|
|
27
27
|
if (override && override.trim()) return path.resolve(override.trim());
|
|
28
28
|
return path.join(os.homedir(), '.dsh');
|
|
29
29
|
}
|
|
30
|
-
const BUTLER_LIB = butlerLibDir();
|
|
31
30
|
|
|
32
31
|
/** runAction() 第三道闸恒查的 L1 键(与 CLI `module list` 末尾恒定口径同源;lib\autonomy-actions.mjs,位置=dry-run 通道之后)。 */
|
|
33
32
|
export const RUN_ACTION_ALWAYS_KEY = 'p3AutoEnabled';
|
|
@@ -66,15 +65,15 @@ let libCache = null;
|
|
|
66
65
|
/** 动态 import butler 侧 lib(缺失/损坏 → 抛给调用方降级,绝不拖垮面板启动)。 */
|
|
67
66
|
async function butlerLib() {
|
|
68
67
|
if (libCache) return libCache;
|
|
69
|
-
const mods = await import(pathToFileURL(
|
|
70
|
-
const auto = await import(pathToFileURL(
|
|
68
|
+
const mods = await import(pathToFileURL(butlerLibPath('autonomy-modules.mjs')).href);
|
|
69
|
+
const auto = await import(pathToFileURL(butlerLibPath('autonomy.mjs')).href);
|
|
71
70
|
// 🌒 影子视图(卡:V3 批次 1):缺失不影响模块开关卡本身 → 单独 try,降级为 null
|
|
72
71
|
let shadow = null;
|
|
73
|
-
try { shadow = await import(pathToFileURL(
|
|
72
|
+
try { shadow = await import(pathToFileURL(butlerLibPath('shadow-view.mjs')).href); } catch { shadow = null; }
|
|
74
73
|
// ① 包 P-3/P-5(施工卡 ①-1):控制室判据底座(**唯一判定入口**)——
|
|
75
74
|
// 缺失/损坏 → 降级为 null(本卡照常渲染既有字段,绝不拖垮面板)。
|
|
76
75
|
let verdict = null;
|
|
77
|
-
try { verdict = await import(pathToFileURL(
|
|
76
|
+
try { verdict = await import(pathToFileURL(butlerLibPath('control-room-verdict.mjs')).href); } catch { verdict = null; }
|
|
78
77
|
libCache = { mods, auto, shadow, verdict };
|
|
79
78
|
return libCache;
|
|
80
79
|
}
|
|
@@ -218,7 +217,7 @@ export const INFRA_REFUSE_CODE = 'infra-protected';
|
|
|
218
217
|
/** 拒关留痕(走既有审计哈希链 `lib/audit.mjs`;审计不可用**不阻断**拒关应答)。 */
|
|
219
218
|
async function recordModuleRefusal({ home, id, scope, by, why, source }) {
|
|
220
219
|
try {
|
|
221
|
-
const m = await import(pathToFileURL(
|
|
220
|
+
const m = await import(pathToFileURL(butlerLibPath('audit.mjs')).href);
|
|
222
221
|
m?.appendAudit?.({ type: 'module-refused', module: id, reason: INFRA_REFUSE_CODE, scope, source, by, why },
|
|
223
222
|
{ home, silent: true });
|
|
224
223
|
} catch { /* 审计不可用不影响"拒关"这个保护动作本身 */ }
|
package/lib/p4-learnings.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import fs from 'node:fs';
|
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import os from 'node:os';
|
|
19
19
|
// 【2c · 2c-B5】状态路径统一入口(legacy 期与 path.join(dir, name) 逐字节相同)。
|
|
20
|
-
import {
|
|
20
|
+
import { butlerLibPath, statePathResolver } from './butler-root.mjs';
|
|
21
21
|
const resolveStatePath = await statePathResolver();
|
|
22
22
|
|
|
23
23
|
export const P4_LEARNINGS_VERSION = '0.1.0';
|
|
@@ -39,7 +39,7 @@ let scoreLibCache = null;
|
|
|
39
39
|
async function loadScoreLib() {
|
|
40
40
|
if (scoreLibCache) return scoreLibCache;
|
|
41
41
|
const { pathToFileURL } = await import('node:url');
|
|
42
|
-
scoreLibCache = await import(pathToFileURL(
|
|
42
|
+
scoreLibCache = await import(pathToFileURL(butlerLibPath('pitfall-score.mjs')).href);
|
|
43
43
|
return scoreLibCache;
|
|
44
44
|
}
|
|
45
45
|
|
package/lib/shards.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import { STATE_CACHE_TTL_MS, SHARD_BUDGET_MS, cacheKeyOf, singleFlight, createBu
|
|
|
41
41
|
import { readPanelSwitches } from './panel-switches.mjs';
|
|
42
42
|
import { collectMetrics } from './metrics.mjs';
|
|
43
43
|
// 【2c · 2c-B5】状态路径统一入口(legacy 期与 path.join(home, name) 逐字节相同)。
|
|
44
|
-
import {
|
|
44
|
+
import { butlerLibPath, statePathResolver } from './butler-root.mjs';
|
|
45
45
|
const resolveStatePath = await statePathResolver();
|
|
46
46
|
|
|
47
47
|
/** 已实现的片名(顺序即文档顺序)。
|
|
@@ -96,8 +96,7 @@ export const FEED_ITEM_CAP = 8;
|
|
|
96
96
|
export const FEED_BYTES_CAP = 5_600;
|
|
97
97
|
/** health 片超时预算(生态面板 HTTP 是固定价,独立于 core 的 1.2s)。 */
|
|
98
98
|
export const HEALTH_BUDGET_MS = 6_000;
|
|
99
|
-
/** butler lib
|
|
100
|
-
const BUTLER_LIB_DIR = (process.env.BUTLER_LIB_DIR ?? '').trim() || butlerLibDir();
|
|
99
|
+
/** butler lib 解析口径:交给 butler-root 的多路回落(显式 → 工作区约定 → npm 包),本文件不再自己拼目录。 */
|
|
101
100
|
|
|
102
101
|
// ---------------------------------------------------------------------------
|
|
103
102
|
// 片白名单(R1:`?parts=core` 必须**子字段级**可断言;M3 要求 core 明写 sessions.interrupted)
|
|
@@ -199,7 +198,7 @@ async function butlerLib(name) {
|
|
|
199
198
|
if (butlerLibraries.has(name)) return butlerLibraries.get(name);
|
|
200
199
|
let mod = null;
|
|
201
200
|
try {
|
|
202
|
-
mod = await import(pathToFileURL(
|
|
201
|
+
mod = await import(pathToFileURL(butlerLibPath(name)).href);
|
|
203
202
|
} catch { mod = null; }
|
|
204
203
|
butlerLibraries.set(name, mod);
|
|
205
204
|
return mod;
|
package/lib/state.js
CHANGED
|
@@ -18,7 +18,7 @@ import * as eva from './host-adapter.mjs';
|
|
|
18
18
|
// 会话日志文件名判据 / 「取哪份」取法 —— **生态唯一源**(★2026-09-24 本批统一,见下方 newestSessionLog 上方长注)。
|
|
19
19
|
// 跨包一律走 ./butler-root.mjs 解析(路径由环境变量决定,源码里不留本机绝对目录;
|
|
20
20
|
// 相对 import 会在部署到 profile 后解析成不存在的包 ⇒ 门禁①直接挡)。
|
|
21
|
-
import { importButlerLib, importToolLib, workspaceRoot,
|
|
21
|
+
import { importButlerLib, importToolLib, workspaceRoot, butlerLibPath, asUrl, statePathResolver } from './butler-root.mjs';
|
|
22
22
|
const { SESSION_FILE_RE, pickSessionFileInDir } = await importToolLib('session-file-name.mjs');
|
|
23
23
|
// 【2c · 2c-B5】状态路径统一入口(legacy 期与 path.join(dshHome(), name) 逐字节相同)。
|
|
24
24
|
const resolveStatePath = await statePathResolver();
|
|
@@ -1085,12 +1085,12 @@ const MAINTENANCE_FILE = resolveStatePath('butler-maintenance.json', { home: dsh
|
|
|
1085
1085
|
const RESTART_REQUEST_FILE = process.env.BUTLER_RESTART_REQUEST_FILE
|
|
1086
1086
|
|| path.join(process.env.TEMP || os.tmpdir(), 'dsh-guard', 'restart-request.json');
|
|
1087
1087
|
const AUDIT_DIR = path.join(dshHome(), 'butler-audit');
|
|
1088
|
-
const BUTLER_LIB_DIR = butlerLibDir();
|
|
1089
1088
|
/** 计划任务每分钟一次 → 3 分钟没刷新心跳 = 协调器被停/被拦(验收 6)。 */
|
|
1090
1089
|
const HEARTBEAT_STALE_MS = 3 * 60 * 1000;
|
|
1091
1090
|
|
|
1091
|
+
// 库路径统一交给 butler-root 的多路回落(显式指定 → 工作区约定 → 已安装的 npm 包)。
|
|
1092
1092
|
async function butlerLib(name) {
|
|
1093
|
-
return import(asUrl(
|
|
1093
|
+
return import(asUrl(butlerLibPath(name)));
|
|
1094
1094
|
}
|
|
1095
1095
|
function readJsonFile(file, fallback = null) {
|
|
1096
1096
|
try {
|