@shundoo-ai/dsh-cosmic 1.0.2 → 1.0.4-alpha.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shundoo-ai/dsh-cosmic",
3
- "version": "1.0.2",
3
+ "version": "1.0.4-alpha.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  ".config",
@@ -10,7 +10,7 @@ description: >
10
10
  | 职责 | 内容 | 载体 |
11
11
  |------|------|------|
12
12
  | ① 开启热部署 | 开 `jar_deploy_enable` 开关、连 IDEA MCP、生成 `dev.json` | `scripts/dev.py` + `assets/dev.json` |
13
- | ② 热加载 | 不重启、不碰 mc,把 jar 注入运行中 cosmic | `assets/kd-reloadjar-agent.jar` |
13
+ | ② 热加载 | 不重启、不碰 mc,把 jar 注入运行中 cosmic | `assets/kd-reloadjar-agent.jar`(**需先复制到机器级缓存再用**,见职责②§3) |
14
14
  | ③ build + 热部署 | 构建 jar(MCP 定位编译错误)→ 热加载 | IDEA MCP / gradle + agent |
15
15
 
16
16
  ## 触发场景
@@ -42,6 +42,32 @@ description: >
42
42
 
43
43
  ---
44
44
 
45
+ ## Windows:一条命令走完全程(推荐)
46
+
47
+ > 下面各职责给的是 **macOS/Linux** 命令。Windows 上**手拼这套流程代价极高** —— 实测一个 Windows 会话为跑通「构建 + 热部署」发了 **63 条 pwsh**、**6 次提权**(探测 PID → 找 JDK → 找 tools.jar → attach,每步都要单独授权)。
48
+
49
+ 本 skill 随包提供封装脚本,**把全程收敛成一次调用 ⇒ 只需一次提权**:
50
+
51
+ ```powershell
52
+ # 体检(不部署)
53
+ pwsh -File {SKILL_DIR}\scripts\cosmic-hotdeploy.ps1
54
+ # 构建 + 热部署
55
+ pwsh -File {SKILL_DIR}\scripts\cosmic-hotdeploy.ps1 -Module <模块名>
56
+ # 干净重建 + 热部署
57
+ pwsh -File {SKILL_DIR}\scripts\cosmic-hotdeploy.ps1 -Module <模块名> -Clean
58
+ # 只热部署已有 jar
59
+ pwsh -File {SKILL_DIR}\scripts\cosmic-hotdeploy.ps1 -Jar <jar路径>
60
+ # 只构建、不部署
61
+ pwsh -File {SKILL_DIR}\scripts\cosmic-hotdeploy.ps1 -Module <模块名> -WhatIf
62
+ ```
63
+
64
+ 内部依次完成:定位工作区 → 解析 preset bundle → 读 `dev.json` → **按端口反查 cosmic PID**(不是 `jps`,见职责②)→ 挑含 `tools.jar` 的 JDK → 构建 → Attacher 热加载。
65
+
66
+ 参数:`-Module`|`-Jar`|`-Clean`|`-WhatIf`|`-LogFile`|`-Workspace`|`-PresetDir`|`-Port`。
67
+ 工作区默认从「脚本位置的上两级」或当前目录推断;布局不同就传 `-Workspace <项目根>`。
68
+
69
+ ---
70
+
45
71
  ## 职责①:开启热部署(每个环境一次性)
46
72
 
47
73
  1. **IDEA 启动 MCP(可选)**:JetBrains MCP,固定 URL `http://127.0.0.1:64342/stream`,用于**代码智能**(符号查找、调用分析、重构、即时查错)。⚠️ **非构建必需**——产 jar 走 gradle 命令行(见职责③),MCP 未配/未连不影响「build + 热部署」主线。
@@ -89,12 +115,36 @@ description: >
89
115
 
90
116
  - 通用:`jps -l | grep kd.bos.service.webserver.JettyServer`(取 `appName=cosmic` 的那个)
91
117
  - macOS 备选:`lsof -tiTCP:<port> -sTCP:LISTEN`
118
+ - **Windows 推荐(实测最可靠)**:按端口反查 + 命令行校验
119
+
120
+ ```powershell
121
+ $conn = Get-NetTCPConnection -LocalPort <port> -State Listen | Select-Object -First 1
122
+ (Get-CimInstance Win32_Process -Filter "ProcessId=$($conn.OwningProcess)").CommandLine # 应含 kd.bos.service.webserver.JettyServer
123
+ ```
124
+
125
+ > ⚠️ **Windows 上 `jps` 看不到 cosmic**(实测):cosmic 常由 **SYSTEM** 账户启动,而 `jps` 只列举**当前用户**的 JVM → 输出里完全没有它,**极易误判「cosmic 没启动」**。用上面的方式零替代。
126
+ > 另:发现 PID 后先扫命令行里有没有 `-XX:+DisableAttachMechanism` —— 带此参数的 JVM **无法 attach**(硬阻断)。
127
+ > 跨用户 attach 本身**可用、无需提权**(Windows JVM attach 命名管道对 Administrators 组放行)。
92
128
 
93
129
  ### 3. 热部署
94
130
 
