@shundoo-ai/dsh-cosmic 1.0.9 → 1.0.10

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.9",
3
+ "version": "1.0.10",
4
4
  "description": "苍穹助手:金蝶云苍穹ERP的开发与数据分析,涵盖表单元数据建模、插件开发与热部署、数据可视化报告。",
5
5
  "keywords": [
6
6
  "dsh",
@@ -90,7 +90,7 @@ description: >
90
90
  > **build + 热部署**:R0 → R1 → R2 → R3 → R4 → R5。
91
91
  > 每一步都给「期望」与「失败处置」;**不做即兴发挥**,缺规则就回写手册(见文末自检)。
92
92
 
93
- > 🔁 **执行方式**:可以逐步执行,也可以把本手册**物化**成工作区内的临时脚本一次跑完(实测:Windows 会话把它写成 `<cwd>/.dsh/cosmic/scripts/hotdeploy.ps1`,此后"构建+热部署"每次只花 1 次调用)。**若物化,遵守四条**:① 放在 `.dsh/` 下(git 忽略、不入库);② **本手册是唯一权威**——手册更新后重新生成,不得反向以脚本为准;③ 脚本只做编排(发现 → 构建 → 注入),**不得引入手册之外的行为**;④ **不得写死本机路径**(工作区 / Gradle 根 / profile 名 / JDK 安装路径),工作区与 Gradle 根按 R1.1 推导、profile 用 `$DSH_PROFILE_DIR`、JDK 按 R2.1 顺位选择 —— 实测有脚本写死 `$cwd` 与 `profiles\web`,换机器即失效。**生成后自查**:在脚本里 grep 一遍本机绝对路径与写死的 profile 名,应当**只出现推导逻辑、不出现具体值**。
93
+ > 🔁 **执行方式**:可以逐步执行,也可以把本手册**物化**成工作区内的临时脚本一次跑完(实测:Windows 会话把它写成 `<cwd>/.dsh/cosmic/scripts/hotdeploy.ps1`,此后"构建+热部署"每次只花 1 次调用)。**若物化,遵守四条**:① 放在 `.dsh/` 下(git 忽略、不入库);② **本手册是唯一权威**——手册更新后重新生成,不得反向以脚本为准;③ 脚本只做编排(发现 → 构建 → 注入),**不得引入手册之外的行为**(**允许**只读的发现/干跑模式 —— 不构建、不注入、不改环境,如 `-DryRun`);④ **不得写死本机路径**(工作区 / Gradle 根 / profile 名 / JDK 安装路径),工作区与 Gradle 根按 R1.1 推导、profile 用 `$DSH_PROFILE_DIR`、JDK 按 R2.1 顺位选择 —— 实测有脚本写死 `$cwd` 与 `profiles\web`,换机器即失效。**生成后自查**:在脚本里 grep 一遍本机绝对路径与写死的 profile 名,应当**只出现推导逻辑、不出现具体值**。
94
94
 
95
95
  > **平台约定:本手册三平台通用**(Windows / macOS / Linux 各步骤都可用)。平台差异见下表;
96
96
  > 失败分支表里**未标注平台的条目三平台通用**,标 `[Win]` 的只会发生在 Windows。
@@ -252,13 +252,15 @@ try { & $GRADLE ":<模块>:build" -x test --console=plain } finally { Pop-Locati
252
252
 
253
253
  | 现象 | 含义 | 处置 |
254
254
  |------|------|------|
255
- | `:X:compileJava UP-TO-DATE` + 秒级完成 | **javac 没跑**,产物是旧字节码 | 要验证"改的代码进了 jar":`clean`(或 `--no-build-cache`)重跑,并核对 `build/classes` 的 class 数量/时间戳 |
255
+ | `:X:compileJava UP-TO-DATE` + 秒级完成 | **javac 没跑**,产物是旧字节码 | 要验证"改的代码进了 jar":加 `--no-build-cache` 重跑,**以 `compileJava` 的任务结果为准**(⚠️ **别只看 class 时间戳** —— `FROM-CACHE` 解包也会把它刷成"刚刚",见下) |
256
256
  | `:X:compileJava FROM-CACHE`(`clean` 后常见) | 从构建缓存还原,同样没跑 javac | 加 `--no-build-cache` |
257
257
  | 有 javac 的 `deprecation` / `unchecked` 提示 | 真编译 | 正常 |
258
258
  | `BUILD SUCCESSFUL` 但 `build/classes` 为空 | 伪成功 | 同上,重跑 |
259
259
 
260
- > 真编译判据:**① 与 ③ 是硬判据** —— ① `compileJava` 后**没有** `UP-TO-DATE`/`FROM-CACHE`;③ `build/classes` 下 class 的 mtime 是**刚刚**。
261
- > **② 只是旁证**:出现 javac 的 `deprecation`/`unchecked` 提示可佐证真编译,但**该文件没触发这类警告时就不会出现 —— 不代表没编译**(实测:1 个 class 真编译,①③ 成立而 ② 为空)。**别把 ② 当必要条件**,否则会误判"没编译"。
260
+ > 真编译判据:**只有 ① 是判别器** —— ① `compileJava` 后**没有** `UP-TO-DATE`/`FROM-CACHE`。
261
+ > **② / ③ 都只是旁证**:
262
+ > - ② 出现 javac 的 `deprecation`/`unchecked` 提示可佐证真编译;但**该文件没触发这类警告时就不会出现 —— 不代表没编译**(实测:1 个 class 真编译,① 成立而 ② 为空)。
263
+ > - ⚠️ **③ 不能当判别器**:`UP-TO-DATE` 时它确实为 False,但 **`FROM-CACHE` 时 Gradle 会解包缓存产物、把每个 class 的 mtime 批量改成同一时刻** → ③ 也变 True(实测:`FROM-CACHE` 且 javac 未跑,100 个 class 的 mtime 同为 13:59:22)。③ 只在 ① 成立时作旁证。
262
264
  > ⚠️ 本模板 `gradle.properties` 带 `org.gradle.caching=true`,`clean` 后仍可能 `FROM-CACHE`(macOS 实测 **879ms**、Windows 约 **2 秒** 跑完)。**要真编译必须加 `--no-build-cache`**(必要时再加 `--rerun-tasks` 强制重跑整任务图 —— 注意它在大模块上会连 jar transform 一起重跑,见 R3.2)。
263
265
  > 🔬 **一个可复用的诊断手法**:想逼出"真实 javac"又不想全量重编,可**删掉某一个产出的 `.class`** 再构建 —— 如果它 **1~2 秒就从 `FROM-CACHE` 还原**,说明构建缓存(`org.gradle.caching=true`)在起作用、**javac 依然没跑**;此时要真编译必须 `--no-build-cache`。(实测:Windows 1.5s `FROM-CACHE`。)
264
266
 
@@ -288,6 +290,19 @@ try { & $GRADLE ":<模块>:build" -x test --console=plain } finally { Pop-Locati
288
290
  > 四点数据:间隔 **13.5h → 139s**(阻塞型)|**46min → 8.65s**|**8min → 8.75s**|**1–8min → 3–4s**。→ **相关因子是"距上次真编译的间隔",不是 daemon 寿命**。
289
291
  > 🔁 **跨日复现(唯一的非当日样本)**:`daemon-28568`(**9/23** 22:49)**首次构建 157s → 同一 daemon 第 2 次 8.7s**,与今日同一形态 ⇒ 该现象**不是当日偶发**。⚠️ 该次只留存 wall(构建期 daemon 日志 0 行、客户端输出未留存),**无法确认当次 javac 是否真跑**,故记"首次构建"而非"首次真编译"。
290
292
  > 成因**未定**:OS page cache 冷读(371 MB 快照被换出)与 Gradle 缓存状态需重新校验,**两者都能解释现有数据,没有可区分二者的证据** —— 不要写成已证结论。
293
+ > 🎯 **判决实验(待做,约 1 分钟;能把"未定"变"已证")** ——两个候选在机制上互斥,一次即可判定:
294
+ > **做法**:在**自然的"隔几小时"冷态**下、**跑构建之前**,先把快照**整目录**读一遍人为预热 page cache,再跑那次"首次真编译":
295
+ > ```powershell
296
+ > # ⚠️ 必须预热整个 javaCompile 快照目录,不能只预热 classAnalysis.bin ——
297
+ > # 该目录下还有 jarAnalysis.bin(41 MB,构建时同样被打开);只预热一个文件
298
+ > # 会让"仍慢"的结果无法定案(可能是剩余文件没预热,而非候选②)。
299
+ > Get-ChildItem "<Gradle根>\.gradle\user-home\caches\<ver>\javaCompile\*.bin" |
300
+ > ForEach-Object { Get-FileHash $_.FullName -Algorithm SHA1 | Out-Null }
301
+ > # 紧接着跑 hotdeploy / :<模块>:build,量 wall(用 daemon 日志的 started/finished 时间戳对)
302
+ > ```
303
+ > (为什么这样就够:R3.2 的判据已限定「`classAnalysis.bin` 已存在**且不再增长**」⇒ 快照不是在**建**、而是在被**读**,所以"先把这批文件读进内存"正好精确检验 page cache 假设。)
304
+ > **判读**:变快(~10s 量级)→ 成因 = **① page cache 冷读** ✅;仍 ~140s → 成因 = **② Gradle 缓存状态重新校验**(与磁盘读无关)✅。**两种结果都定案,不必跑两次。**
305
+ > ⚠️ 前提:必须是**自然冷态**(连续构建会把窗口消耗掉);做完后把结果与结论写回本条,并删除"未定"字样。
291
306
  > 内存已排除:实测该机 **65 GB 总量 / 40.6 GB 可用**(也印证删掉 `-Xmx` 那条建议是对的)。
292
307
  > 源文件可追溯:`<Gradle根>/.gradle/user-home/daemon/<ver>/daemon-<pid>.out.log`(如 `daemon-33008`(46min→8.65s)/ `daemon-1944`(8min→8.75s)/ `daemon-7524`(13.5h→139s)/ `daemon-28568`(9/23 跨日复现:首次构建 157s → 第 2 次 8.7s))。
293
308
 
@@ -30,7 +30,7 @@ CreatePipe error=5, 拒绝访问
30
30
 
31
31
  - 用户只改一行 Java 代码,`gradle :<模块>:build` 耗时 **32 秒**(预期 5~10 秒)
32
32
  - 无改动构建约 4 秒(UP-TO-DATE 正常),**一旦有改动就全量重编**(583 个类)
33
- - 判断标准:改一行后看 `build/classes/java/main` 下 class 文件时间戳,**全部更新 = 全量重编**
33
+ - 判断标准:改一行后看 `build/classes/java/main` 下 class 文件时间戳,**全部更新 = 全量重编**(⚠️ 此法有 `FROM-CACHE` 假阳性,见 §1.1 末尾)
34
34
 
35
35
  ### 1.1 快速识别"增量失效"
36
36
 
@@ -47,6 +47,17 @@ $new = $classes | Where-Object { $_.LastWriteTime -ge (Get-Date).AddMinutes(-2)
47
47
  > ⚠️ 注意:如果两次构建间隔很短(如先全量后增量),2 分钟窗口会混入前一次全量的产物,
48
48
  > 造成"还是全量"的误判。精确做法:以**最后一次构建结束后**为基准,取最新时间戳 ±30s 窗口,
49
49
  > 或用两次构建的时间点精确切分(见第 5 节示例)。
50
+ >
51
+ > ⚠️⚠️ **时间戳法还有一个天然假阳性:`FROM-CACHE`**(2026-09-24 实测)。Gradle 从**构建缓存**解包产物时,
52
+ > 会把**所有** class 的 mtime 批量刷成同一时刻(实测 100 个 class 同为 13:59:22)——于是上面脚本也会报
53
+ > `recompiled == total`,**而 javac 根本没跑**。**照本节的"结果解读"会直接得出"全量重编、增量失效"的假诊断。**
54
+ > **所以判据必须回到 Gradle 日志的任务结果**:
55
+ > - `FROM-CACHE` → 缓存还原(**不是**重编,也不是增量失效)
56
+ > - `Full recompilation is required because …` → 真·全量重编
57
+ > - `Incremental compilation of N classes completed` → 真·增量
58
+ >
59
+ > 一句话:**时间戳只能当旁证,"有没有真编译"的唯一判别器是 `compileJava` 的任务结果里没有 `UP-TO-DATE`/`FROM-CACHE`**。
60
+ > 交叉引用:§7 第 7 条、`cosmic-vibe-coding/SKILL.md` 的 R3.1。
50
61
 
51
62
  ## 2. 排查工具速查
52
63
 
@@ -398,7 +409,8 @@ $jar = "<模块>\build\libs\<模块>-1.0.0.jar"
398
409
  5. **`Get-NetTCPConnection` 在非管理员权限下会误报"无监听"**,用 `Invoke-WebRequest` 探测兜底
399
410
  6. **build 前确认 Daemon 活着**:`gradle --status` 应见 `IDLE` 条目;`--no-daemon` 会为每次构建重付 JVM 冷启动 + 配置(本项目实测 Δ≈10s;另一工作区全新 daemon 冷启动 3–5s —— **别引用"+30s"**,见 §4.7 注)
400
411
  7. **识别全量重编**:改一行后 class 文件时间戳全变 = 全量;只有个别变 = 增量正常;
401
- 多次构建间隔短时用「最新时间戳 ±30s」窗口精确切分,避免混入前一次全量产物
412
+ 多次构建间隔短时用「最新时间戳 ±30s」窗口精确切分,避免混入前一次全量产物。
413
+ ⚠️ **但别只靠时间戳下结论**(2026-09-24 实测):**`FROM-CACHE` 解包缓存产物时会把所有 class 的 mtime 批量刷成同一时刻**(实测 100 个 class 同为 13:59:22),看起来也像"全变",而 javac 根本没跑。要区分「全量重编」与「缓存还原」,**看 Gradle 日志**:`Full recompilation is required because …`(全量)vs `FROM-CACHE` / `Incremental compilation of N classes completed`(非全量)。同理,`compileJava` 是否为真编译,**唯一判别器是任务结果里没有 `UP-TO-DATE`/`FROM-CACHE`**,时间戳只能当旁证。
402
414
  8. **`dependencies --configuration compileClasspath` 对 fileTree 是惰性展开**,不一定显示 jar 明细,
403
415
  要确认真实 classpath 必须用自定义任务打印 `sourceSets.main.compileClasspath.files`
404
416
  9. **`jps` 看不到 cosmic 进程**(实测):本机 cosmic 由 **SYSTEM** 账户启动
@@ -33,7 +33,7 @@
33
33
  |------|------|------|
34
34
  | IDEA 项目目录 | = `<cwd>`(工作目录)| |
35
35
  | IDEA MCP | 调工具一律传 `projectPath=<cwd>` | 否则报 Unable to determine the target project |
36
- | build jar 热部署 | 走 cosmic-vibe-coding 技能 | |
36
+ | build jar 热部署 | 走 cosmic-vibe-coding 技能的**运行手册(R0–R5)**;**若工作区已存在 `.dsh/cosmic/scripts/hotdeploy.ps1`(手册的物化脚本),优先用它** | 物化脚本 = 1 次调用;从零推导手册 ≈ 15 次。脚本须遵守手册「执行方式」四条约束(放 `.dsh/` 下 / 手册为唯一权威、手册更新后重新生成 / 只做编排 / 不得写死本机路径),允许 `-DryRun` 这类**只读干跑** |
37
37
  | cosmic-vibe-coding | `{PRESET}/skills/cosmic-build/skills/cosmic-vibe-coding` | |
38
38
  | 苍穹服务地址、JDK | `dev.py read`(erp_url / java_home) | 见 cosmic-vibe-coding SKILL.md |
39
39
  | appconfig.json | `<cwd>/app/config/appconfig.json` | **应用级开发配置**(compile / GRADLE_ROOT / JAVA_HOME / autoregister / pluginProjects),非 `_runtime` 运行时临时数据;`appconfig_load.py` 回退已固定工作区路径(`KD_APPCONFIG` 环境变量仍可覆盖) |
@@ -41,7 +41,7 @@
41
41
  | GRADLE_USER_HOME | `<cwd>/.gradle/user-home` | gradle wrapper 缓存;dist 从系统 `~/.gradle/wrapper/dists` **复制**(勿用软链——wrapper 会删链重解压) |
42
42
  | gradlew / gradlew.bat | POSIX:项目根 `gradlew` 需 `chmod +x`;Windows:需 `gradle/wrapper/`(gradle-wrapper.properties + gradle-wrapper.jar)齐全,`gradlew.bat` 才可运行 | 模板生成时可能缺 x 位;缺 wrapper 元数据时从模板/系统 dist 补齐 |
43
43
  | 插件编译脚本 | `{VENV}/bin/python compile_plugin_source.py` | 脚本只注入 `JAVA_HOME`、**不注入 `GRADLE_USER_HOME`**,调用方必须显式 `export GRADLE_USER_HOME=<cwd>/.gradle/user-home`,否则 Gradle 默认写用户主目录(Windows:`%USERPROFILE%\.gradle`)触发权限拦截;cosmic_home、解释器约定见上 |
44
- | 构建权限边界 | 产 jar 走 `:<模块>:build`(写工作区);勿用 `buildJar`/`deployJar`(写 cosmic 安装目录 outputdir,工作区外) | Windows 另有 `CreatePipe` 限制,构建/热部署须放开权限:受限沙箱下**把「体检+构建+注入」合成尽量少的命令**(授权按命令计),**不要先单独跑一次「体检」**(白花一次授权);完整步骤见 cosmic-vibe-coding 技能的**运行手册(R0–R5)**;Attacher classpath 分隔符 Windows 用 `;` |
44
+ | 构建权限边界 | 产 jar 走 `:<模块>:build`(写工作区);勿用 `buildJar`/`deployJar`(写 cosmic 安装目录 outputdir,工作区外) | Windows 另有 `CreatePipe` 限制,构建/热部署须放开权限:受限沙箱下**把「体检+构建+注入」合成尽量少的命令**(授权按命令计),**不要先单独跑一次「体检」**(白花一次授权);⚠️ **受限策略下运行手册会止步于 R0.4**(端口反查 `CimException: 拒绝访问`)—— 先确认能否放开策略/授权,再开跑,别把它误判成"cosmic 没启动";完整步骤见 cosmic-vibe-coding 技能的**运行手册(R0–R5)**;Attacher classpath 分隔符 Windows 用 `;` |
45
45
 
46
46
  > 📌 配置按**工作区**隔离(appconfig / GRADLE_USER_HOME / ERP 凭证均在 `<cwd>` 下),多工作区可并行开展不同项目;bash 每次调用为独立 shell,命令中需显式携带工作区约定值(如 `export GRADLE_USER_HOME=<cwd>/.gradle/user-home`),勿依赖跨调用 export。沙箱写权限仅限工作区(preset 技能脚本 `~/.dsh/...`、`~/kd` 等外部路径修改需授权)。权限放开按平台:**macOS/Linux 无管道限制,构建/热部署无需放开**(缓存入工作区即可);**Windows 禁跨进程管道(`CreatePipe error=5`),构建/热部署每次须单命令级放开 `danger-full-access`**(或会话级放宽;`--no-daemon` 无效)。
47
47
  > 📌 **发邮件不需要提权**:`send_email.py` 用 `password = "授权码"` 时明文直读、**不起子进程**,纯网络调用,任何平台都无需放开权限。(只有 `auth_cmd` 那条路要起子进程捕获 stdout,Windows 受限令牌下才需提权 —— 见 `references/email-delivery.md`。)