dsh-x-core 0.1.21 → 0.1.23
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/assets/dsh-preview.png +0 -0
- package/assets/dsh.ico +0 -0
- package/assets/icon.ico +0 -0
- package/assets/tray.ico +0 -0
- package/assets/tray.png +0 -0
- package/core-npm.js +201 -183
- package/desktop-plugins.js +86 -0
- package/desktop.js +200 -0
- package/filemanager-base.js +7 -8
- package/filemanager-linux.js +124 -0
- package/filemanager.js +4 -7
- package/native-notify.js +74 -0
- package/node/node +2 -1
- package/package.json +1 -1
- package/packs/dsh-x-workbench/README.md +61 -0
- package/packs/dsh-x-workbench/dspack.json +4 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/LICENSE-APACHE-2.0.txt +202 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/NOTICE.md +6 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/SKILL.md +697 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/authentication.md +313 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/commands.md +300 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/profiling.md +120 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/proxy-support.md +194 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/session-management.md +201 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/snapshot-refs.md +229 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/video-recording.md +173 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/templates/authenticated-session.sh +105 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/templates/capture-workflow.sh +69 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/templates/form-automation.sh +62 -0
- package/packs/dsh-x-workbench/home/skills/electron/LICENSE-APACHE-2.0.txt +202 -0
- package/packs/dsh-x-workbench/home/skills/electron/NOTICE.md +6 -0
- package/packs/dsh-x-workbench/home/skills/electron/SKILL.md +243 -0
- package/packs/dsh-x-workbench/manifest.json +35 -0
- package/perf/patch-hooks.mjs +14 -1
- package/perf/plugin-profile.mjs +204 -0
- package/perf/register.mjs +4 -0
- package/platform.js +2 -1
- package/public/booting.html +475 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/icon.png +0 -0
- package/public/index.html +9277 -8173
- package/public/internal-tabs.js +45 -41
- package/public/launcher.css +285 -98
- package/public/mascot.js +1 -1
- package/public/theme.css +33 -6
- package/public/water.js +1 -1
- package/registry.js +38 -3
- package/server.js +1060 -119
- package/settings.js +78 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件启动体检:正常启动路径上顺手采样,按包统计启动期 CPU 与内存分配。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不用「每个 entry 的墙钟耗时」:Cordis 是并发初始化插件的,所有 entry 同时开始,
|
|
5
|
+
* 逐个计时每个人都是 3–5 秒,区分不出谁真的费时。这里改用 V8 采样剖析:
|
|
6
|
+
* - CPU:Profiler 采样主线程,按调用栈 URL 归到包,得到每个包的独占 CPU 时间;
|
|
7
|
+
* - 内存:HeapProfiler 采样分配,按调用栈 URL 归到包,得到启动期分配量(不是常驻占用)。
|
|
8
|
+
*
|
|
9
|
+
* 只观测,不改变启动顺序:采样器本身开销约 1–2%,启动稳定后停掉并写一个 JSON。
|
|
10
|
+
* loader 钩子锚点匹配不到时(dsh 升级改了实现)没有 entry 时间线,但采样仍然有效。
|
|
11
|
+
*/
|
|
12
|
+
import { Session } from 'node:inspector/promises'
|
|
13
|
+
import { mkdirSync, renameSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
const KEY = Symbol.for('dsh-x/plugin-profile')
|
|
17
|
+
const DIR = process.env.DSH_X_PLUGIN_PROFILE_DIR || ''
|
|
18
|
+
const NAME = process.env.DSH_X_PLUGIN_PROFILE || ''
|
|
19
|
+
const VERSION = process.env.DSH_VERSION || ''
|
|
20
|
+
const PROFILE = process.env.DSH_PROFILE || ''
|
|
21
|
+
/** 启动器给的「版本@profile」才是这次实例的真实身份(DSH_PROFILE 只是管理器当前选中的那份)。 */
|
|
22
|
+
const [REPORT_VERSION, REPORT_PROFILE] = NAME.includes('@')
|
|
23
|
+
? [NAME.slice(0, NAME.indexOf('@')), NAME.slice(NAME.indexOf('@') + 1)]
|
|
24
|
+
: [VERSION, PROFILE]
|
|
25
|
+
/** 落盘文件名只留安全字符,报告内容仍用原始版本/profile。 */
|
|
26
|
+
const REPORT_NAME = (NAME || `${VERSION}@${PROFILE}`).replace(/[^0-9A-Za-z@._-]/g, '_')
|
|
27
|
+
/** CPU 采样间隔(微秒):1ms,开销小到启动时间上看不出来。 */
|
|
28
|
+
const CPU_INTERVAL_US = 1000
|
|
29
|
+
/** 最后一个 entry 稳定后再等这么久才收尾:给异步 apply 留出完成时间。 */
|
|
30
|
+
const IDLE_MS = 1500
|
|
31
|
+
/** 兜底:有 entry 一直不结算时也不能让采样器永远开着。 */
|
|
32
|
+
const MAX_MS = 120_000
|
|
33
|
+
|
|
34
|
+
const pending = new Map()
|
|
35
|
+
const entries = []
|
|
36
|
+
let session = null
|
|
37
|
+
let startedAt = 0
|
|
38
|
+
let timer = null
|
|
39
|
+
let cap = null
|
|
40
|
+
let stopped = false
|
|
41
|
+
|
|
42
|
+
function packageOf(url, names) {
|
|
43
|
+
const normalized = String(url || '').replace(/\\/g, '/')
|
|
44
|
+
if (!normalized) return '(原生/GC)'
|
|
45
|
+
const matched = /node_modules\/(@[^/]+\/[^/]+|[^/]+)\//.exec(normalized)
|
|
46
|
+
if (matched) return matched[1]
|
|
47
|
+
// 链接安装的插件不在 node_modules 下:用 loader 报上来的包名对路径
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
if (normalized.includes(`/${name}/`)) return name
|
|
50
|
+
}
|
|
51
|
+
if (normalized.startsWith('node:')) return '(Node 内部)'
|
|
52
|
+
if (normalized.includes('/perf/')) return '(启动器钩子)'
|
|
53
|
+
return '(其他)'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function summarize(cpuProfile, heapProfile) {
|
|
57
|
+
const names = new Set(entries.map((row) => row.name).filter((name) => name && !name.startsWith('cordis:')))
|
|
58
|
+
const nodes = new Map((cpuProfile.nodes || []).map((node) => [node.id, node]))
|
|
59
|
+
const cpuUs = new Map()
|
|
60
|
+
const nativeUs = new Map()
|
|
61
|
+
let totalUs = 0
|
|
62
|
+
let idleUs = 0
|
|
63
|
+
const samples = cpuProfile.samples || []
|
|
64
|
+
const deltas = cpuProfile.timeDeltas || []
|
|
65
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
66
|
+
const delta = deltas[index] || 0
|
|
67
|
+
totalUs += delta
|
|
68
|
+
const node = nodes.get(samples[index])
|
|
69
|
+
const fn = node?.callFrame?.functionName || ''
|
|
70
|
+
if (fn === '(idle)' || fn === '(root)') { idleUs += delta; continue }
|
|
71
|
+
const url = node?.callFrame?.url || ''
|
|
72
|
+
if (!url) {
|
|
73
|
+
// 没有 URL 的帧:GC、V8 编译/内置函数。单列出来,否则会盖住插件的账。
|
|
74
|
+
const key = fn || '(匿名原生)'
|
|
75
|
+
nativeUs.set(key, (nativeUs.get(key) || 0) + delta)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
const pkg = packageOf(url, names)
|
|
79
|
+
cpuUs.set(pkg, (cpuUs.get(pkg) || 0) + delta)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const allocBytes = new Map()
|
|
83
|
+
let totalAlloc = 0
|
|
84
|
+
const walk = (node) => {
|
|
85
|
+
if (!node) return
|
|
86
|
+
const size = node.selfSize || 0
|
|
87
|
+
if (size > 0) {
|
|
88
|
+
const pkg = packageOf(node.callFrame?.url, names)
|
|
89
|
+
allocBytes.set(pkg, (allocBytes.get(pkg) || 0) + size)
|
|
90
|
+
totalAlloc += size
|
|
91
|
+
}
|
|
92
|
+
for (const child of node.children || []) walk(child)
|
|
93
|
+
}
|
|
94
|
+
walk(heapProfile?.head)
|
|
95
|
+
|
|
96
|
+
const packages = [...new Set([...cpuUs.keys(), ...allocBytes.keys()])].map((name) => ({
|
|
97
|
+
name,
|
|
98
|
+
cpuMs: Math.round((cpuUs.get(name) || 0) / 1000),
|
|
99
|
+
allocKB: Math.round((allocBytes.get(name) || 0) / 1024),
|
|
100
|
+
plugin: names.has(name),
|
|
101
|
+
})).sort((a, b) => b.cpuMs - a.cpuMs)
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
version: REPORT_VERSION,
|
|
105
|
+
profile: REPORT_PROFILE,
|
|
106
|
+
at: Date.now(),
|
|
107
|
+
windowMs: Math.round(performance.now() - startedAt),
|
|
108
|
+
cpuMs: Math.round(totalUs / 1000),
|
|
109
|
+
idleMs: Math.round(idleUs / 1000),
|
|
110
|
+
allocMB: +(totalAlloc / 1048576).toFixed(1),
|
|
111
|
+
packages,
|
|
112
|
+
native: [...nativeUs.entries()]
|
|
113
|
+
.map(([name, us]) => ({ name, cpuMs: Math.round(us / 1000) }))
|
|
114
|
+
.sort((a, b) => b.cpuMs - a.cpuMs)
|
|
115
|
+
.slice(0, 12),
|
|
116
|
+
entries,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function writeReport(report) {
|
|
121
|
+
try {
|
|
122
|
+
mkdirSync(DIR, { recursive: true })
|
|
123
|
+
const file = join(DIR, `${REPORT_NAME}.json`)
|
|
124
|
+
const temp = `${file}.tmp`
|
|
125
|
+
writeFileSync(temp, JSON.stringify(report))
|
|
126
|
+
renameSync(temp, file)
|
|
127
|
+
} catch { /* 体检写不进去不能影响启动 */ }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function stop() {
|
|
131
|
+
if (stopped) return
|
|
132
|
+
stopped = true
|
|
133
|
+
if (timer !== null) { clearTimeout(timer); timer = null }
|
|
134
|
+
if (cap !== null) { clearTimeout(cap); cap = null }
|
|
135
|
+
if (!session) return
|
|
136
|
+
try {
|
|
137
|
+
const cpu = await session.post('Profiler.stop')
|
|
138
|
+
// 堆分配采样已撤:账本只看 CPU;内存用启动结算时读一次 RSS。
|
|
139
|
+
writeReport(summarize(cpu.profile, null))
|
|
140
|
+
} catch { /* 采样失败不能影响运行 */ }
|
|
141
|
+
try { session.disconnect() } catch { /* ignore */ }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function schedule() {
|
|
145
|
+
if (timer !== null) clearTimeout(timer)
|
|
146
|
+
timer = setTimeout(() => { void stop() }, IDLE_MS)
|
|
147
|
+
timer.unref?.()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function settle(entry, failed) {
|
|
151
|
+
const row = pending.get(entry)
|
|
152
|
+
if (!row || row.done) return
|
|
153
|
+
row.done = true
|
|
154
|
+
pending.delete(entry)
|
|
155
|
+
entries.push({
|
|
156
|
+
name: row.name,
|
|
157
|
+
startMs: Math.round(row.started - startedAt),
|
|
158
|
+
endMs: Math.round(performance.now() - startedAt),
|
|
159
|
+
heapKB: Math.round(process.memoryUsage().heapUsed / 1024),
|
|
160
|
+
...(failed ? { failed: true } : {}),
|
|
161
|
+
})
|
|
162
|
+
schedule()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function start() {
|
|
166
|
+
if (!DIR) return
|
|
167
|
+
try {
|
|
168
|
+
session = new Session()
|
|
169
|
+
session.connect()
|
|
170
|
+
await session.post('Profiler.enable')
|
|
171
|
+
await session.post('Profiler.setSamplingInterval', { interval: CPU_INTERVAL_US })
|
|
172
|
+
await session.post('Profiler.start')
|
|
173
|
+
startedAt = performance.now()
|
|
174
|
+
cap = setTimeout(() => { void stop() }, MAX_MS)
|
|
175
|
+
cap.unref?.()
|
|
176
|
+
} catch {
|
|
177
|
+
session = null
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function installPluginProfile() {
|
|
182
|
+
if (!DIR) return
|
|
183
|
+
globalThis[KEY] = {
|
|
184
|
+
enter(entry) {
|
|
185
|
+
if (!entry || pending.has(entry)) return
|
|
186
|
+
pending.set(entry, { name: nameOf(entry), started: performance.now() })
|
|
187
|
+
},
|
|
188
|
+
imported(entry) {
|
|
189
|
+
const row = pending.get(entry)
|
|
190
|
+
if (!row) return
|
|
191
|
+
const fiber = entry?.fiber
|
|
192
|
+
if (fiber && typeof fiber.await === 'function') {
|
|
193
|
+
// fiber 进入 active(或失败)才算这个插件启动完;apply 里的异步初始化都在里面。
|
|
194
|
+
fiber.await().then(() => settle(entry, false), () => settle(entry, true))
|
|
195
|
+
} else settle(entry, false)
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
void start()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function nameOf(entry) {
|
|
202
|
+
const name = entry?.options?.name
|
|
203
|
+
return typeof name === 'string' && name ? name : String(entry?.options?.id || 'unknown')
|
|
204
|
+
}
|
package/perf/register.mjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { registerHooks } from 'node:module'
|
|
2
2
|
import { load } from './patch-hooks.mjs'
|
|
3
|
+
import { installPluginProfile } from './plugin-profile.mjs'
|
|
3
4
|
import { createStartupResolver, installationRoots, STARTUP_RESOLVER } from './startup-resolver.mjs'
|
|
4
5
|
|
|
6
|
+
// 启动 CPU 账本:只在启动器给了输出目录时启用,启动窗口内采一次,结算后停。
|
|
7
|
+
installPluginProfile()
|
|
8
|
+
|
|
5
9
|
const resolver = createStartupResolver({
|
|
6
10
|
roots: installationRoots(process.env.DSH_BIN),
|
|
7
11
|
enabled: process.env.DSH_PERF_RESOLVE_CACHE !== '0',
|
package/platform.js
CHANGED
|
@@ -14,6 +14,7 @@ const ROOT = dirname(fileURLToPath(import.meta.url))
|
|
|
14
14
|
|
|
15
15
|
export const IS_WINDOWS = process.platform === 'win32'
|
|
16
16
|
export const IS_MAC = process.platform === 'darwin'
|
|
17
|
+
export const IS_LINUX = process.platform === 'linux'
|
|
17
18
|
|
|
18
19
|
/** 管理服务是不是跑在 Electron 主进程里(Windows/macOS 外壳都是,见 shell/main.mjs)。 */
|
|
19
20
|
export const IN_ELECTRON = Boolean(process.versions.electron)
|
|
@@ -65,5 +66,5 @@ export const APP_DIR_NAME = 'DSH'
|
|
|
65
66
|
/** macOS 包的名字和 bundle id:Info.plist、LaunchAgent、自更新都认它们。 */
|
|
66
67
|
export const MAC_APP_NAME = 'DSH-X.app'
|
|
67
68
|
export const MAC_BUNDLE_ID = 'io.github.yyh-001.dsh-x'
|
|
68
|
-
/**
|
|
69
|
+
/** 外壳可执行文件名(两个平台都来自 electron-builder 的 executableName,Windows 上就是 NSIS 的主程序名)。 */
|
|
69
70
|
export const LAUNCHER_NAME = 'DSH'
|