131
+ > ⚠️ **别直接用 preset 里的 `{SKILL_DIR}/assets/kd-reloadjar-agent.jar`** —— 先把 agent jar 复制到**机器级缓存**再用(`{AGENT}`,见下)。原因:Attacher 会把这个 jar **载入运行中的 cosmic JVM**,而 JVM 的 classloader 会**长期持有**该文件句柄;preset 一旦升级,包管理器要**整目录替换**(每个文件都得能删/改名)→ 必然在这个 jar 上失败(Windows 报 `os error 32`/拒绝访问)。
132
+ >
133
+ > `{AGENT}` = `$DSH_HOME/cache/cosmic/agent/kd-reloadjar-agent-<sha1前8位>.jar`(**带内容哈希命名**,所以新版永远不会覆盖可能已被 JVM 载入的旧副本)。`init-python.{sh,ps1}` 会预先复制;`cosmic-hotdeploy.ps1` 也会自动解析并复制。手工操作时:
134
+
95
135
  ```bash
96
- <java_home>/bin/java -cp <java_home>/lib/tools.jar:{SKILL_DIR}/assets/kd-reloadjar-agent.jar kd.reloadjar.Attacher \
97
- <cosmicPID> {SKILL_DIR}/assets/kd-reloadjar-agent.jar \
136
+ # 先把 preset 里的原件复制到机器级缓存(哈希命名),再用缓存路径
137
+ AGENT_DIR="${DSH_HOME:-$HOME/.dsh}/cache/cosmic/agent"
138
+ SRC="{SKILL_DIR}/assets/kd-reloadjar-agent.jar"
139
+ HASH=$( (shasum -a 1 "$SRC" 2>/dev/null || sha1sum "$SRC") | cut -c1-8 )
140
+ AGENT="$AGENT_DIR/kd-reloadjar-agent-$HASH.jar"
141
+ mkdir -p "$AGENT_DIR" && [ -f "$AGENT" ] || cp "$SRC" "$AGENT"
142
+ ```
143
+
144
+ ```bash
145
+ # 用缓存副本调 Attacher(Windows 上把 : 换成 ;)
146
+ <java_home>/bin/java -cp <java_home>/lib/tools.jar:"$AGENT" kd.reloadjar.Attacher \
147
+ <cosmicPID> "$AGENT" \
98
148
  "<jar绝对路径1>,<jar绝对路径2>" \
99
149
  <account_id> <tenant_id> \
100
150
  <可选logFile绝对路径>
@@ -104,6 +154,7 @@ description: >
104
154
  - jar 路径可直接用构建产物 `build/libs/xxx.jar`,**无需拷到 lib/cus**。
105
155
  - 最后一个参数 `logFile` **可选**:填了则在开发机侧写成功/失败标记日志(`status=SUCCESS/FAILED`)。
106
156
  - classpath 分隔符:macOS/Linux 用 `:`,Windows 用 `;`。
157
+ - **Windows 直接用**「Windows:一条命令走完全程」里的 `cosmic-hotdeploy.ps1`(它已内置上述缓存逻辑)。
107
158
 
108
159
  ### 4. 验证
109
160
 
@@ -131,6 +182,13 @@ GRADLE_USER_HOME=<cwd>/.gradle/user-home "$GRADLE" :<模块>:build -x test
131
182
  - 产物在 `<模块>/build/libs/*.jar`(本环境 `code/yun/sd-khxty-yun/build/libs/sd-khxty-yun-1.0.0.jar`)。
132
183
  - ⚠️ 产 jar **只认 gradle 命令行**:IDEA MCP `build_project` 默认增量、源码未变会跳过(`isSuccess:true` 但 jar 不更新);勿用 `buildJar`/`deployJar`(那是冷启动拷贝到 lib/cus,热部署不需要)。
133
184
 
185
+ > **Windows 必读(实测踩坑,失败一次排查很久)**
186
+ > - **必须显式设 `JAVA_HOME` 指向 JDK(不是 JRE)**:Windows 注册表 `HKLM\SOFTWARE\JavaSoft\Java Development Kit` 的 `JavaHome` 会被 JRE 安装包改写成 `...\jre1.8.0_481`,Gradle 采信它 → `java.home` 指向 JRE → 报 `Could not find tools.jar`。显式 `$env:JAVA_HOME="C:\Program Files\Java\jdk1.8.0_XXX"`
187
+ > - **Gradle 版本必须按 `gradle/wrapper/gradle-wrapper.properties` 锁定**,不要「取 `~/.gradle/wrapper/dists` 下最新」—— 机器上可能同时装了要求 Java 22 的新版 Gradle,会直接把构建带偏。
188
+ > - ⚠️ **「伪成功构建」陷阱**:`compileJava` 处于 `UP-TO-DATE` 时 Gradle **根本不加载编译器**,缺 tools.jar 也不报错 → 看到 `BUILD SUCCESSFUL` + jar 正常产出,**但里面是旧字节码**。首次接入务必 `clean build` 验证一次真编译。
189
+ >
190
+ > 完整分析见 `cosmic-dev-best-practices/references/cosmic-hotdeploy-build-slow-windows.md` §4.3 / §4.7。Windows 上直接用上面「Windows:一条命令走完全程」的封装脚本即可,它已内置这些约束。
191
+
134
192
  ### 2. 定位错误 / 理解代码(IDEA MCP 代码智能,可选加分项)
135
193
 
136
194
  > 此节可选:MCP 未配/未连不影响「build + 热部署」主线(gradle stderr 本身也会报编译错误,含文件名+行号)。
@@ -181,4 +239,5 @@ agent(`kd.reloadjar.ReloadAgent`)在 cosmic 进程内自动执行这两步
181
239
  |------|------|------|
182
240
  | `assets/dev.json` | ① 开启热部署 | 空配置模板(`dev.py` 读它作字段基础,生成工作区 `.dsh/cosmic/config/dev.json`) |
183
241
  | `scripts/dev.py` | ① 开启热部署 | 环境配置脚本(`init`=读 `.kd/config.json` 覆盖 dev.json;`read`=只读;`__file__` 反推 skill 根) |
184
- | `assets/kd-reloadjar-agent.jar` | 热加载 | 热重载 agent(含 `kd.reloadjar.ReloadAgent` + `Attacher`) |
242
+ | `scripts/cosmic-hotdeploy.ps1` | ②③ 热加载 / 构建+热部署 | **Windows 一键封装**(可移植:工作区自动推断、无硬编码路径):体检 / `-Module` 构建+热部署 / `-Clean` / `-Jar` 直传 / `-WhatIf`。见「Windows:一条命令走完全程」 |
243
+ | `assets/kd-reloadjar-agent.jar` | ② 热加载 | 热重载 agent(含 `kd.reloadjar.ReloadAgent` + `Attacher`)。⚠️ **原件不要直接加载** —— 先复制到 `$DSH_HOME/cache/cosmic/agent/kd-reloadjar-agent-<sha1前8位>.jar` 再用(避免 preset 升级时被 JVM 锁住,见职责②§3) |
@@ -0,0 +1,257 @@
1
+ # cosmic-hotdeploy.ps1 -- Kingdee Cosmic build + hot-deploy (Windows, portable)
2
+ #
3
+ # PURPOSE
4
+ # Windows port of the cosmic-vibe-coding skill workflow (the skill docs give
5
+ # macOS/Linux commands). Discovers the running cosmic JVM, builds the module
6
+ # with the wrapper-pinned Gradle + a real JDK8, then hot-loads the jar via the
7
+ # Attacher agent -- no cosmic restart, no touching mc.
8
+ #
9
+ # USAGE
10
+ # pwsh -File cosmic-hotdeploy.ps1 # health check only
11
+ # pwsh -File cosmic-hotdeploy.ps1 -Module <name> # build + hot-deploy
12
+ # pwsh -File cosmic-hotdeploy.ps1 -Module <name> -Clean # clean build + hot-deploy
13
+ # pwsh -File cosmic-hotdeploy.ps1 -Jar a.jar,b.jar # hot-deploy existing jars
14
+ # pwsh -File cosmic-hotdeploy.ps1 -Module <name> -WhatIf # dry run (build only)
15
+ #
16
+ # PORTABILITY
17
+ # The workspace is inferred from this script's location (expects to live in
18
+ # <workspace>/runtime/); override with -Workspace. The skill bundle is located
19
+ # via $DSH_PROFILE_DIR or $DSH_HOME; override with -PresetDir. No other
20
+ # machine-specific paths are hard-coded -- the JDK is discovered from the
21
+ # running cosmic process, then from JAVA_HOME, then from common install roots.
22
+ #
23
+ # NOTE
24
+ # Windows classpath separator is ';' (not ':'). The Attacher must run on a JDK
25
+ # whose major version matches the target JVM (JDK8 + lib\tools.jar for cosmic).
26
+
27
+ [CmdletBinding()]
28
+ param(
29
+ [string[]]$Jar,
30
+ [string]$Module,
31
+ [switch]$Clean,
32
+ [switch]$WhatIf,
33
+ [string]$LogFile,
34
+ [string]$Workspace,
35
+ [string]$PresetDir,
36
+ [int]$Port
37
+ )
38
+
39
+ $ErrorActionPreference = 'Stop'
40
+
41
+ # ---- 0. Resolve workspace (no hard-coded user paths) ----
42
+ if (-not $Workspace) {
43
+ # Expect this script at <workspace>\runtime\cosmic-hotdeploy.ps1
44
+ if ($PSScriptRoot) {
45
+ $Workspace = Split-Path $PSScriptRoot -Parent
46
+ } else {
47
+ $Workspace = (Get-Location).Path
48
+ }
49
+ # If it was moved elsewhere, fall back to the cwd when that looks like a workspace.
50
+ if (-not (Test-Path (Join-Path $Workspace 'settings.gradle'))) {
51
+ if (Test-Path (Join-Path (Get-Location).Path 'settings.gradle')) {
52
+ $Workspace = (Get-Location).Path
53
+ }
54
+ }
55
+ }
56
+ if (-not (Test-Path (Join-Path $Workspace 'settings.gradle'))) {
57
+ throw "Workspace '$Workspace' has no settings.gradle. Pass -Workspace <project root>."
58
+ }
59
+ $cwd = (Resolve-Path $Workspace).Path
60
+
61
+ # ---- 0.1 Resolve the cosmic skill bundle ----
62
+ if (-not $PresetDir) {
63
+ $roots = @()
64
+ if ($env:DSH_PROFILE_DIR) { $roots += (Join-Path $env:DSH_PROFILE_DIR 'node_modules\@shundoo-ai\dsh-cosmic') }
65
+ if ($env:DSH_HOME) { $roots += (Join-Path $env:DSH_HOME '.agent-presets\cosmic') }
66
+ $PresetDir = $roots | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1
67
+ }
68
+ if (-not $PresetDir) {
69
+ throw "Cannot locate the cosmic skill bundle. Set DSH_PROFILE_DIR/DSH_HOME, or pass -PresetDir <path containing skills\cosmic-build>."
70
+ }
71
+ $srcAgent = Join-Path $PresetDir 'skills\cosmic-build\skills\cosmic-vibe-coding\assets\kd-reloadjar-agent.jar'
72
+ if (-not (Test-Path $srcAgent)) { throw "Missing reload agent: $srcAgent" }
73
+
74
+ # ---- 0.2 Agent jar: use a machine-level, content-hashed COPY -- never load it from the preset ----
75
+ # Why: the Attacher loads this jar into the running cosmic JVM, and the JVM's classloader keeps
76
+ # the JarFile handle open for the JVM's lifetime. npm/pnpm upgrades replace the *whole package
77
+ # directory* (move the old dir away, put the new one in), which requires every file inside to be
78
+ # deletable/renamable -> the upgrade always fails on this jar (Windows: os error 32 / access
79
+ # denied). The old git-based layout never hit this because git only rewrites files whose content
80
+ # actually changed, and this jar rarely changes.
81
+ # Naming the copy with the content hash means a *new* preset version never overwrites an old copy
82
+ # that may already be loaded by a JVM.
83
+ $dshHome = if ($env:DSH_HOME) { $env:DSH_HOME }
84
+ elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dsh' }
85
+ elseif ($env:HOME) { Join-Path $env:HOME '.dsh' }
86
+ else { $null }
87
+ $agent = $srcAgent
88
+ if ($dshHome) {
89
+ $agentHash = (Get-FileHash $srcAgent -Algorithm SHA1).Hash.Substring(0, 8).ToLower()
90
+ $agentDir = Join-Path (Join-Path $dshHome 'cache\cosmic') 'agent'
91
+ $cachedAgent = Join-Path $agentDir "kd-reloadjar-agent-$agentHash.jar"
92
+ try {
93
+ if (-not (Test-Path $cachedAgent)) {
94
+ New-Item -ItemType Directory -Force -Path $agentDir | Out-Null
95
+ Copy-Item $srcAgent $cachedAgent -Force
96
+ }
97
+ $agent = $cachedAgent
98
+ # best-effort housekeeping: keep only the newest few hashed copies
99
+ Get-ChildItem $agentDir -Filter 'kd-reloadjar-agent-*.jar' -ErrorAction SilentlyContinue |
100
+ Sort-Object LastWriteTime -Descending | Select-Object -Skip 5 |
101
+ Remove-Item -Force -ErrorAction SilentlyContinue
102
+ } catch {
103
+ Write-Host "[agent] WARN cannot use the cached copy ($($_.Exception.Message)); falling back to the preset copy (preset upgrades may then hit a file lock)."
104
+ }
105
+ }
106
+ $devScript = Join-Path $PresetDir 'skills\cosmic-build\skills\cosmic-vibe-coding\scripts\dev.py'
107
+
108
+ # ---- 1. Read dev.json (env config produced by dev.py init) ----
109
+ $devJson = Join-Path $cwd '.dsh\cosmic\config\dev.json'
110
+ if (-not (Test-Path $devJson)) {
111
+ throw "Missing $devJson -- run 'dev.py init' first (enable hot-deploy / set ERP login)."
112
+ }
113
+ $dev = Get-Content $devJson -Raw | ConvertFrom-Json
114
+
115
+ # Port: explicit -Port wins, then dev.json, then the port embedded in erp_url.
116
+ $effPort = if ($Port) { $Port }
117
+ elseif ($dev.port) { [int]$dev.port }
118
+ elseif ($dev.erp_url -match '^https?://[^:/]+:(\d+)') { [int]$Matches[1] }
119
+ else { 80 }
120
+ $accountId = $dev.account_id
121
+ $tenantId = $dev.tenant_id
122
+ if (-not $accountId) { throw "dev.json has no account_id -- re-run 'dev.py init'." }
123
+
124
+ # ---- 2. Discover the cosmic PID by port ownership, then validate the JVM ----
125
+ $conn = Get-NetTCPConnection -LocalPort $effPort -State Listen -ErrorAction SilentlyContinue |
126
+ Select-Object -First 1
127
+ if (-not $conn) { throw "Nothing is listening on port $effPort -- is cosmic running?" }
128
+ $targetPid = $conn.OwningProcess
129
+ $proc = Get-CimInstance Win32_Process -Filter "ProcessId=$targetPid"
130
+ if ($proc.CommandLine -notmatch 'kd\.bos\.service\.webserver\.JettyServer') {
131
+ throw "Process $targetPid on port $effPort is not the cosmic JettyServer: $($proc.CommandLine)"
132
+ }
133
+ if ($proc.CommandLine -match 'DisableAttachMechanism') {
134
+ throw "Process $targetPid has -XX:+DisableAttachMechanism -- hot-deploy cannot attach to it."
135
+ }
136
+ $owner = ($proc | Invoke-CimMethod -MethodName GetOwner).User
137
+
138
+ # ---- 3. Pick the JDK: prefer the one the cosmic process itself runs on ----
139
+ # Derive JAVA_HOME from the process executable (reliable, unlike parsing the
140
+ # command line, which may quote "C:\path with spaces"\bin\java differently).
141
+ $candidates = @()
142
+ if ($proc.ExecutablePath) {
143
+ $javaBin = Split-Path $proc.ExecutablePath -Parent # ...\jdk\bin
144
+ if ((Split-Path $javaBin -Leaf) -ieq 'bin') { $candidates += (Split-Path $javaBin -Parent) }
145
+ }
146
+ if ($env:JAVA_HOME) { $candidates += $env:JAVA_HOME }
147
+ # Common JDK install roots across drives (best effort, machine independent).
148
+ foreach ($root in @("$env:ProgramFiles\Java", "${env:ProgramFiles(x86)}\Java", 'C:\Java',
149
+ 'D:\Java', 'D:\Program Files\Java', "$env:USERPROFILE\.jdks")) {
150
+ if ($root -and (Test-Path $root)) {
151
+ $candidates += (Get-ChildItem $root -Directory -ErrorAction SilentlyContinue |
152
+ Where-Object { $_.Name -match '^jdk' } | ForEach-Object { $_.FullName })
153
+ }
154
+ }
155
+ $candidates = $candidates | Where-Object { $_ } | Select-Object -Unique
156
+
157
+ # A usable JDK for the Attacher has lib\tools.jar (JDK8) and bin\java.exe.
158
+ $attacherJdk = $candidates |
159
+ Where-Object { (Test-Path (Join-Path $_ 'lib\tools.jar')) -and (Test-Path (Join-Path $_ 'bin\java.exe')) } |
160
+ Select-Object -First 1
161
+ if (-not $attacherJdk) {
162
+ throw @"
163
+ No usable JDK for the Attacher (needs lib\tools.jar + bin\java.exe).
164
+ Candidates tried: $($candidates -join ' | ')
165
+ The target cosmic JVM needs a matching JDK whose major version matches it.
166
+ If cosmic runs on JDK8, install a JDK8 (not a JRE!) and pass -Workspace/-PresetDir as needed,
167
+ or set `$env:JAVA_HOME to a real JDK.
168
+ "@
169
+ }
170
+ $java = Join-Path $attacherJdk 'bin\java.exe'
171
+ $toolsJar = Join-Path $attacherJdk 'lib\tools.jar'
172
+
173
+ Write-Host "[env] workspace = $cwd"
174
+ Write-Host "[env] erp_url = $($dev.erp_url) (port $effPort)"
175
+ Write-Host "[env] cosmic PID = $targetPid (owner=$owner)"
176
+ Write-Host "[env] attacher JDK = $attacherJdk"
177
+ Write-Host "[env] accountId = $accountId / tenantId = $tenantId"
178
+ if ($agent -ne $srcAgent) {
179
+ Write-Host "[agent] $agent (cached copy; the preset original is never loaded)"
180
+ } else {
181
+ Write-Host "[agent] $agent (PRESET ORIGINAL -- preset upgrades may hit a file lock while cosmic runs)"
182
+ }
183
+
184
+ # ---- 4. Optional: build first ----
185
+ if ($Module) {
186
+ $gradleHome = Join-Path $cwd '.gradle\user-home'
187
+
188
+ # 4.1 Gradle version must follow the wrapper, never "newest dist" -- a newer
189
+ # dist (e.g. 8.x) may demand a newer Java runtime and derail this JDK8 chain.
190
+ $wrapperProps = Join-Path $cwd 'gradle\wrapper\gradle-wrapper.properties'
191
+ if (-not (Test-Path $wrapperProps)) { throw "Missing $wrapperProps." }
192
+ $m = [regex]::Match((Get-Content $wrapperProps -Raw), 'gradle-([0-9.]+)-bin')
193
+ if (-not $m.Success) { throw "Cannot parse the Gradle version from $wrapperProps." }
194
+ $distVer = $m.Groups[1].Value
195
+ $distRoot = Join-Path $env:USERPROFILE ".gradle\wrapper\dists\gradle-$distVer-bin"
196
+ $gradleBat = Get-ChildItem $distRoot -Recurse -Filter 'gradle.bat' -ErrorAction SilentlyContinue |
197
+ Select-Object -First 1
198
+ if (-not $gradleBat) {
199
+ throw "Gradle $distVer not found under $distRoot. Let the wrapper download it first (run .\gradlew.bat)."
200
+ }
201
+
202
+ # 4.2 Build JDK: same preference order as above, but must carry tools.jar.
203
+ $buildJdk = $attacherJdk
204
+ if ($WhatIf) { Write-Host '[whatif] dry run: skipping the actual build.' }
205
+
206
+ $gradleArgs = @()
207
+ if ($Clean) { $gradleArgs += (':' + $Module + ':clean') }
208
+ $gradleArgs += (':' + $Module + ':build')
209
+ $gradleArgs += @('-x', 'test', '--console=plain')
210
+ Write-Host "[build] gradle $distVer | $($gradleArgs -join ' ')"
211
+ Write-Host "[build] JAVA_HOME=$buildJdk (GRADLE_USER_HOME=$gradleHome)"
212
+ $env:GRADLE_USER_HOME = $gradleHome
213
+ $env:JAVA_HOME = $buildJdk
214
+ & $gradleBat.FullName @gradleArgs
215
+ if ($LASTEXITCODE -ne 0) { throw "Gradle build failed (exit $LASTEXITCODE) -- see the compile errors above." }
216
+ }
217
+
218
+ # ---- 5. Resolve jars (auto-locate the build output when -Module was given) ----
219
+ if ((-not $Jar -or $Jar.Count -eq 0) -and $Module) {
220
+ $settings = Get-Content (Join-Path $cwd 'settings.gradle') -Raw
221
+ $mm = [regex]::Match($settings,
222
+ "project\(':$([regex]::Escape($Module))'\)\.projectDir\s*=\s*new File\('([^']+)'\)")
223
+ $moduleDir = if ($mm.Success) { Join-Path $cwd ($mm.Groups[1].Value -replace '/', '\') }
224
+ else { Join-Path $cwd "code\$Module" }
225
+ $libsDir = Join-Path $moduleDir 'build\libs'
226
+ $Jar = @(Get-ChildItem $libsDir -Filter '*.jar' -ErrorAction SilentlyContinue |
227
+ Where-Object { $_.Name -notlike '*sources*' -and $_.Name -notlike '*javadoc*' } |
228
+ Sort-Object LastWriteTime -Descending | Select-Object -First 1 |
229
+ ForEach-Object { $_.FullName })
230
+ if (-not $Jar) { throw "No build output jar in $libsDir." }
231
+ }
232
+ if (-not $Jar -or $Jar.Count -eq 0) {
233
+ Write-Host '[done] Health check only -- no hot-deploy (pass -Jar or -Module).'
234
+ exit 0
235
+ }
236
+ $jars = $Jar | ForEach-Object { (Resolve-Path $_).Path }
237
+ foreach ($j in $jars) {
238
+ Write-Host "[jar] $j ($([math]::Round((Get-Item $j).Length / 1KB)) KB)"
239
+ }
240
+
241
+ # ---- 6. Attach: save the jar bytes, then reload the ExtClassLoader ----
242
+ $attArgs = @(
243
+ '-cp', "$toolsJar;$agent",
244
+ 'kd.reloadjar.Attacher',
245
+ $targetPid, $agent,
246
+ ($jars -join ','),
247
+ $accountId, $tenantId
248
+ )
249
+ if ($LogFile) {
250
+ $dir = Split-Path $LogFile -Parent
251
+ if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
252
+ $attArgs += (Join-Path (Resolve-Path $dir).Path (Split-Path $LogFile -Leaf))
253
+ }
254
+
255
+ & $java @attArgs
256
+ if ($LASTEXITCODE -ne 0) { throw "Hot-deploy failed (exit $LASTEXITCODE)." }
257
+ Write-Host '[done] Hot-deploy complete (SUCCESS = save + reset both applied).'
@@ -139,13 +139,25 @@ api fileTree(trd) {
139
139
  `<cwd>/.dsh/cosmic/config/dev.json`:
140
140
 
141
141
  ```json
142
- { "java_home": "C:\\Program Files\\java\\jdk" }
142
+ { "java_home": "C:\\Program Files\\Java\\jdk1.8.0_281" }
143
143
  ```
144
144
 
145
- - 以 IDEA 项目 SDK 为准(本项目 = `C:\Program Files\java\jdk`,JDK 8,含 tools.jar)
146
- - Attacher 需与 cosmic 进程同 JDK **大版本**(JDK 8,具体小版本无关)
145
+ - 以 IDEA 项目 SDK 为准(实测本机 = `C:\Program Files\Java\jdk1.8.0_281`,JDK 8,含 tools.jar)
146
+ - Attacher 需与 cosmic 进程同 JDK **大版本**(JDK 8,具体小版本无关;cosmic 自带
147
+ `F:\cosmic\xinghan8\jdk` = 1.8.0_212,与本机 1.8.0_281 混用无碍)
147
148
  - `dev.py init` 自动发现不到时手动补;改后 `dev.py read` 验证
148
149
 
150
+ > ⚠️ **必设 `JAVA_HOME`,否则 `compileJava` 必失败**(实测踩坑):本机 Windows 注册表
151
+ > `HKLM\SOFTWARE\JavaSoft\Java Development Kit` 的 `JavaHome` 被 8u481 安装包改写成
152
+ > **JRE** 路径 `C:\Program Files\Java\jre1.8.0_481`。Gradle 在该值存在时优先采信它,
153
+ > 于是 `java.home` 指向 JRE → 没有 `tools.jar` → 报
154
+ > `Could not find tools.jar. Please check that ...\jre1.8.0_481 contains a valid JDK installation`。
155
+ > 显式 `$env:JAVA_HOME="C:\Program Files\Java\jdk1.8.0_281"` 即解(实测全量编译 10s 通过)。
156
+ >
157
+ > 🔎 **同源陷阱:「伪成功构建」**。若 `compileJava` 处于 `UP-TO-DATE`,Gradle 根本不加载
158
+ > 编译器,缺 tools.jar 也不报错 —— 你会看到 `BUILD SUCCESSFUL` + `jar` 正常产出,
159
+ > **但 jar 里是旧字节码**。首次接入务必用 `clean build` 验证一次真编译(本次就是这么暴露的)。
160
+
149
161
  ### 4.4 验证 classpath 是否真的干净(临时 init script)
150
162
 
151
163
  `showcp.gradle`(用完可删):
@@ -205,11 +217,19 @@ Get-ChildItem "<lib>" -Recurse -Filter *.jar | ForEach-Object {
205
217
  > 见 §0),须先单命令级放开 `danger-full-access`;`--no-daemon` 也救不了。
206
218
 
207
219
  ```powershell
208
- $env:JAVA_HOME = "C:\Program Files\java\jdk"
220
+ $env:JAVA_HOME = "C:\Program Files\Java\jdk1.8.0_281" # 必须:注册表 java.home 指向 JRE(见 §4.3)
209
221
  $gradle = "C:\Users\<user>\.gradle\wrapper\dists\gradle-7.6.3-bin\<hash>\gradle-7.6.3\bin\gradle.bat"
210
222
  & $gradle ":<模块>:build" -x test --console=plain
211
223
  ```
212
224
 
225
+ > ⚠️ **Gradle 版本必须按 `gradle/wrapper/gradle-wrapper.properties` 锁定**,不要「取
226
+ > `~/.gradle/wrapper/dists` 下最新」。本机同时存在 `gradle-8.8-bin`,它要求 Java 22
227
+ > 运行时,与本项目 JDK 8 + tools.jar 链路不兼容,会直接把构建带偏。
228
+ >
229
+ > 🧩 本工作区已把整套流程封装为脚本:`runtime/cosmic-hotdeploy.ps1`
230
+ > (体检 / `-Jar` 热加载 / `-Module` 构建+热加载 / `-Clean`),内置上述 JDK、Gradle
231
+ > 版本、PID 发现、account/tenant 读取逻辑,可直接用。
232
+
213
233
  > ❌ 不要加 `--no-daemon`:每次 fork 单次 Daemon(JVM 冷启动 + 重新加载配置 ≈ +30s)
214
234
  > ✅ 复用常驻 Daemon(`org.gradle.daemon=true` 默认开启),改几行代码约 5-10 秒
215
235
 
@@ -314,6 +334,11 @@ for incremental compilation. See the debug log for more details.
314
334
  ### 6.1 发现 cosmic PID
315
335
 
316
336
  ```powershell
337
+ # ⭐ 方式零(推荐):按端口归属进程反查 + 命令行校验(实测最可靠)
338
+ $conn = Get-NetTCPConnection -LocalPort <cosmic_port> -State Listen | Select-Object -First 1
339
+ $cosmicPID = $conn.OwningProcess
340
+ (Get-CimInstance Win32_Process -Filter "ProcessId=$cosmicPID").CommandLine # 应含 kd.bos.service.webserver.JettyServer
341
+
317
342
  # 方式一:HTTP 探测端口(最可靠,权限问题兜底)
318
343
  Invoke-WebRequest http://127.0.0.1:<cosmic_port>/ierp # HTTP 200 = cosmic 活着
319
344
 
@@ -355,6 +380,15 @@ $jar = "<模块>\build\libs\<模块>-1.0.0.jar"
355
380
  多次构建间隔短时用「最新时间戳 ±30s」窗口精确切分,避免混入前一次全量产物
356
381
  8. **`dependencies --configuration compileClasspath` 对 fileTree 是惰性展开**,不一定显示 jar 明细,
357
382
  要确认真实 classpath 必须用自定义任务打印 `sourceSets.main.compileClasspath.files`
383
+ 9. **`jps` 看不到 cosmic 进程**(实测):本机 cosmic 由 **SYSTEM** 账户启动
384
+ (`F:\cosmic\xinghan8\jdk\bin\java ... -DappName=cosmic ... kd.bos.service.webserver.JettyServer`),
385
+ 而 `jps` 只列举当前用户的 JVM → 输出里完全没有它,容易误判「cosmic 没启动」。
386
+ 用 §6.1 方式零(按端口反查 OwningProcess)替代。
387
+ 10. **跨用户 attach 是可用的,无需提权**:cosmic 跑在 SYSTEM 下、Attacher 由
388
+ `lihaisheng\administrator` 启动,实测 attach 成功(Windows JVM attach 命名管道对
389
+ Administrators 组放行),不必 `Start-Process -Verb RunAs`。
390
+ 11. **`-XX:+DisableAttachMechanism` 是硬阻断**:本机 IDEA 侧另一个 JVM(`CosmicStudio-runnable.jar`)
391
+ 带此参数,对它 attach 必然失败;发现 PID 后先扫命令行里有没有这个 flag。
358
392
  9. **`java_home` 以 IDEA 项目 SDK 为准**(Windows 常见 `C:\Program Files\java\jdk`),
359
393
  不要误填 cosmic 自带 JDK 目录;Attacher 只需与 cosmic 同 JDK 大版本(同为 JDK8 即兼容)
360
394
  10. **gradle 构建命令不要加 `--no-daemon`**;多 IDLE Daemon 并存无害,`--stop` 可一次性清理
@@ -47,3 +47,25 @@ Write-Host "==> 验证导入"
47
47
  uv run --python $VENV python -c "import pandas, numpy, plotly, jinja2, requests; print('OK', pandas.__version__, numpy.__version__, plotly.__version__, jinja2.__version__, 'requests', requests.__version__)"
48
48
 
49
49
  Write-Host "==> init-python 完成"
50
+
51
+ # 顺带把热部署用的 agent jar 预复制到机器级缓存(与 venv 同理:**不要把会被外部进程独占
52
+ # 加载的文件留在 preset 里**)。Attacher 会把这个 jar 载入运行中的 cosmic JVM,JVM 长期持有
53
+ # 其句柄;preset 升级要整目录替换 → 会在这个 jar 上失败(Windows os error 32 / 拒绝访问)。
54
+ # 文件名带内容哈希:新版不会覆盖可能已被 JVM 载入的旧副本。幂等。
55
+ $AgentSrc = Join-Path $ROOT "skills\cosmic-build\skills\cosmic-vibe-coding\assets\kd-reloadjar-agent.jar"
56
+ if (Test-Path $AgentSrc) {
57
+ $AgentDir = Join-Path $DshHome "cache\cosmic\agent"
58
+ $AgentHash = (Get-FileHash $AgentSrc -Algorithm SHA1).Hash.Substring(0, 8).ToLower()
59
+ $AgentDst = Join-Path $AgentDir "kd-reloadjar-agent-$AgentHash.jar"
60
+ if (-not (Test-Path $AgentDir)) { New-Item -ItemType Directory -Force -Path $AgentDir | Out-Null }
61
+ if (Test-Path $AgentDst) {
62
+ Write-Host "==> agent 缓存已存在: $AgentDst"
63
+ } else {
64
+ Copy-Item $AgentSrc $AgentDst -Force
65
+ Write-Host "==> agent 已缓存: $AgentDst"
66
+ }
67
+ # 只保留最近 5 份哈希副本(best-effort)
68
+ Get-ChildItem $AgentDir -Filter "kd-reloadjar-agent-*.jar" -ErrorAction SilentlyContinue |
69
+ Sort-Object LastWriteTime -Descending | Select-Object -Skip 5 |
70
+ Remove-Item -Force -ErrorAction SilentlyContinue
71
+ }
@@ -48,3 +48,22 @@ echo "==> 验证导入"
48
48
  uv run --python "$VENV" python -c 'import pandas, numpy, plotly, jinja2, requests; print("OK", pandas.__version__, numpy.__version__, plotly.__version__, jinja2.__version__, "requests", requests.__version__)'
49
49
 
50
50
  echo "==> init-python 完成"
51
+
52
+ # 4) 顺带把热部署用的 agent jar 预复制到机器级缓存(与 venv 同理:**不要把会被外部进程
53
+ # 独占加载的文件留在 preset 里**)。Attacher 会把这个 jar 载入运行中的 cosmic JVM,
54
+ # JVM 长期持有其句柄;preset 升级要整目录替换 → 会在这个 jar 上失败(Windows os error 32)。
55
+ # 文件名带内容哈希:新版不会覆盖可能已被 JVM 载入的旧副本。幂等。
56
+ AGENT_SRC="$ROOT/skills/cosmic-build/skills/cosmic-vibe-coding/assets/kd-reloadjar-agent.jar"
57
+ if [ -f "$AGENT_SRC" ]; then
58
+ AGENT_DIR="$DSH_HOME/cache/cosmic/agent"
59
+ AGENT_HASH="$( (shasum -a 1 "$AGENT_SRC" 2>/dev/null || sha1sum "$AGENT_SRC") | cut -c1-8 )"
60
+ AGENT_DST="$AGENT_DIR/kd-reloadjar-agent-$AGENT_HASH.jar"
61
+ mkdir -p "$AGENT_DIR"
62
+ if [ -f "$AGENT_DST" ]; then
63
+ echo "==> agent 缓存已存在: $AGENT_DST"
64
+ else
65
+ cp "$AGENT_SRC" "$AGENT_DST" && echo "==> agent 已缓存: $AGENT_DST"
66
+ fi
67
+ # 只保留最近 5 份哈希副本(best-effort)
68
+ ls -1t "$AGENT_DIR"/kd-reloadjar-agent-*.jar 2>/dev/null | tail -n +6 | while read -r old; do rm -f "$old" 2>/dev/null || true; done
69
+ fi