dsh-ris-ide 0.0.0-stage → 1.6.9

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.
@@ -0,0 +1,248 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <!-- The IDE panel resolves every host route against document.baseURI
7
+ (`routeUrl`), so an explicit root base keeps this page talking to the same
8
+ origin the DSH window uses — same port, same cookie, no new server. -->
9
+ <base href="/" />
10
+ <title>RIS-IDE</title>
11
+ <style>
12
+ /* This page deliberately does NOT boot the DSH frontend: no session, no
13
+ conversation, no plugin bundles. It is one panel and nothing else, so an
14
+ all-day window does not grow with the session history. */
15
+ :root { color-scheme: light dark; }
16
+ html, body { height: 100%; margin: 0; }
17
+ body {
18
+ font-family: system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
19
+ background: Canvas; color: CanvasText;
20
+ }
21
+ #root { height: 100%; min-height: 0; }
22
+ /* Loading / failure notice. Overlaid on purpose: the mount target stays empty
23
+ (React 18 createRoot does not clear an existing child) and is hidden once the
24
+ panel has mounted. */
25
+ #boot {
26
+ position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
27
+ padding: 24px; text-align: center; font-size: 13px; line-height: 1.6; opacity: 0.8;
28
+ }
29
+ #boot code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
30
+ </style>
31
+ </head>
32
+ <body>
33
+ <div id="boot">正在加载 RIS-IDE…</div>
34
+ <div id="root"></div>
35
+ <script>
36
+ // ONE IDE window per workspace. Clicking ⧉ RIS-IDE again must not stack windows:
37
+ // this page claims a per-workspace lock BEFORE booting the panel.
38
+ //
39
+ // IMPORTANT (dsh restart): `window.open(url, sameName)` REUSES the window the
40
+ // user already has open — the browser navigates it, and the OLD document may
41
+ // still be releasing its lock while this one boots. So a failed claim is
42
+ // retried for ~1.2s before we believe a second window really exists; and even
43
+ // then we never auto-close (that made the new window flash and vanish). The
44
+ // page shows a notice instead, with "切换到已有窗口" / "仍然在这里打开".
45
+ (function () {
46
+ var resolveClaim
47
+ window.__risIdeClaimReady = new Promise(function (resolve) { resolveClaim = resolve })
48
+ var claim = { blocked: false }
49
+ window.__risIdeClaim = claim
50
+ var params = new URLSearchParams(window.location.search)
51
+ var cwd = params.get('cwd') || ''
52
+ // The opener passes the workspace-derived window name (`wname`); the hash
53
+ // fallback keeps older bookmarked URLs working. The lock key IS that name, so
54
+ // a second page cannot take it while the first one is alive.
55
+ var value = String(cwd).replace(/[\\/]+$/, '')
56
+ var hash = 0
57
+ for (var index = 0; index < value.length; index += 1) hash = ((hash * 31) + value.charCodeAt(index)) | 0
58
+ var windowName = params.get('wname') || ('dsh-ris-ide-window-' + (hash >>> 0).toString(36))
59
+ var key = windowName
60
+ var channel = null
61
+ try { channel = typeof BroadcastChannel === 'function' ? new BroadcastChannel(key) : null } catch (error) { channel = null }
62
+ var boot = document.getElementById('boot')
63
+ /** Best effort: ask any other IDE window to come forward before we go away. */
64
+ function nudgeHolder() {
65
+ try { if (channel !== null) channel.postMessage({ type: 'focus' }) } catch (error) { /* closed */ }
66
+ }
67
+ function button(label, onClick) {
68
+ var node = document.createElement('button')
69
+ node.type = 'button'
70
+ node.textContent = label
71
+ node.style.cssText = 'margin:0 4px;padding:4px 10px;font:inherit;cursor:pointer'
72
+ node.addEventListener('click', onClick)
73
+ return node
74
+ }
75
+ /** Another window holds the lock: offer a choice, never close this one. */
76
+ function showBlocked() {
77
+ claim.blocked = true
78
+ if (boot === null) { resolveClaim(false); return }
79
+ boot.textContent = ''
80
+ var text = document.createElement('div')
81
+ text.textContent = 'RIS-IDE 已在另一个窗口打开。'
82
+ var row = document.createElement('div')
83
+ row.style.marginTop = '10px'
84
+ // Focusing another browser window is not reliably allowed (Chrome may ignore
85
+ // a background window's focus()), so the honest action is to close this
86
+ // duplicate: the window that holds the lock is the one to keep.
87
+ row.appendChild(button('关闭这个窗口', function () {
88
+ nudgeHolder()
89
+ try { window.close() } catch (error) { /* already gone */ }
90
+ }))
91
+ row.appendChild(button('仍然在这里打开', function () {
92
+ // The user overrides the single-window guard: boot here as well.
93
+ claim.blocked = false
94
+ boot.textContent = '正在加载 RIS-IDE…'
95
+ if (typeof window.__risIdeBootPanel === 'function') window.__risIdeBootPanel()
96
+ }))
97
+ boot.appendChild(text)
98
+ boot.appendChild(row)
99
+ resolveClaim(false)
100
+ }
101
+ function becomeHolder() {
102
+ if (channel === null) return
103
+ channel.onmessage = function (event) {
104
+ var data = event !== null && event !== undefined ? event.data : null
105
+ if (data !== null && typeof data === 'object' && data.type === 'focus') {
106
+ try { window.focus() } catch (error) { /* ignore */ }
107
+ }
108
+ }
109
+ }
110
+ var locks = typeof navigator !== 'undefined' ? navigator.locks : undefined
111
+ if (locks && typeof locks.request === 'function') {
112
+ var attempt = 0
113
+ function tryClaim() {
114
+ attempt += 1
115
+ locks.request(key, { ifAvailable: true }, function (lock) {
116
+ if (lock === null) {
117
+ // A reused window's previous document can still hold the lock for a
118
+ // moment; retry with a short backoff before declaring a second window.
119
+ if (attempt < 5) { setTimeout(tryClaim, 120 * attempt); return undefined }
120
+ showBlocked()
121
+ return undefined
122
+ }
123
+ becomeHolder()
124
+ resolveClaim(true)
125
+ // Hold the lock for the lifetime of this window.
126
+ return new Promise(function () { /* never resolves */ })
127
+ }).catch(function () {
128
+ if (attempt < 5) setTimeout(tryClaim, 120 * attempt)
129
+ else showBlocked()
130
+ })
131
+ }
132
+ tryClaim()
133
+ return
134
+ }
135
+ if (channel === null) { becomeHolder(); resolveClaim(true); return }
136
+ // No Web Locks: ask any live IDE window on this channel whether it is there.
137
+ var answered = false
138
+ channel.onmessage = function (event) {
139
+ var data = event !== null && event !== undefined ? event.data : null
140
+ if (data === null || typeof data !== 'object') return
141
+ if (data.type === 'ping') {
142
+ try { channel.postMessage({ type: 'pong' }) } catch (error) { /* ignore */ }
143
+ try { window.focus() } catch (error) { /* ignore */ }
144
+ return
145
+ }
146
+ if (data.type === 'pong' && !answered) { answered = true; showBlocked() }
147
+ }
148
+ try { channel.postMessage({ type: 'ping' }) } catch (error) { /* ignore */ }
149
+ setTimeout(function () { if (!answered) { becomeHolder(); resolveClaim(true) } }, 400)
150
+ })()
151
+ </script>
152
+ <script src="dsh-ris-ide/window/vendor/react.js"></script>
153
+ <script src="dsh-ris-ide/window/vendor/react-dom.js"></script>
154
+ <script>
155
+ // The browser half is the SAME lib/client.js the DSH page loads; it asks the
156
+ // module loader for its factory and nothing else, so this shim is the whole
157
+ // adaptation layer (the only import it performs is `require('react')`).
158
+ window.__ModuleLoader__ = {
159
+ load: function (spec) { window.__risIdeSpec = spec },
160
+ }
161
+ </script>
162
+ <script src="dsh-ris-ide/window/app.js"></script>
163
+ <script>
164
+ (function () {
165
+ /** Show a boot failure in the static overlay; never throws (the overlay may be gone). */
166
+ function failBoot(message) {
167
+ try {
168
+ var boot = document.getElementById('boot')
169
+ if (boot === null) return
170
+ boot.textContent = 'RIS-IDE 无法启动:' + message + '(关掉本窗口,在主界面重新打开)'
171
+ } catch {
172
+ // Nothing left to show it in — the console below carries the detail.
173
+ }
174
+ }
175
+ function bootPanel() {
176
+ var root = document.getElementById('root')
177
+ var boot = document.getElementById('boot')
178
+ function fail(message) {
179
+ failBoot(message)
180
+ }
181
+ var spec = window.__risIdeSpec
182
+ if (!spec || typeof spec.factory !== 'function') { fail('客户端脚本没有加载'); return }
183
+ if (typeof window.React !== 'object' || typeof window.ReactDOM !== 'object') { fail('React 没有加载'); return }
184
+ var client
185
+ try {
186
+ client = spec.factory(function (id) {
187
+ if (id === 'react') return window.React
188
+ if (id === 'react-dom/client') return window.ReactDOM
189
+ throw new Error('require("' + id + '") 在独立窗口中不可用')
190
+ })
191
+ } catch (error) {
192
+ fail((error && error.message) || String(error))
193
+ return
194
+ }
195
+ // Fill the screen's work area = "maximized" for a script-opened window: the
196
+ // opener asks for it, but browsers sometimes clamp popup sizes, and a window
197
+ // opened by same-origin script may size itself. Never fatal — if the browser
198
+ // refuses, the window keeps the size the opener requested.
199
+ try {
200
+ if (window.screen && typeof window.screen.availWidth === 'number' && window.screen.availWidth > 0) {
201
+ window.moveTo(0, 0)
202
+ window.resizeTo(window.screen.availWidth, window.screen.availHeight)
203
+ }
204
+ } catch (error) {
205
+ /* keep the opener size */
206
+ }
207
+ var params = new URLSearchParams(window.location.search)
208
+ var sessionId = params.get('sessionId') || ''
209
+ var cwd = params.get('cwd') || ''
210
+ var programId = params.get('programId') || ''
211
+ // `debug=1` (the compact panel's 🐞 button) starts the JDWP session on arrival.
212
+ var debug = params.get('debug') === '1'
213
+ document.title = 'RIS-IDE' + (cwd === '' ? '' : ' — ' + cwd)
214
+ if (typeof client.mountStandalone !== 'function') { fail('客户端脚本版本太旧'); return }
215
+ Promise.resolve(client.mountStandalone(root, {
216
+ sessionId: sessionId,
217
+ cwd: cwd,
218
+ programId: programId,
219
+ debug: debug,
220
+ })).then(function () {
221
+ // The panel paints itself; drop the loading overlay so it cannot swallow clicks.
222
+ boot.style.display = 'none'
223
+ }, function (error) {
224
+ fail((error && error.message) || String(error))
225
+ })
226
+ }
227
+ // Boot only once the one-window claim has settled: a REPLACED window (same
228
+ // name, dsh restarted) retries the lock first, and a genuinely second window
229
+ // gets the notice/choice instead of being closed under the user.
230
+ window.__risIdeBootPanel = bootPanel
231
+ // The boot chain itself is fenced too: a throw here used to become an
232
+ // unhandled rejection in a page that has no error UI (blank window).
233
+ Promise.resolve(window.__risIdeClaimReady).then(function () {
234
+ if (window.__risIdeClaim && window.__risIdeClaim.blocked === true) return
235
+ try {
236
+ bootPanel()
237
+ } catch (error) {
238
+ console.error('[dsh-ris-ide] boot failed', error)
239
+ failBoot((error && error.message) || String(error))
240
+ }
241
+ }).catch(function (error) {
242
+ console.error('[dsh-ris-ide] boot rejected', error)
243
+ failBoot((error && error.message) || String(error))
244
+ })
245
+ })()
246
+ </script>
247
+ </body>
248
+ </html>
package/package.json CHANGED
@@ -1,6 +1,98 @@
1
1
  {
2
2
  "name": "dsh-ris-ide",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.6.9",
4
+ "license": "MIT",
5
+ "description": "RIS-IDE local dev panel for the DSH web GUI: live log tail, one-click start/stop of workspace programs, JDWP debugging, file tree + editor, and a standalone IDE window that carries no session history. · DSH 右侧栏本地开发面板:日志实时跟随、一键启停本地程序、Java 断点调试、文件树与编辑器、独立 IDE 窗口。",
6
+ "keywords": [
7
+ "dsh",
8
+ "dsh-plugin",
9
+ "deepseek-harness",
10
+ "cordis",
11
+ "ide",
12
+ "devtools",
13
+ "log-viewer",
14
+ "spring-boot",
15
+ "jdwp",
16
+ "debugger",
17
+ "file-tree",
18
+ "code-editor"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/ris-cloud/dsh-ris-ide.git"
23
+ },
24
+ "homepage": "https://gitee.com/riscloud/dsh-ris-ide#readme",
25
+ "bugs": {
26
+ "url": "https://gitee.com/riscloud/dsh-ris-ide/issues"
27
+ },
28
+ "type": "module",
29
+ "main": "lib/index.js",
30
+ "exports": {
31
+ ".": "./lib/index.js",
32
+ "./client": "./lib/client.js",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "lib/index.js",
37
+ "lib/client.js",
38
+ "lib/debug",
39
+ "bin/run-with-log.sh",
40
+ "bin/run-with-log.ps1",
41
+ "test",
42
+ "cordis.patch.yml",
43
+ "README.md",
44
+ "skills",
45
+ "lib",
46
+ "LICENSE"
47
+ ],
48
+ "engines": {
49
+ "dsh": ">=0.2.0-rc.2"
50
+ },
51
+ "peerDependencies": {
52
+ "@deepseek-ai/dsh-client-ui-sidebar-right": ">=0.2.1-rc.1",
53
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.2.1-rc.1"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@deepseek-ai/dsh-client-ui-sidebar-right": {
57
+ "optional": true
58
+ },
59
+ "@deepseek-ai/dsh-client-ui-slots": {
60
+ "optional": true
61
+ }
62
+ },
63
+ "dsh": {
64
+ "manifestVersion": 1,
65
+ "bundle": {
66
+ "patch": "./cordis.patch.yml"
67
+ },
68
+ "client": {
69
+ "platform": "web"
70
+ }
71
+ },
72
+ "scripts": {
73
+ "test": "node --test \"test/*.test.mjs\"",
74
+ "build": "tsdown --config tsdown.config.mjs",
75
+ "check:listing": "node bin/check-listing.mjs"
76
+ },
77
+ "devDependencies": {
78
+ "@codemirror/commands": "^6.6.0",
79
+ "@codemirror/lang-java": "^6.0.1",
80
+ "@codemirror/language": "^6.10.0",
81
+ "@codemirror/search": "^6.5.0",
82
+ "@codemirror/state": "^6.4.0",
83
+ "@codemirror/view": "^6.26.0",
84
+ "@lezer/highlight": "^1.2.0",
85
+ "react": "^18.2.0",
86
+ "react-dom": "^18.2.0",
87
+ "tsdown": "^0.22.14",
88
+ "@codemirror/lang-css": "^6.0.0",
89
+ "@codemirror/lang-html": "^6.0.0",
90
+ "@codemirror/lang-javascript": "^6.0.0",
91
+ "@codemirror/lang-json": "^6.0.0",
92
+ "@codemirror/lang-markdown": "^6.0.0",
93
+ "@codemirror/lang-sql": "^6.0.0",
94
+ "@codemirror/lang-vue": "^6.0.0",
95
+ "@codemirror/lang-xml": "^6.0.0",
96
+ "@codemirror/lang-yaml": "^6.0.0"
97
+ }
98
+ }
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: dsh-ris-ide-debug
3
+ description: "用 ris_dev_* 工具操作本机 RIS-IDE/侧边栏开发面板:列出/启动/停止/重启工作区里的 Spring Boot 与前端程序、读实时日志、并用 JDWP 调试 Java 项目(以调试模式重启并 attach、加/删/清断点、读线程/调用栈/变量、单步/继续/暂停)。当用户说「启动/停止某个服务」「看日志」「打断点/调试某个接口」「为什么这里是这个值」「跑一下 ris-model-biz/ris-upms-biz/ris-ui-3」「把 IDE 工作台打开给我看」时使用。前端项目没有 JVM,只能启停与看日志,不能调试;无表达式求值/条件断点/热替换。"
4
+ ---
5
+
6
+ # RIS-IDE 调试技能(dsh-ris-ide-debug)
7
+
8
+ > 这套命令由 `dsh-ris-ide` 插件注册,和侧边栏「RIS-IDE」面板**同一套扫描/启停/调试内核**。
9
+ > 你只能**选程序**(`id` / `name` / `path`),**永远不传命令**:启动命令由宿主按工作区内容(pom / package.json / `.dsh-ris-ide.json`)自己拼。
10
+
11
+ ## 0. 铁律(先读,避免踩坑)
12
+
13
+ 1. `ris_dev_start` 是**启动进程**;`ris_dev_debug {action:"start"}` 是**以调试模式重启并 attach**(正在跑的服务会被停掉再起)。两者不可混用。
14
+ 2. `start` 是异步的:Spring 约 **40–55 s** 才 UP。看 `ris_dev_status` 或 `ris_dev_logs` 判断,别急着断言失败。
15
+ 3. **JDWP 只服务一个 debugger 会话**:绝对不要用 `nc`/`telnet` 探端口(会把会话占掉)。attach 完成的信号是 JVM 日志出现 `Listening for transport dt_socket`,`start` 内部已经这么等了。
16
+ 4. 断点用**全限定类名 + 可执行行号**(Maven 默认带 debug 信息),**不是文件路径**;行号不在该类的 LineNumberTable 里不会命中。
17
+ 5. 调试能力边界:**没有表达式求值(evaluate)、无条件断点/日志断点、无热替换**。能看线程、栈、局部变量、字段、数组、String。
18
+ 6. 宿主重启后调试会话与断点都丢失;重新 `start` + `breakpoint_add` 即可。
19
+ 7. `suspend: true` 会真的把 JVM 停在启动处(Spring 卡住),记得 `resume`。
20
+ 8. 自定义 `startCommand` 的条目必须在 `java` 后带 `${JDWP_ARGS}` 占位符,否则调试启动直接报错(错误信息会说明)。
21
+
22
+ ## 1. 发现与生命周期
23
+
24
+ | 命令 | 参数 | 作用 |
25
+ |---|---|---|
26
+ | `ris_dev_programs` | `refresh?`, `runningOnly?` | 列工作区可运行程序:`id / name / kind / dir / mainClass|script / running / pid / logPath` |
27
+ | `ris_dev_start` | `id` 或 `name` 或 `path` | 启动(Spring:`mvn -Pdev compile` → `exec java`;前端:`<pm> run <script>`),走 `run-with-log.sh`;已在跑返回 `alreadyRunning: true` |
28
+ | `ris_dev_restart` | 同上 | 停整个进程组再起(端口一定释放) |
29
+ | `ris_dev_stop` | 同上 | TERM wrapper → pidfile 宽限 → KILL;只对 `/proc` cmdline 是 `run-with-log.sh` 的 PID 发信号 |
30
+ | `ris_dev_status` | 无 / 一个 ref | 不带参=全量;带参=`running/pid/uptime/url/logPath` |
31
+ | `ris_dev_logs` | ref, `lines?`(80, 上限 500), `open?`(默认 true) | 读日志尾部并把「在侧边栏打开这份日志」排进队列;返回体带 `content`,直接读它 |
32
+
33
+ `name` 支持**唯一片段**(如 `ris-model`),有歧义会报错并列出候选;不确定就先用 `ris_dev_programs` 拿 `id`。
34
+
35
+ ## 2. 调试(只对 `kind: spring`)
36
+
37
+ `ris_dev_debug { action, id|name|path, ... }`,全部 action:
38
+
39
+ | action | 额外参数 | 作用 |
40
+ |---|---|---|
41
+ | `open` | — | 只把 RIS-IDE 工作台 tab 弹给用户(**不开始调试**) |
42
+ | `start` | `suspend?` | 以 JDWP 重启并 attach(随机 loopback 端口) |
43
+ | `stop` | — | 只断开,服务继续跑(真要停服务用 `ris_dev_stop`) |
44
+ | `status` | — | `attached / suspended / port`,很便宜,可反复调 |
45
+ | `poll` | `since?` | 增量事件(断点命中/单步/vmstart);记住上次的 `seq` 传 `since`,否则会重复读 |
46
+ | `breakpoints` | — | 列断点 `requestId / className / line` |
47
+ | `breakpoint_add` | `className`, `line`, `suspendPolicy?`(thread\|all) | 加行断点 |
48
+ | `breakpoint_remove` | `requestId` | 删一个 |
49
+ | `breakpoint_clear` | — | 清空 |
50
+ | `threads` | — | 线程列表 |
51
+ | `stack` | `threadId`, `max?`(50) | 调用栈 |
52
+ | `variables` | `threadId`, `frameIndex?`(0) | 局部变量/字段/数组(String 在 `stringValue`) |
53
+ | `step` | `kind?`(over\|into\|out), `threadId?` | 单步 |
54
+ | `resume` | `threadId?` | 继续 |
55
+ | `pause` | — | 挂起 |
56
+
57
+ ## 3. 标准调试回合(照抄即可)
58
+
59
+ **先查状态再动手**(`state` 一次给出:运行中/pid/内存、JDWP attached/port/suspended、已有断点、预置断点数):
60
+
61
+ ```
62
+ ris_dev_programs {} # 找 id / name
63
+ ris_dev_debug { "action": "state", "name": "<program>" } # ← 先看:能调试吗/连了吗/已有哪些断点
64
+ ris_dev_debug { "action": "start", "name": "<program>" } # 40–55 s(静默:不弹 IDE)
65
+ ris_dev_debug { "action": "status","name": "<program>" } # attached=true / port
66
+ ris_dev_debug { "action": "breakpoint_add", "name": "<program>",
67
+ "className": "cn.com.ris.cloud.model.ModelHelper", "line": 168 }
68
+ ris_dev_debug { "action": "breakpoint_remove", "name": "<program>",
69
+ "className": "cn.com.ris.cloud.model.ModelHelper", "line": 168 } # 按位置删(已知 requestId 也可传 requestId)
70
+ ris_dev_logs { "name": "<program>", "lines": 120, "open": false } # 配合日志看启动/请求(不弹侧边栏)
71
+ ris_dev_debug { "action": "poll", "name": "<program>" } # 等命中(记 seq)
72
+ ris_dev_debug { "action": "threads","name": "<program>" }
73
+ ris_dev_debug { "action": "stack", "name": "<program>", "threadId": "<id>" }
74
+ ris_dev_debug { "action": "variables", "name": "<program>", "threadId": "<id>", "frameIndex": 0 }
75
+ ris_dev_debug { "action": "step", "name": "<program>", "kind": "over" }
76
+ ris_dev_debug { "action": "resume", "name": "<program>" }
77
+ ris_dev_debug { "action": "stop", "name": "<program>" } # 只断开
78
+ ris_dev_stop { "name": "<program>" } # 真要停服务
79
+ ```
80
+
81
+
82
+
83
+ ```
84
+ ris_dev_programs {} # 找 id / name
85
+ ris_dev_debug { "action": "open", "name": "<program>" } # 让用户在 IDE 里看到
86
+ ris_dev_debug { "action": "start", "name": "<program>" } # 40–55 s
87
+ ris_dev_debug { "action": "status","name": "<program>" } # attached=true / port
88
+ ris_dev_debug { "action": "breakpoint_add", "name": "<program>",
89
+ "className": "cn.com.ris.cloud.model.ModelHelper", "line": 168 }
90
+ ris_dev_logs { "name": "<program>", "lines": 120 } # 配合日志看启动/请求
91
+ ris_dev_debug { "action": "poll", "name": "<program>" } # 等命中(记 seq)
92
+ ris_dev_debug { "action": "threads","name": "<program>" }
93
+ ris_dev_debug { "action": "stack", "name": "<program>", "threadId": "<id>" }
94
+ ris_dev_debug { "action": "variables", "name": "<program>", "threadId": "<id>", "frameIndex": 0 }
95
+ ris_dev_debug { "action": "step", "name": "<program>", "kind": "over" }
96
+ ris_dev_debug { "action": "resume", "name": "<program>" }
97
+ ris_dev_debug { "action": "stop", "name": "<program>" } # 只断开
98
+ ris_dev_stop { "name": "<program>" } # 真要停服务
99
+ ```
100
+
101
+ 典型节奏:`poll` 没事件就先给用户**发真实请求**(前端/接口)再 `poll`;命中后用 `threads → stack → variables` 定位,`resume` 继续;循环排查时保留断点、只 `poll since=<seq>`。
102
+
103
+ ## 3.5 和 IDE 共享同一份调试状态(重要)
104
+
105
+ 宿主只有一个 `debugSessions` 单例:**你的 `ris_dev_debug` 和用户在 IDE 里点的是同一个 JDWP 连接**。断点、挂起状态、事件队列、`seq` 都是共享的;你 `breakpoint_add` 的断点用户能看见,用户在行号上加的断点你也能看见。
106
+
107
+ 因此**命中不一定是你的断点**:
108
+
109
+ - `poll` 返回的 `breakpoint`/`step` 事件现在带有 `payload.className` 和 `payload.line`(宿主用 `requestId` 回填),**先核对是不是你加的那个**;
110
+ - 不是你的:`stack`/`variables` 看到的是**用户那个断点所在线程**的栈,直接 `step` 会顺着用户的路径走。正确做法是 `resume` 或 `step {threadId: <你自己事件的 threadId>}`,然后继续 `poll since=<seq>`;
111
+ - 你的断点之后才命中时,事件会再出现一次(`requestId` 是你 `breakpoint_add` 返回的那个);
112
+ - 用户手动 `⏵ 继续`/按 F8 也会推进同一个 VM —— 你拿到的 `seq` 跳变、或 `vmSuspended=false` 都属正常,不要当成"断点失效"。
113
+
114
+ 另外:断点在 **detach/重启后会保留**(宿主把它转存成预置断点,下次 attach 自动装上);`breakpoint_clear` 才是真清空。
115
+
116
+ ## 3.6 默认静默:不要替用户弹界面
117
+
118
+ 调试默认**静默进行**——`ris_dev_debug` 的任何 action(`state/start/breakpoint_*/poll/threads/stack/variables/step/resume/pause/stop`)**都不会打开或弹出 IDE**;只有 `action: "open"` 会。所以:
119
+
120
+ - **不要**「顺手 open」:除非用户说「给我看 / 打开工作台 / 我要看调试过程 / 显示日志」,才调 `ris_dev_debug {action:"open"}`(要看日志再加 `ris_dev_logs {..., "open": true}`);
121
+ - 读日志默认带 `"open": false`(`ris_dev_logs` 默认会弹侧边栏日志);
122
+ - 用户已经在开着工作台时**不用你 open**:它会自己跟着(别处命中断点会自动切项目、打开文件、高亮停住行);
123
+ - 控制台 `▶ 启动`/`🐞 调试` 是用户的手动操作,走 UI;agent 负责静默的部分。
124
+
125
+ ## 4. 在 IDE 里对应什么(人看到的东西)
126
+
127
+ - 侧边栏「RIS-IDE」tab:左上是工作空间目录树(整个 cwd,点文件即打开)、左下控制台(每个程序的 `▶ / ■ / 🐞 / ▤` = start / stop / debug / logs),右上是源码(**打开即可编辑**,`Ctrl/Cmd+S` 保存,未保存页签带 `*`,关闭会提示保存;源码用侧边栏渲染器,Java 有语法高亮),右下是调试 + 实时日志。
128
+ - 调试区工具行:`🐞 ⏹ ⏸ ⏵ ⤵ ⤷ ⤴ ⟳ ⊟`,分别对应 start / stop / pause / resume / step(over/into/out) / poll / 折叠。
129
+ - 源码左侧细栏是**断点栏**:双击加断点、点红点删断点(追加/删除的就是 `breakpoint_add` / `breakpoint_remove`)。
130
+ - 顶栏显示**工作空间路径**、检索框、`📄` 打开 `.dsh-ris-ide.json`、`⚙` 生成/更新配置、`⟲` 还原布局尺寸。
131
+
132
+ ## 5. 用户问「怎么装这个技能」时
133
+
134
+ 本技能随插件发布,可让用户二选一(也可直接执行):
135
+
136
+ - 项目级(推荐,随仓库):`<项目根>/.dsh/skills/dsh-ris-ide-debug/SKILL.md`
137
+ - 用户级(本机全局):`~/.dsh/skills/dsh-ris-ide-debug/SKILL.md`
138
+
139
+ 用工具安装(优先,幂等、会校验内容是否变化):
140
+
141
+ ```
142
+ ris_dev_skill { "action": "install", "target": "project" } # 或 "user"
143
+ ris_dev_skill { "action": "status" } # 看装没装 / 是否过期
144
+ ris_dev_skill { "action": "show" } # 只读内容,不落盘
145
+ ```
146
+
147
+ 写入后 DSH 的 skills provider 会监听目录自动发现;若目录此前不存在导致没出现,重开一个会话再看技能列表。
148
+
149
+ ## 6. 失败速查
150
+
151
+ | 现象 | 先查 |
152
+ |---|---|
153
+ | `start` 秒退 | `ris_dev_logs { lines: 120 }` 的尾巴(编译错/端口占用/缺环境) |
154
+ | 调试启动报「缺占位符」 | 该程序的 `startCommand` 要带 `${JDWP_ARGS}` |
155
+ | `not-attached` | 先 `status`;`start` 没完成就调 step/poll 会这样 |
156
+ | 断点不命中 | 类名是否全限定、行号是否可执行行、类是否已加载(未加载会等 `ClassPrepare` 30 s) |
157
+ | 端口被占 | 用 `ris_dev_restart`(整组进程信号),不要手 `kill` |
158
+ | 前端程序 | 没有 JVM:只能 `start/stop/logs`,`debug` 只对 Spring |