Node.js 与 InterPSS Java Library 集成方案
August 28, 2026 · View on GitHub
Goals
- Replace today’s Host shell-out to
IpssCmd(java -cp … org.interpss.agent.IpssCmd aclf …) with an in-process bridge (java-bridge) so Node can call Java without spawning a new JVM per run. - Keep the loaded
AclfNetworklong-lived for the Host process lifetime, held in aSimuModelRepository(base-case cache). Clearing the reference allows GC; otherwise the net stays in memory for reuse (reload-free second run, in-memory summarize).
Current state (as-is)
| Layer | Behavior |
|---|---|
Host (interpss-dynamic/host-body.js) | runAclf builds classpath (target/classes + lib/ipss_runnable.jar + lib/deps/*) and shells IpssCmd with ~180s timeout |
IpssCmd | One-shot main(): load → ACLF → write CSV / network_info → exit |
NetworkLoader | Supports ieee and psse (not IEEE-only) |
SimuModelRepository (agent) | Base-case get/set only; not wired into runners yet |
IpssCmd as a CLI entrypoint is not the in-process API. Host needs a library facade, not main().
Approach: In-Process Bridge (java-bridge)
Embed a JVM inside the Node (Cordis Host) process. Node calls Java like local functions; best latency, shared heap for the cached network.
Trade-off: a JVM or native crash can take down the whole Host process.
Library basics
- Start JVM —
ensureJvm(e.g.-Xmx4gfor large RAW cases). In packaged Electron hosts, setisPackagedElectron: trueand unpackjava-bridgenative binaries from asar. - Classpath —
appendClasspathwith the same layout Host uses today, or the Uber JAR fromIpssCmd.md(target/ipss-agent-cmd-*-uber.jar). Prefer one documented layout; expandlib/deps/*.jarexplicitly if wildcards are unsupported. - Call style — every Java method gets Sync (
fooSync) and async (foo). Host must use async for load/run so the Cordis event loop is not blocked. - Exceptions — Java exceptions surface as JS
Errorwithcause. - Runtime — JRE/JDK required; on Windows also VC++ Redistributable 2015+.
Design principles
- Java owns the network — Node never traverses EMF/
AclfNetwork; only strings, JSON, and filesystem paths cross the bridge. - One facade —
org.interpss.agent.bridge.IpssAgentBridge(name TBD). Host does not import adapters/runners piecemeal. - Host response compatibility — keep existing
runAclfreturn shape (ok,networkInfo,files,resultDir, …) so Client UI need not change in phase 1. - Filesystem conventions stay —
wspacepaths,aclf_run.jsonresolution, writingresult/*_DF_*.csvremain Java-side (or Host-resolved absolute paths passed in). In-process does not remove those conventions. - Phase 1 complete for reports —
runReportuses in-processIpssAgentBridge.runReport()(Java Markdown generators inorg.interpss.agent.report).
Java facade (minimal API)
// Phase 1: one repo per JVM (Host process).
// Phase 2: Map<sessionId, SimuModelRepository> if multi-session Host.
public final class IpssAgentBridge {
private final SimuModelRepository repo = new SimuModelRepository();
/** Load ieee|psse into base-case cache. Does not run LF. */
public String loadCase(String format, String absoluteCasePath);
/**
* Run ACLF on cached base (or load+run if path given).
* Writes CSV under resultsDir. Uses absoluteConfigPath like ProjectPaths.
*/
public String runAclf(String format, String absoluteCasePath,
String absoluteConfigPath,
String absoluteResultsDir, String stem);
/** In-memory summary from cached net (MCP-style). No file I/O required. */
public String summarize(String scope, String sortRule, int numRec);
/** Generate NERC or ACLF Markdown report from CSV outputs under wspace. */
public String runReport(String reportType, String displayName,
String absoluteProjectRoot, String resultDirRelative, String csvPrefix);
/** IpssNetworkInfo.format on cached base. */
public String getNetworkInfo();
public void clear();
}
All return values are JSON strings (or plain text for networkInfo) so java-bridge stays simple.
| Method | Success payload (sketch) |
|---|---|
loadCase | { ok, format, input, busCount, branchCount } |
runAclf | { ok, converged, networkInfo, files: [...], resultDir } |
summarize | { ok, scope, text } (or structured rows) |
Internals reuse: NetworkLoader, AclfRunConfigRec / config apply, existing AclfRunner logic (factor “run on net + write CSV” away from CLI-only entry), IpssNetworkInfo. Summarize can follow MCP’s AclfResultAdapter / scope enums (Net, Bus, Gen, Load, Branch).
Repository: keep agent SimuModelRepository base-case API for phase 1; when adjust/compare is needed, align with MCP (createChangeCase + jsonCopy()).
Concurrency: one lock/queue around load/run/summarize on the same repo so concurrent Host RPCs cannot race.
Node bootstrap (once per Host process)
The bootstrap runs in the persistent plugin (a full-Node ESM host row), which
is the only half that can require('java-bridge') — the dynamic plugin's Host
sandbox disables require/process. The persistent plugin publishes the bridge
as the javaBridge Cordis service (ctx.provide); the dynamic plugin consumes it
via ctx.get('javaBridge').
One-time enablement (build the Uber JAR, re-pack the plugin with its java-bridge
dependency, install into the DSH profile, restart) is scripted:
scripts/setup-java-bridge.sh.
const { ensureJvm, appendClasspath, importClass } = require('java-bridge')
await ensureJvm({ opts: ['-Xmx4g'] /* , isPackagedElectron: true if needed */ })
appendClasspath([
root + '/target/ipss-agent-cmd-1.0.0-uber.jar',
])
const Bridge = await importClass('org.interpss.agent.bridge.IpssAgentBridge')
const bridge = await Bridge.newInstanceAsync() // or static getInstance()
Host RPC mapping
| Today / new | In-process behavior |
|---|---|
runAclf | Resolve abs paths → bridge.runAclf(...) async → same { ok, networkInfo, files, … } |
loadCase (new) | bridge.loadCase(format, absPath) — cache only |
summarizeResult (new) | bridge.summarize(scope, sort, n) — requires prior load/run |
readCsv / checkResult* | Unchanged (FS) |
runReport | bridge.runReport('nerc', displayName, projectRoot, resultDir, null) → { ok, markdown, resultDir, … } |
getAclfOptions / saveAclfOptions | Unchanged (JSON on disk; pass config path into runAclf) |
Suggested METHODS addition: 'loadCase', 'summarizeResult' (keep 'runAclf').
runAclf compat path:
- Resolve workspace root,
wspace, case abs path, config (same two-tier rules asProjectPaths.resolveAclfRunConfig), results dir + stem. - Call bridge async (never
*Syncfor LF). - Optionally
reuse: trueskips reload when bridge already holds that case path (storeloadedInputon the bridge).
Session / state
| Phase | Model |
|---|---|
| 1 | One SimuModelRepository per Host JVM; loadCase replaces base. Document “one active case”. Enough for typical single-workspace Cordis use. |
| 2 | Key byworkspaceRoot if multiple sessions share the Host; pass id into every bridge call. |
Acceptance criteria
- First
runAclfon ieee118 matches current CSV +network_infooutput. - Second run with reuse /
loadCasethenrunAclfdoes not re-parse the case file. summarizeResult({ scope: 'Bus', sort_rule: 'Lowest Bus Voltage', num_rec: 10 })works with no shell.- Existing Client UI still works with current
runAclfresponse fields.
Suggested implementation order
- Add
IpssAgentBridge+ wireSimuModelRepository; factorAclfRunnerfor in-process use (CLIIpssCmdcan keep calling the same core). - Host: JVM init once; switch
runAclfto bridge; keep return shape. - Add
loadCase/summarizeResultRPCs. - (Later) CA, change-case adjust/compare, multi-session map.
Explicitly not in phase 1
- Direct JS access to
IeeeFileAdapter/AclfNetwork - Sync ACLF on the Cordis event loop
- Contingency analysis via bridge
- Multi-session map
Reports (runReport) are implemented via IpssAgentBridge.runReport() calling org.interpss.agent.report.ReportRunner.