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.
Files changed (50) hide show
  1. package/assets/dsh-preview.png +0 -0
  2. package/assets/dsh.ico +0 -0
  3. package/assets/icon.ico +0 -0
  4. package/assets/tray.ico +0 -0
  5. package/assets/tray.png +0 -0
  6. package/core-npm.js +201 -183
  7. package/desktop-plugins.js +86 -0
  8. package/desktop.js +200 -0
  9. package/filemanager-base.js +7 -8
  10. package/filemanager-linux.js +124 -0
  11. package/filemanager.js +4 -7
  12. package/native-notify.js +74 -0
  13. package/node/node +2 -1
  14. package/package.json +1 -1
  15. package/packs/dsh-x-workbench/README.md +61 -0
  16. package/packs/dsh-x-workbench/dspack.json +4 -0
  17. package/packs/dsh-x-workbench/home/skills/agent-browser/LICENSE-APACHE-2.0.txt +202 -0
  18. package/packs/dsh-x-workbench/home/skills/agent-browser/NOTICE.md +6 -0
  19. package/packs/dsh-x-workbench/home/skills/agent-browser/SKILL.md +697 -0
  20. package/packs/dsh-x-workbench/home/skills/agent-browser/references/authentication.md +313 -0
  21. package/packs/dsh-x-workbench/home/skills/agent-browser/references/commands.md +300 -0
  22. package/packs/dsh-x-workbench/home/skills/agent-browser/references/profiling.md +120 -0
  23. package/packs/dsh-x-workbench/home/skills/agent-browser/references/proxy-support.md +194 -0
  24. package/packs/dsh-x-workbench/home/skills/agent-browser/references/session-management.md +201 -0
  25. package/packs/dsh-x-workbench/home/skills/agent-browser/references/snapshot-refs.md +229 -0
  26. package/packs/dsh-x-workbench/home/skills/agent-browser/references/video-recording.md +173 -0
  27. package/packs/dsh-x-workbench/home/skills/agent-browser/templates/authenticated-session.sh +105 -0
  28. package/packs/dsh-x-workbench/home/skills/agent-browser/templates/capture-workflow.sh +69 -0
  29. package/packs/dsh-x-workbench/home/skills/agent-browser/templates/form-automation.sh +62 -0
  30. package/packs/dsh-x-workbench/home/skills/electron/LICENSE-APACHE-2.0.txt +202 -0
  31. package/packs/dsh-x-workbench/home/skills/electron/NOTICE.md +6 -0
  32. package/packs/dsh-x-workbench/home/skills/electron/SKILL.md +243 -0
  33. package/packs/dsh-x-workbench/manifest.json +35 -0
  34. package/perf/patch-hooks.mjs +14 -1
  35. package/perf/plugin-profile.mjs +204 -0
  36. package/perf/register.mjs +4 -0
  37. package/platform.js +2 -1
  38. package/public/booting.html +475 -0
  39. package/public/favicon.ico +0 -0
  40. package/public/favicon.png +0 -0
  41. package/public/icon.png +0 -0
  42. package/public/index.html +9277 -8173
  43. package/public/internal-tabs.js +45 -41
  44. package/public/launcher.css +285 -98
  45. package/public/mascot.js +1 -1
  46. package/public/theme.css +33 -6
  47. package/public/water.js +1 -1
  48. package/registry.js +38 -3
  49. package/server.js +1060 -119
  50. 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
- /** 外壳可执行文件名(Windows:Cargo.toml 的 [[bin]] name;macOS:Electron 包的 executableName)。 */
69
+ /** 外壳可执行文件名(两个平台都来自 electron-builder 的 executableName,Windows 上就是 NSIS 的主程序名)。 */
69
70
  export const LAUNCHER_NAME = 'DSH'