dsh-ris-yds 0.0.0-stage → 1.0.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/lib/yms.js ADDED
@@ -0,0 +1,302 @@
1
+ // YMS runtime: build the launch (env + JVM flags + rdbga), start in foreground
2
+ // (for the dsh-ris-ide run-with-log wrapper) or detached (own pidfile + logs),
3
+ // stop, status. Ports the proven work/start-yms.sh logic into cross-OS node.
4
+
5
+ import { spawn } from 'node:child_process'
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, openSync, closeSync, statSync, renameSync, readSync } from 'node:fs'
7
+ import { join } from 'node:path'
8
+ import http from 'node:http'
9
+ import { portOpen } from './socks.js'
10
+ import { ymsHomeIsBuilt } from './build.js'
11
+
12
+ export function ymsPidFile(cfg) {
13
+ return join(cfg.stateDir, 'yms.pid')
14
+ }
15
+
16
+ // EXACT flag set from the proven work/start-yms.sh (YonCode launch template).
17
+ // Do NOT edit module/package pairs from memory — wrong module names are
18
+ // silently ignored by the JVM and break Liberty JNDI init (observed:
19
+ // PseudoContext "no JNDI feature is configured" forever, module deploy fails).
20
+ // The last 2 exports + 3 opens come from the YonCode-generated
21
+ // .vscode/launch.json (ensureLaunchConfig) — same provenance, kept in sync.
22
+ const ADD_EXPORTS = [
23
+ 'java.base/sun.security.action=ALL-UNNAMED',
24
+ 'java.naming/com.sun.jndi.ldap=ALL-UNNAMED',
25
+ 'java.naming/com.sun.jndi.url.ldap=ALL-UNNAMED',
26
+ 'jdk.naming.dns/com.sun.jndi.dns=ALL-UNNAMED',
27
+ 'jdk.naming.dns/com.sun.jndi.url.dns=ALL-UNNAMED',
28
+ 'java.security.jgss/sun.security.krb5.internal=ALL-UNNAMED',
29
+ 'jdk.attach/sun.tools.attach=ALL-UNNAMED',
30
+ 'jdk.management.agent/jdk.internal.agent=ALL-UNNAMED',
31
+ 'java.base/jdk.internal.vm=ALL-UNNAMED',
32
+ ]
33
+ const ADD_OPENS = [
34
+ 'java.base/java.util=ALL-UNNAMED',
35
+ 'java.base/java.lang=ALL-UNNAMED',
36
+ 'java.base/java.util.concurrent=ALL-UNNAMED',
37
+ 'java.base/java.io=ALL-UNNAMED',
38
+ 'java.base/java.nio=ALL-UNNAMED',
39
+ 'java.base/sun.nio.ch=ALL-UNNAMED',
40
+ 'java.naming/javax.naming.spi=ALL-UNNAMED',
41
+ 'java.naming/com.sun.naming.internal=ALL-UNNAMED',
42
+ 'jdk.naming.rmi/com.sun.jndi.url.rmi=ALL-UNNAMED',
43
+ 'java.naming/javax.naming=ALL-UNNAMED',
44
+ 'java.rmi/java.rmi=ALL-UNNAMED',
45
+ 'java.sql/java.sql=ALL-UNNAMED',
46
+ 'java.management/javax.management=ALL-UNNAMED',
47
+ 'java.base/java.lang.reflect=ALL-UNNAMED',
48
+ 'java.desktop/java.awt.image=ALL-UNNAMED',
49
+ 'java.base/java.security=ALL-UNNAMED',
50
+ 'java.base/java.net=ALL-UNNAMED',
51
+ 'java.base/java.text=ALL-UNNAMED',
52
+ 'java.base/sun.net.www.protocol.https=ALL-UNNAMED',
53
+ ]
54
+
55
+ /**
56
+ * rdbga cookie value (base64 of "module,/context@bind:port").
57
+ *
58
+ * Every part is validated: a half-filled config used to produce
59
+ * `base64("null,/@null:31010")`, and browser tools would then happily write that into
60
+ * the workbench — pointing the gateway at an instance that cannot exist. Fail loudly.
61
+ */
62
+ export function rdbgaValue(cfg) {
63
+ if (cfg.module == null) throw new Error('rdbga 值缺少 module——先在项目目录下运行(yonbuilder.json / module.xml)')
64
+ const bind = String(cfg.serverBindAddress ?? '')
65
+ if (/^(\d{1,3}\.){3}\d{1,3}$/.test(bind) !== true) {
66
+ throw new Error('rdbga 值缺少合法的 serverBindAddress(IPv4)——补项目 .vscode/settings.json 的 yds.serverBindAddress')
67
+ }
68
+ const port = Number(cfg.serverPort)
69
+ if (Number.isInteger(port) !== true || port < 1 || port > 65535) {
70
+ throw new Error('rdbga 值缺少合法的 serverPort——补项目 .vscode/settings.json 的 yds.serverPort')
71
+ }
72
+ const context = String(cfg.context ?? '')
73
+ const raw = `${cfg.module},${context}@${bind}:${port}`
74
+ return Buffer.from(raw, 'utf8').toString('base64')
75
+ }
76
+
77
+ /** Full launch description: env, jvm args, args. */
78
+ export function launchSpec(cfg, { jvmArgs = [] } = {}) {
79
+ if (cfg.module == null) throw new Error('未识别模块名')
80
+ const logsDir = join(cfg.ymsHome, 'logs')
81
+ const env = {
82
+ ...process.env,
83
+ mw_profiles_active: cfg.env,
84
+ YDS_STATE: 'debugging',
85
+ access_key: cfg.accessKey ?? '',
86
+ access_secret: cfg.accessSecret ?? '',
87
+ 'access.key': cfg.accessKey ?? '',
88
+ 'access.secret': cfg.accessSecret ?? '',
89
+ enable_cloud_debug: 'true',
90
+ YMS_ENABLE_MODULE: cfg.module,
91
+ yms_enable_module: cfg.module,
92
+ }
93
+ const jvm = [
94
+ `-DYMS_WORK_PATH=${join(cfg.ymsHome, 'workarea')}`,
95
+ '-DYMS-DEV=true',
96
+ '-DYMS-DEV-MODE=debug',
97
+ `-Dyms.log.path=${logsDir}`,
98
+ `-Dyms.dev.libProjectsFile=${join(cfg.ymsHome, '.libraryProjects.list')}`,
99
+ `-Dapp.metainfo.preferred-networks=${cfg.serverBindAddress}`,
100
+ '-Dapp.metainfo.ping_connect_timeout=200',
101
+ `-Ddomain.url=${cfg.workbenchUrl}`,
102
+ `-Dserver.port=${cfg.ymsPort}`,
103
+ `-Drdbga=${rdbgaValue(cfg)}`,
104
+ // start_mode=static: server discovery reads config/ip_node_mapping.json
105
+ // instead of eureka (unreachable from a dev machine) — from YonCode's
106
+ // generated .vscode/launch.json. Makes outbound RPC fall back to the
107
+ // domain.url cloud-debug rewrite for unreachable pod instances.
108
+ '-Dapp.metainfo.start_mode=static',
109
+ '-Dyms_fetch_config_version=2.0.0',
110
+ '-Dyms.bootstrap.enableNewConfig=true',
111
+ ...ADD_EXPORTS.map((v) => `--add-exports=${v}`),
112
+ ...ADD_OPENS.map((v) => `--add-opens=${v}`),
113
+ ...jvmArgs.filter((v) => typeof v === 'string' && v.trim() !== '').map((v) => v.trim()),
114
+ ]
115
+ return { java: cfg.java, env, jvm, main: 'iuap.yms.YMSLauncher', cp: join(cfg.ymsHome, 'yms.jar'), logsDir }
116
+ }
117
+
118
+ /** Ensure .javaExecutable / .libraryProjects.list exist so the home is self-contained. */
119
+ export function ensureHomeMarkers(cfg) {
120
+ if (cfg.java == null) throw new Error('未找到 JDK')
121
+ const exe = join(cfg.ymsHome, '.javaExecutable')
122
+ if (!existsSync(exe)) writeFileSync(exe, cfg.java + '\n', 'utf8')
123
+ const libs = join(cfg.ymsHome, '.libraryProjects.list')
124
+ if (!existsSync(libs)) writeFileSync(libs, '\n', 'utf8')
125
+ }
126
+
127
+ /**
128
+ * Detached start: spawn java setsid, stdout/stderr -> yms-console.log,
129
+ * pidfile in .dsh-ris-yds/yms.pid. Returns { started, pid } or
130
+ * { alreadyRunning }.
131
+ */
132
+ /** Remember the port the running instance listens on (read back by config). */
133
+ function rememberPort(cfg) {
134
+ try {
135
+ mkdirSync(cfg.stateDir, { recursive: true })
136
+ writeFileSync(join(cfg.stateDir, 'yms-port'), `${cfg.ymsPort}\n`, { encoding: 'utf8' })
137
+ } catch { /* the port file is a convenience, never a requirement */ }
138
+ }
139
+
140
+ export async function startDetached(cfg, { jvmArgs = [] } = {}) {
141
+ const pidFile = ymsPidFile(cfg)
142
+ const running = await readRunning(cfg)
143
+ if (running !== null) return { started: false, alreadyRunning: true, pid: running.pid }
144
+ if (!ymsHomeIsBuilt(cfg)) throw new Error('yms-home 未构建(先执行 yds build)')
145
+ if (cfg.java == null) throw new Error('未找到 JDK——用 .env 的 java.path 或 JAVA_HOME 指定')
146
+ if (cfg.module == null) throw new Error('未识别模块名——检查项目 yonbuilder.json / module.xml')
147
+ const spec = launchSpec(cfg, { jvmArgs })
148
+ ensureHomeMarkers(cfg)
149
+ rememberPort(cfg)
150
+ mkdirSync(cfg.stateDir, { recursive: true })
151
+ const log = join(cfg.stateDir, 'yms-console.log')
152
+ // crude rotation: keep the last full log, never let it grow unbounded
153
+ try {
154
+ const st = statSync(log)
155
+ if (st.size > 16 * 1024 * 1024) renameSync(log, `${log}.old`)
156
+ } catch { /* absent */ }
157
+ const out = openSync(log, 'a')
158
+ const err = openSync(log, 'a')
159
+ const child = spawn(spec.java, [...spec.jvm, '-cp', spec.cp, spec.main], {
160
+ cwd: cfg.ymsHome,
161
+ env: spec.env,
162
+ detached: true,
163
+ stdio: ['ignore', out, err],
164
+ windowsHide: true,
165
+ })
166
+ child.unref()
167
+ // parent copies of the log fds are dup'd into the child; close ours
168
+ try { closeSync(out) } catch { /* ignore */ }
169
+ try { closeSync(err) } catch { /* ignore */ }
170
+ writeFileSync(pidFile, String(child.pid), 'utf8')
171
+ // wait for the Liberty port to accept (~35s typical; hard budget 120s)
172
+ let up = false
173
+ for (let i = 0; i < 120 && !up; i += 1) {
174
+ // eslint-disable-next-line no-await-in-loop
175
+ up = await portOpen('127.0.0.1', cfg.ymsPort, 500)
176
+ if (!up) {
177
+ try { process.kill(child.pid, 0) } catch { throw new Error('YMS 进程已退出,查看 .dsh-ris-yds/yms-console.log') }
178
+ // eslint-disable-next-line no-await-in-loop
179
+ await new Promise((r) => setTimeout(r, 500))
180
+ }
181
+ }
182
+ if (!up) {
183
+ try { unlinkSync(pidFile) } catch { /* ignore */ }
184
+ try { process.kill(child.pid, 'SIGTERM') } catch { /* ignore */ }
185
+ throw new Error(`YMS 端口 ${cfg.ymsPort} 在 120 秒内未就绪——查看 ${log}`)
186
+ }
187
+ return { started: true, pid: child.pid, port: cfg.ymsPort, consoleLog: log }
188
+ }
189
+
190
+ /** Foreground start: java as our child, signals forwarded (for run-with-log wrappers). */
191
+ export function startForeground(cfg, { jvmArgs = [], onStdout, signal } = {}) {
192
+ if (!ymsHomeIsBuilt(cfg)) throw new Error('yms-home 未构建(先执行 yds build)')
193
+ if (cfg.java == null) throw new Error('未找到 JDK——用 .env 的 java.path 或 JAVA_HOME 指定')
194
+ if (cfg.module == null) throw new Error('未识别模块名——检查项目 yonbuilder.json / module.xml')
195
+ const spec = launchSpec(cfg, { jvmArgs })
196
+ ensureHomeMarkers(cfg)
197
+ const child = spawn(spec.java, [...spec.jvm, '-cp', spec.cp, spec.main], {
198
+ cwd: cfg.ymsHome,
199
+ env: spec.env,
200
+ stdio: ['inherit', 'inherit', 'inherit'],
201
+ signal,
202
+ windowsHide: true,
203
+ })
204
+ const fwd = (sig) => {
205
+ try { child.kill(sig) } catch { /* ignore */ }
206
+ }
207
+ process.on('SIGTERM', () => fwd('SIGTERM'))
208
+ process.on('SIGINT', () => fwd('SIGINT'))
209
+ // record the java pid so `yds status` / `yds stop` are not blind to
210
+ // IDE-started (foreground) instances; unlink when the child goes away.
211
+ try {
212
+ mkdirSync(cfg.stateDir, { recursive: true })
213
+ writeFileSync(ymsPidFile(cfg), String(child.pid), 'utf8')
214
+ child.once('close', () => { try { unlinkSync(ymsPidFile(cfg)) } catch { /* ignore */ } })
215
+ } catch { /* best effort */ }
216
+ return child
217
+ }
218
+
219
+ /** pid is really our YMS java (cmdline marker), not a reused pid. */
220
+ function pidIsYms(pid) {
221
+ try {
222
+ const cmd = readFileSync(`/proc/${pid}/cmdline`, 'utf8')
223
+ return cmd.includes('yms.jar') || cmd.includes('YMSLauncher')
224
+ } catch { return false }
225
+ }
226
+
227
+ /** Read the pidfile; verify the process is alive AND ours; null otherwise (stale pidfile is unlinked). */
228
+ export async function readRunning(cfg) {
229
+ const pidFile = ymsPidFile(cfg)
230
+ if (!existsSync(pidFile)) return null
231
+ const pid = Number(readFileSync(pidFile, 'utf8').trim())
232
+ if (!Number.isInteger(pid) || pid <= 0) return null
233
+ try { process.kill(pid, 0) } catch { try { unlinkSync(pidFile) } catch { /* ignore */ } return null }
234
+ if (!pidIsYms(pid)) {
235
+ // pid reused by an unrelated process after a crash/reboot — stale marker
236
+ try { unlinkSync(pidFile) } catch { /* ignore */ }
237
+ return null
238
+ }
239
+ return { pid }
240
+ }
241
+
242
+ /** Stop the detached YMS: TERM, grace (YMS is slow), KILL. */
243
+ export async function stopYms(cfg, { graceMs = 30000 } = {}) {
244
+ const running = await readRunning(cfg) // identity-verified: never kills a reused pid
245
+ if (running === null) return { stopped: false, reason: '未在运行' }
246
+ try { process.kill(running.pid, 'SIGTERM') } catch { /* ignore */ }
247
+ const deadline = Date.now() + graceMs
248
+ while (Date.now() < deadline) {
249
+ // eslint-disable-next-line no-await-in-loop
250
+ await new Promise((r) => setTimeout(r, 500))
251
+ try { process.kill(running.pid, 0) } catch { break }
252
+ }
253
+ try { process.kill(running.pid, 0); process.kill(running.pid, 'SIGKILL') } catch { /* gone */ }
254
+ try { unlinkSync(ymsPidFile(cfg)) } catch { /* ignore */ }
255
+ return { stopped: true, pid: running.pid }
256
+ }
257
+
258
+ /** Status row: process + port + forwarder + config state. */
259
+ export async function status(cfg) {
260
+ const running = await readRunning(cfg) // identity-checked
261
+ const port = await portOpen('127.0.0.1', cfg.ymsPort, 800)
262
+ let ready = null
263
+ if (port === true) {
264
+ ready = await httpGet(`http://127.0.0.1:${cfg.ymsPort}/_yms/ready`, 3000)
265
+ }
266
+ return {
267
+ process: running,
268
+ port,
269
+ ready: ready !== null ? ready.status : null,
270
+ ymsHomeBuilt: ymsHomeIsBuilt(cfg),
271
+ configPresent: existsSync(join(cfg.ymsHome, 'config', 'yms_middleware.json')),
272
+ socksForwarder: cfg.socks === null ? null : await portOpen('127.0.0.1', cfg.dbForwardPort, 300),
273
+ }
274
+ }
275
+
276
+ function httpGet(url, timeout) {
277
+ return new Promise((resolve_) => {
278
+ const done = (value) => resolve_(value)
279
+ const r = http.get(url, (res) => { res.resume(); done({ status: res.statusCode }) })
280
+ r.setTimeout(timeout, () => { r.destroy(); done(null) })
281
+ r.once('error', () => done(null))
282
+ })
283
+ }
284
+
285
+ /** Tail helper for yds logs — reads only the last 256KB (log can be huge). */
286
+ export function tailFile(path, lines = 80) {
287
+ if (!existsSync(path)) return null
288
+ const st = statSync(path)
289
+ const readSize = Math.min(st.size, 256 * 1024)
290
+ const fh = openSync(path, 'r')
291
+ try {
292
+ const buf = Buffer.alloc(readSize)
293
+ readSync(fh, buf, 0, readSize, st.size - readSize)
294
+ const text = buf.toString('utf8')
295
+ const all = text.split('\n')
296
+ // first line may be a partial line from the mid-file cut — drop it
297
+ if (readSize < st.size && all.length > 1) all.shift()
298
+ return all.slice(-lines).join('\n')
299
+ } finally {
300
+ closeSync(fh)
301
+ }
302
+ }
package/lib/yoncode.js ADDED
@@ -0,0 +1,249 @@
1
+ /**
2
+ * YonCode host patching.
3
+ *
4
+ * YonCode derives the address it advertises to the platform (serverBindAddress)
5
+ * from a local NIC. When `.vscode/settings.json` points it at a callback address
6
+ * the machine does not own — the normal case behind NAT, in WSL, or through a
7
+ * port forward — it refuses to launch with
8
+ * "Invalid IP address, no interface found with address <ip>".
9
+ *
10
+ * We cannot fix that from our side: the check lives in YonCode's bundled
11
+ * extension (`resources/app/extensions/yonbuilder-development-studio-core/dist/
12
+ * extension.js`). This module detects whether the check is still active and can
13
+ * turn the throw into a warning — with a `.bak` next to it, so it can be undone
14
+ * and re-applied after a YonCode upgrade rewrites the file.
15
+ *
16
+ * Nothing here touches the project; it is opt-in from the user (the agent asks
17
+ * before writing into a Program Files directory).
18
+ */
19
+
20
+ import { copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from 'node:fs'
21
+ import { homedir } from 'node:os'
22
+ import { join } from 'node:path'
23
+
24
+ /** Marker our patch leaves in the bundle (also how a patch is detected). */
25
+ export const YONCODE_PATCH_MARKER = '[yds-patch]'
26
+
27
+ /** The file we patch, relative to the YonCode installation directory. */
28
+ export const YONCODE_EXTENSION_RELATIVE = 'resources/app/extensions/yonbuilder-development-studio-core/dist/extension.js'
29
+
30
+ /** Matches the `if (!ipv4IsValid(ipv4)) { throw … }` block in the shipped bundle. */
31
+ const VALIDATION_RE = /if\s*\(\s*!\s*_util\.ipv4IsValid\([^)]*\)\s*\)\s*\{\s*throw new Error\([^;]*?\);\s*\}/
32
+
33
+ /**
34
+ * The YMS launch `vmArgs` snippet YonCode builds — the anchor we insert the JVM
35
+ * proxy flags after. It is a plain template string in the bundle.
36
+ */
37
+ const SOCKS_ANCHOR = /-Dapp\.metainfo\.preferred-networks=\\?\$\{command:yds\.serverBindAddress\} -Dserver\.port=\\?\$\{config:yds\.serverPort\}/
38
+
39
+ /** Our injected flags, so a re-patch replaces instead of stacking. */
40
+ const SOCKS_FLAGS_RE = / -DsocksProxyHost=[^\s"']+ -DsocksProxyPort=\d+/
41
+
42
+ /**
43
+ * Why this exists: ProxyBridge (WinDivert) breaks the Oracle TNS handshake —
44
+ * the connection is accepted, then reset right after `T2C7Oversion.doOVERSION`,
45
+ * so the YMS module cannot reach its database. A JVM started with
46
+ * `-DsocksProxyHost` opens an ordinary TCP connection through the SOCKS server
47
+ * instead, which is the same path that already works from WSL.
48
+ * @param socks - `{host, port}` from `.env` (`socks.host`/`socks.port`).
49
+ * @returns the flags to append, or null when no SOCKS server is configured.
50
+ */
51
+ export function socksFlags(socks) {
52
+ const host = typeof socks?.host === 'string' ? socks.host.trim() : ''
53
+ const port = Number(socks?.port)
54
+ if (host === '' || !Number.isInteger(port) || port <= 0 || port > 65535) return null
55
+ return ` -DsocksProxyHost=${host} -DsocksProxyPort=${port}`
56
+ }
57
+
58
+ /**
59
+ * Where a YonCode install may be, most specific first.
60
+ *
61
+ * `cfg.yoncodeHome` (`.env` `yoncode.home` or `YONCODE_HOME`) wins; otherwise the
62
+ * platform defaults, including the Windows drives WSL can see.
63
+ * @param cfg - resolved plugin config (`cfg.yoncodeHome` optional).
64
+ * @returns candidate installation directories.
65
+ */
66
+ export function yoncodeCandidates(cfg = {}) {
67
+ const out = []
68
+ const push = (value) => {
69
+ if (typeof value === 'string' && value !== '' && !out.includes(value)) out.push(value)
70
+ }
71
+ // An explicit location is authoritative: if it is wrong we report "absent"
72
+ // rather than silently inspecting some other installation.
73
+ const explicit = typeof cfg.yoncodeHome === 'string' && cfg.yoncodeHome !== ''
74
+ ? cfg.yoncodeHome
75
+ : (typeof process.env.YONCODE_HOME === 'string' && process.env.YONCODE_HOME !== '' ? process.env.YONCODE_HOME : '')
76
+ if (explicit !== '') return [explicit]
77
+ if (process.platform === 'win32') {
78
+ push('C:\\Program Files\\YonCode')
79
+ push('D:\\Program Files\\YonCode')
80
+ }
81
+ if (process.platform === 'darwin') {
82
+ push('/Applications/YonCode.app/Contents/Resources/app')
83
+ push(join(homedir(), 'Applications/YonCode.app/Contents/Resources/app'))
84
+ }
85
+ push('/opt/YonCode')
86
+ push('/usr/share/YonCode')
87
+ push('/usr/local/YonCode')
88
+ // A Windows install reached from WSL.
89
+ for (const drive of ['c', 'd', 'e']) push(`/mnt/${drive}/Program Files/YonCode`)
90
+ return out
91
+ }
92
+
93
+ /**
94
+ * Read the bundle and classify it.
95
+ *
96
+ * States: `patched` (our warning is in place), `unpatched` (the throw is still
97
+ * there), `unrecognised` (file exists but has neither shape — a different
98
+ * YonCode version we refuse to guess about), `absent` (no installation found),
99
+ * `incomplete` (directory exists without the bundle).
100
+ * @param cfg - resolved plugin config.
101
+ * @returns `{ found, home, file, state, backup, detail }`.
102
+ */
103
+ export function detectYoncode(cfg = {}) {
104
+ let seenIncomplete = null
105
+ for (const home of yoncodeCandidates(cfg)) {
106
+ const file = join(home, YONCODE_EXTENSION_RELATIVE)
107
+ if (!existsSync(file)) {
108
+ if (seenIncomplete === null && existsSync(home)) seenIncomplete = home
109
+ continue
110
+ }
111
+ let text = ''
112
+ try {
113
+ if (statSync(file).size > 32 * 1024 * 1024) return { found: true, home, file, state: 'unrecognised', backup: null, detail: '文件过大,跳过' }
114
+ text = readFileSync(file, 'utf8')
115
+ } catch (error) {
116
+ return { found: true, home, file, state: 'unrecognised', backup: null, detail: `无法读取:${error instanceof Error ? error.message : String(error)}` }
117
+ }
118
+ const backup = `${file}.bak`
119
+ const hasBackup = existsSync(backup)
120
+ const socksMatch = SOCKS_FLAGS_RE.exec(text)
121
+ const socksValue = socksMatch === null ? '' : socksMatch[0].trim()
122
+ const socksState = socksMatch !== null ? 'patched' : SOCKS_ANCHOR.test(text) ? 'unpatched' : 'unrecognised'
123
+ const withSocks = (result) => ({ ...result, found: true, home, file, socksState, socksValue, backup: hasBackup ? backup : null })
124
+ if (text.includes(YONCODE_PATCH_MARKER)) {
125
+ return withSocks({ state: 'patched', detail: '网卡 IP 校验已改为告警' })
126
+ }
127
+ if (VALIDATION_RE.test(text)) {
128
+ return withSocks({ state: 'unpatched', detail: '网卡 IP 校验仍在(serverBindAddress 不是本机网卡时拒绝启动)' })
129
+ }
130
+ return withSocks({ state: 'unrecognised', detail: '找不到校验代码,可能是不认识的 YonCode 版本' })
131
+ }
132
+ if (seenIncomplete !== null) {
133
+ return { found: true, home: seenIncomplete, file: join(seenIncomplete, YONCODE_EXTENSION_RELATIVE), state: 'incomplete', backup: null, detail: '安装目录存在但没找到 YonCode 扩展包' }
134
+ }
135
+ return { found: false, home: null, file: null, state: 'absent', backup: null, detail: '没有找到 YonCode 安装(可用 .env 的 yoncode.home 指定)' }
136
+ }
137
+
138
+ /** True when the failure was a permission problem, i.e. elevation would help. */
139
+ function isPermissionError(error) {
140
+ const code = error?.code ?? ''
141
+ return code === 'EACCES' || code === 'EPERM' || code === 'EROFS' || /permission denied/i.test(error?.message ?? '')
142
+ }
143
+
144
+ /**
145
+ * Patch the bundle: turn the NIC check into a warning, and (optionally) append the
146
+ * JVM SOCKS flags to the YMS launch arguments.
147
+ *
148
+ * Both edits share one `.bak` of the untouched file, so {@link restoreYoncode}
149
+ * undoes them together. Idempotent: an already patched bundle reports
150
+ * `changed: false`.
151
+ * @param detected - the result of {@link detectYoncode}.
152
+ * @param {{socks?: {host: string, port: number}}} [options] - SOCKS server to inject.
153
+ * @returns `{ ok, changed, state, socksState, error?, needElevation? }`.
154
+ */
155
+ export function patchYoncode(detected, options = {}) {
156
+ if (!detected.found) return { ok: false, changed: false, state: detected.state, socksState: detected.socksState, error: detected.detail }
157
+ const flags = socksFlags(options.socks)
158
+ const wantsSocks = flags !== null
159
+ const needsValidation = detected.state === 'unpatched'
160
+ // Same flags already in place → nothing to do; a *different* SOCKS server must
161
+ // replace the old pair instead of stacking a second one.
162
+ const needsSocks = wantsSocks && (detected.socksState !== 'patched' || (detected.socksValue ?? '') !== flags.trim())
163
+ if (!needsValidation && !needsSocks) {
164
+ // `ok` means "the bundle has every edit this call asked for": the NIC check
165
+ // when it was still active, the JVM flags when a SOCKS server was given.
166
+ const ok = detected.state === 'patched' && (!wantsSocks || detected.socksState === 'patched')
167
+ const error = ok
168
+ ? undefined
169
+ : detected.socksState === 'unrecognised' && wantsSocks
170
+ ? '找不到 YMS 启动参数模板,无法注入 JVM 代理参数'
171
+ : detected.detail
172
+ return { ok, changed: false, state: detected.state, socksState: detected.socksState, error }
173
+ }
174
+ try {
175
+ let text = readFileSync(detected.file, 'utf8')
176
+ if (needsValidation && !VALIDATION_RE.test(text)) return { ok: false, changed: false, state: 'unrecognised', socksState: detected.socksState, error: '文件内容已变化,无法定位校验代码' }
177
+ if (needsSocks && !SOCKS_FLAGS_RE.test(text) && !SOCKS_ANCHOR.test(text)) {
178
+ return { ok: false, changed: false, state: detected.state, socksState: 'unrecognised', error: '找不到 YMS 启动参数模板,无法注入 JVM 代理参数' }
179
+ }
180
+ if (!existsSync(`${detected.file}.bak`)) copyFileSync(detected.file, `${detected.file}.bak`)
181
+ if (needsValidation) {
182
+ text = text.replace(VALIDATION_RE, (block) => {
183
+ const match = /ipv4IsValid\(([^)]*)\)/.exec(block)
184
+ const argument = match === null ? 'ipv4' : match[1].trim()
185
+ return `if (!_util.ipv4IsValid(${argument})) {\n console.warn("${YONCODE_PATCH_MARKER} serverBindAddress " + ${argument} + " is not a local NIC IP, skip validation");\n }`
186
+ })
187
+ }
188
+ if (needsSocks) {
189
+ text = SOCKS_FLAGS_RE.test(text)
190
+ ? text.replace(SOCKS_FLAGS_RE, flags)
191
+ : text.replace(SOCKS_ANCHOR, (anchor) => anchor + flags)
192
+ }
193
+ writeFileSync(detected.file, text)
194
+ return {
195
+ ok: true,
196
+ changed: true,
197
+ state: needsValidation ? 'patched' : detected.state,
198
+ socksState: needsSocks ? 'patched' : detected.socksState,
199
+ }
200
+ } catch (error) {
201
+ const message = error instanceof Error ? error.message : String(error)
202
+ return { ok: false, changed: false, state: detected.state, socksState: detected.socksState, error: message, needElevation: isPermissionError(error) }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Put the original bundle back from `.bak`.
208
+ * @param detected - the result of {@link detectYoncode}.
209
+ * @returns `{ ok, changed, state, error?, needElevation? }`.
210
+ */
211
+ export function restoreYoncode(detected) {
212
+ const backup = detected.backup ?? (detected.file === null ? null : `${detected.file}.bak`)
213
+ if (backup === null || !existsSync(backup)) {
214
+ return { ok: false, changed: false, state: detected.state, error: '没有 .bak 备份,无法还原(重装 YonCode 也会恢复原文件)' }
215
+ }
216
+ try {
217
+ copyFileSync(backup, detected.file)
218
+ return { ok: true, changed: true, state: 'unpatched' }
219
+ } catch (error) {
220
+ const message = error instanceof Error ? error.message : String(error)
221
+ return { ok: false, changed: false, state: detected.state, error: message, needElevation: isPermissionError(error) }
222
+ }
223
+ }
224
+
225
+ /**
226
+ * One-line summary for `yds doctor` and the CLI.
227
+ * @param detected - the result of {@link detectYoncode}.
228
+ * @returns human readable line.
229
+ */
230
+ export function describeYoncode(detected) {
231
+ if (!detected.found) return `yoncode: ${detected.detail}`
232
+ const where = detected.home ?? detected.file
233
+ const socks = detected.socksState === 'patched' ? ',JVM 代理参数已注入' : ''
234
+ return `yoncode: ${detected.state}(${detected.detail}${socks})${where === null ? '' : ` · ${where}`}`
235
+ }
236
+
237
+ /**
238
+ * Should we warn the user about the launch configuration we are about to write?
239
+ *
240
+ * Only when YonCode is installed AND its check is active: an absent YonCode is
241
+ * not a problem (we run standalone), and a patched one is already fine.
242
+ * @param detected - the result of {@link detectYoncode}.
243
+ * @returns the warning text, or null.
244
+ */
245
+ export function yoncodeWarning(detected) {
246
+ if (!detected.found || detected.state !== 'unpatched') return null
247
+ return 'YonCode 仍在校验 serverBindAddress 必须是本机网卡 IP;用外网/端口转发/回调地址启动会失败。'
248
+ + '可以让 ris-yds 把该校验改成告警(需写 YonCode 安装目录的权限),运行 `yds yoncode patch` 或让我代做。'
249
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "dsh-ris-yds",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "YonBIP YDS local runtime for the DSH agent: config resolution (project settings.json + optional .env), YYCToken, confcenter sync, SOCKS forward, yms-home build/start/stop with JDWP, and dsh-ris-ide run-config integration. Cross-OS (linux/darwin/win32, WSL aware). · YonBIP YDS 本地运行时:项目配置解析(settings.json / .env)、YYCToken、confcenter 拉取、SOCKS 转发、yms-home 构建与启停(含 JDWP 调试)、dsh-ris-ide 启动器集成;跨平台(linux/darwin/win32,识别 WSL)。",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ris-cloud/dsh-ris-yds.git"
9
+ },
10
+ "homepage": "https://gitee.com/riscloud/dsh-ris-yds#readme",
11
+ "bugs": {
12
+ "url": "https://gitee.com/riscloud/dsh-ris-yds/issues"
13
+ },
14
+ "keywords": [
15
+ "dsh",
16
+ "dsh-plugin",
17
+ "deepseek-harness",
18
+ "yds",
19
+ "yonbip",
20
+ "yonyou",
21
+ "yms",
22
+ "liberty"
23
+ ],
24
+ "type": "module",
25
+ "main": "lib/index.js",
26
+ "dsh": {
27
+ "manifestVersion": 1,
28
+ "bundle": {
29
+ "patch": "cordis.patch.yml"
30
+ }
31
+ },
32
+ "exports": {
33
+ ".": "./lib/index.js",
34
+ "./cli": "./lib/cli.js",
35
+ "./package.json": "./package.json"
36
+ },
37
+ "bin": {
38
+ "yds": "bin/yds.js"
39
+ },
40
+ "files": [
41
+ "LICENSE",
42
+ "README.md",
43
+ "bin",
44
+ "cordis.patch.yml",
45
+ "lib",
46
+ "native",
47
+ "resources",
48
+ "skills",
49
+ "test",
50
+ "assets"
51
+ ],
52
+ "engines": {
53
+ "node": ">=18",
54
+ "dsh": ">=0.2.0-rc.2"
55
+ },
56
+ "scripts": {
57
+ "test": "node --test \"test/*.test.mjs\"",
58
+ "check:listing": "node bin/check-listing.mjs"
59
+ }
60
+ }