dsh-mesh-ui 0.0.1 → 0.0.2

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.
@@ -1,233 +1,240 @@
1
- // dsh-mesh-ui — 委托面板数据层(只读)
2
- //
3
- // 供设置页「AI 网络」之外的独立面板使用:委托列表 / 单任务状态 / 对端会话正文。
4
- // 全部只读:
5
- // - 列表:读本地 ~/.dsh-mesh/delegations.json
6
- // - 任务状态:跑 `mesh-delegate.mjs status <taskId>`(只读查询,不改变对端)
7
- // - 会话正文:走对端 session_history 只读工具(解压会话日志,无模型往返)
8
- //
9
- // 归宿:reusable(dsh-mesh/plugin/dsh-mesh-ui/lib/)
10
-
11
- import { readFileSync } from 'node:fs'
12
- import { join } from 'node:path'
13
- import { cliJson, meshHome, nodeExec, resolveRepo, run, unwrapPeerCall } from './collect.js'
14
-
15
- export const NODE_LABELS = {}
16
-
17
- export function nodeLabel(name) {
18
- return NODE_LABELS[String(name || '').toLowerCase()] || String(name || '未知节点')
19
- }
20
-
21
- function readJson(file, fallback = null) {
22
- try {
23
- return JSON.parse(readFileSync(file, 'utf8'))
24
- } catch {
25
- return fallback
26
- }
27
- }
28
-
29
- /** 委托列表(本地台账,按更新时间倒序)。 */
30
- export function listDelegations(limit = 50) {
31
- const data = readJson(join(meshHome(), 'delegations.json'), { tasks: {} })
32
- const localNode = readJson(join(meshHome(), 'config.json'), {})?.node?.name || ''
33
- const tasks = Object.values(data?.tasks || {}).map((task) => {
34
- const start = Date.parse(task.createdAt || '')
35
- const end = Date.parse(task.updatedAt || '') || Date.now()
36
- const from = task.from || ''
37
- return {
38
- taskId: task.taskId || '',
39
- conversationId: task.conversationId || '',
40
- sessionId: task.sessionId || '',
41
- from,
42
- fromLabel: nodeLabel(from),
43
- fromIsLocal: Boolean(from) && from === localNode,
44
- fromInstance: task.fromInstance || '',
45
- node: task.node || '',
46
- nodeLabel: nodeLabel(task.node),
47
- toIsLocal: Boolean(task.node) && task.node === localNode,
48
- instance: task.instance || '',
49
- workspace: task.workspace || '',
50
- dedicated: Boolean(task.dedicated),
51
- state: task.state || 'unknown',
52
- startedAt: task.startedAt || '',
53
- finishedAt: task.finishedAt || '',
54
- isActive: ['working', 'submitted', 'accepted', 'question', 'provisioning', 'running'].indexOf(task.state || '') >= 0,
55
- activeFresh: ['working', 'submitted', 'accepted', 'question', 'provisioning', 'running'].indexOf(task.state || '') >= 0 && (Date.now() - Date.parse(task.updatedAt || 0) < 12 * 3600 * 1000),
56
- displayAt: task.finishedAt || task.updatedAt || '',
57
- policy: task.requestedPolicy || task.policy || '',
58
- goal: String(task.goal || ''),
59
- createdAt: task.createdAt || '',
60
- updatedAt: task.updatedAt || '',
61
- durationSec: (() => {
62
- if (!Number.isFinite(start)) return null
63
- const value = Math.max(0, Math.round((end - start) / 1000))
64
- return value > 12 * 3600 ? null : value // 台账 updatedAt 会被轮询刷新,超长值不可信
65
- })(),
66
- stalledReason: task.stalledReason || '',
67
- lastSummary: task.summary || ''
68
- }
69
- })
70
- tasks.sort((a, b) => {
71
- if (a.activeFresh !== b.activeFresh) return a.activeFresh ? -1 : 1
72
- return String(b.displayAt || b.updatedAt || '').localeCompare(String(a.displayAt || a.updatedAt || ''))
73
- })
74
- const byState = {}
75
- for (const task of tasks) byState[task.state] = (byState[task.state] || 0) + 1
76
- return { total: tasks.length, byState, localNode, tasks: tasks.slice(0, Math.max(1, Math.min(limit, 200))) }
77
- }
78
-
79
- /** 对端是 POSIX 时把 Windows 拼接产生的 \root\Code\... 归一化成 /root/workspace/...。 */
80
-
81
- /** 读对端某个 JSON 文件(只读 fs_read,路径来自本地台账 task.paths)。 */
82
- async function readPeerJson(repo, node, filePath) {
83
- if (!repo || !node || !filePath) return null
84
- const args = JSON.stringify({ path: String(filePath) })
85
- const { stdout } = await run(nodeExec(), [join(repo, 'src', 'cli.js'), 'peer', 'call', 'fs_read', '--to', String(node), '--args', args], { cwd: repo, timeout: 15_000, env: { ...process.env } })
86
- const start = String(stdout || '').indexOf('{')
87
- if (start < 0) return null
88
- try {
89
- const wrapper = JSON.parse(String(stdout).slice(start))
90
- const payload = unwrapPeerCall(wrapper)
91
- if (!payload || payload.ok === false) return null
92
- const content = payload.content ?? payload.text ?? payload
93
- if (typeof content === 'string') { try { return JSON.parse(content) } catch { return null } }
94
- return content && typeof content === 'object' ? content : null
95
- } catch {
96
- return null
97
- }
98
- }
99
-
100
- /** 从对端产物里算真实时间线(request.requestedAt → status.updatedAt)。 */
101
- async function artifactTimeline(repo, node, taskId) {
102
- const data = readJson(join(meshHome(), 'delegations.json'), { tasks: {} })
103
- const task = data?.tasks?.[taskId]
104
- const paths = task?.paths || {}
105
- const requestLocal = paths.requestLocal || paths.request || ''
106
- const statusRemote = paths.statusRemote || paths.status || ''
107
- const [request, status] = await Promise.all([
108
- Promise.resolve(requestLocal && !/^[A-Za-z]:\\\\/.test(requestLocal) === false ? readJson(requestLocal, null) : readJson(requestLocal, null)),
109
- readPeerJson(repo, node, statusRemote)
110
- ])
111
- const requestedAt = request?.requestedAt || task?.createdAt || ''
112
- const finishedAt = status?.updatedAt || status?.heartbeatAt || ''
113
- const startMs = Date.parse(requestedAt)
114
- const endMs = Date.parse(finishedAt)
115
- const durationSec = Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs ? Math.round((endMs - startMs) / 1000) : null
116
- return { requestedAt, finishedAt, durationSec, state: status?.state || '', summary: status?.summary || '', error: status?.error || '' }
117
- }
118
-
119
- /** 单任务状态 + 详情(只读;对端产物 + 会话元信息 + 时间线)。 */
120
- export async function taskStatus(taskId) {
121
- const repo = resolveRepo()
122
- if (!repo) return { ok: false, error: 'dsh-mesh repo not found' }
123
- if (!/^[A-Za-z0-9._:-]{4,120}$/.test(String(taskId || ''))) return { ok: false, error: 'invalid taskId' }
124
- const registry = readJson(join(meshHome(), 'delegations.json'), { tasks: {} })
125
- const entry = registry?.tasks?.[String(taskId)] || null
126
- const node = entry?.node || ''
127
- const paths = entry?.paths || {}
128
-
129
- const { error, stdout, stderr } = await run(nodeExec(), [join(repo, 'scripts', 'mesh-delegate.mjs'), 'status', String(taskId)], {
130
- cwd: repo,
131
- timeout: 45_000,
132
- env: { ...process.env }
133
- })
134
- const start = String(stdout || '').indexOf('{')
135
-
136
- // 对端产物(status.json / result.json)+ 本机 request.json + 对端会话元信息,全部并发
137
- const [requestJson, statusJson, resultJson, sessionMeta] = node
138
- ? await Promise.all([
139
- Promise.resolve(paths.requestLocal ? readJson(paths.requestLocal, null) : null),
140
- readPeerJson(repo, node, posixPath(paths.statusRemote || paths.status)),
141
- readPeerJson(repo, node, posixPath(paths.resultRemote || paths.result)),
142
- entry?.sessionId ? sessionHistory(node, entry.sessionId, 1).then((r) => (r.ok ? { totalMessages: r.payload?.totalMessages ?? 0, frameCount: r.payload?.frameCount ?? 0, bytes: r.payload?.bytes ?? 0, title: r.payload?.title || '' } : null)).catch(() => null) : Promise.resolve(null)
143
- ])
144
- : [null, null, null, null]
145
-
146
- const seconds = (from, to) => {
147
- const a = Date.parse(from || '')
148
- const b = Date.parse(to || '')
149
- return Number.isFinite(a) && Number.isFinite(b) && b >= a ? Math.round((b - a) / 1000) : null
150
- }
151
- const requestedAt = requestJson?.requestedAt || entry?.createdAt || ''
152
- const startedAt = statusJson?.startedAt || statusJson?.heartbeatAt || statusJson?.updatedAt || ''
153
- const finishedAt = statusJson?.updatedAt || resultJson?.completedAt || ((statusJson?.state === 'done' || statusJson?.state === 'failed' || statusJson?.state === 'cancelled') ? statusJson?.heartbeatAt || '' : '')
154
- const heartbeatAt = statusJson?.heartbeatAt || ''
155
- const detail = {
156
- request: requestJson,
157
- status: statusJson,
158
- result: resultJson,
159
- session: sessionMeta,
160
- timeline: {
161
- requestedAt,
162
- startedAt,
163
- heartbeatAt,
164
- finishedAt,
165
- responseSec: seconds(requestedAt, heartbeatAt || startedAt),
166
- execSec: seconds(startedAt, finishedAt),
167
- totalSec: seconds(requestedAt, finishedAt)
168
- },
169
- // 对端做过哪些操作 / 权限范围(来自 result.json 与 request.json 的协议字段)
170
- operations: {
171
- commandsRun: Array.isArray(resultJson?.commandsRun) ? resultJson.commandsRun : [],
172
- evidence: Array.isArray(resultJson?.evidence) ? resultJson.evidence : [],
173
- notes: resultJson?.notes || resultJson?.readonlyEvidence || '',
174
- readonly: resultJson?.readonly === true,
175
- scope: requestJson?.scope || '',
176
- acceptance: requestJson?.acceptance || '',
177
- sessionPolicy: requestJson?.sessionPolicy || '',
178
- targetInstance: requestJson?.instance || entry?.instance || '',
179
- workspace: requestJson?.workspace || entry?.workspace || ''
180
- },
181
- artifacts: statusJson && requestJson
182
- ? { requestedAt, finishedAt, durationSec: seconds(requestedAt, finishedAt), state: statusJson.state || '', summary: statusJson.summary || '', error: statusJson.error || '' }
183
- : null
184
- }
185
-
186
- if (start < 0) {
187
- return {
188
- ok: true,
189
- degraded: true,
190
- error: error || 'mesh-delegate status 未返回 JSON(对端可能离线或超时)',
191
- status: entry
192
- ? { state: entry.state, sessionState: '', staleSeconds: null, error: 'status.json 未取到', summary: entry.goal ? '(台账)' + String(entry.goal).slice(0, 200) : '' }
193
- : { state: 'unknown', sessionState: '', staleSeconds: null, error: '台账里也没有该任务' },
194
- artifacts: detail.artifacts,
195
- detail
196
- }
197
- }
198
- try {
199
- const status = JSON.parse(String(stdout).slice(start))
200
- return { ok: true, status, artifacts: detail.artifacts, detail }
201
- } catch {
202
- return { ok: false, error: 'task status JSON parse failed', stderr: String(stderr || '').slice(-400) }
203
- }
204
- }
205
-
206
- /** 对端会话正文(只读;走 session_history,最多 200 条消息)。 */
207
- export async function sessionHistory(node, sessionId, limit = 40) {
208
- const repo = resolveRepo()
209
- if (!repo) return { ok: false, error: 'dsh-mesh repo not found' }
210
- if (!/^[A-Za-z0-9._:-]{1,40}$/.test(String(node || ''))) return { ok: false, error: 'invalid node' }
211
- if (!/^[A-Za-z0-9._-]{8,120}$/.test(String(sessionId || ''))) return { ok: false, error: 'invalid sessionId' }
212
- const lines = Math.min(Math.max(Number(limit) || 40, 1), 200)
213
- const args = JSON.stringify({ sessionId: String(sessionId), limit: lines })
214
- const { error, stdout, stderr } = await run(
215
- nodeExec(),
216
- [join(repo, 'src', 'cli.js'), 'peer', 'call', 'session_history', '--to', String(node), '--args', args],
217
- { cwd: repo, timeout: 60_000, env: { ...process.env } }
218
- )
219
- const start = String(stdout || '').indexOf('{')
220
- if (start >= 0) {
221
- try {
222
- const wrapper = JSON.parse(String(stdout).slice(start))
223
- const payload = unwrapPeerCall(wrapper)
224
- if (payload?.ok === true) return { ok: true, payload }
225
- return { ok: false, error: payload?.message || 'session_history failed', payload }
226
- } catch {
227
- /* fall through */
228
- }
229
- }
230
- return { ok: false, error: error || 'session_history returned no JSON', stderr: String(stderr || '').slice(-400) }
231
- }
232
-
233
- export { cliJson }
1
+ // dsh-mesh-ui — 委托面板数据层(只读)
2
+ //
3
+ // 供设置页「AI 网络」之外的独立面板使用:委托列表 / 单任务状态 / 对端会话正文。
4
+ // 全部只读:
5
+ // - 列表:读本地 ~/.dsh-mesh/delegations.json
6
+ // - 任务状态:跑 `mesh-delegate.mjs status <taskId>`(只读查询,不改变对端)
7
+ // - 会话正文:走对端 session_history 只读工具(解压会话日志,无模型往返)
8
+ //
9
+ // 归宿:reusable(dsh-mesh/plugin/dsh-mesh-ui/lib/)
10
+
11
+ import { readFileSync } from 'node:fs'
12
+ import { join } from 'node:path'
13
+ import { cliJson, meshHome, nodeExec, resolveRepo, run, unwrapPeerCall } from './collect.js'
14
+
15
+ export const NODE_LABELS = {}
16
+
17
+ export function nodeLabel(name) {
18
+ return NODE_LABELS[String(name || '').toLowerCase()] || String(name || '未知节点')
19
+ }
20
+
21
+ function readJson(file, fallback = null) {
22
+ try {
23
+ return JSON.parse(readFileSync(file, 'utf8'))
24
+ } catch {
25
+ return fallback
26
+ }
27
+ }
28
+
29
+ /** 委托列表(本地台账,按更新时间倒序)。 */
30
+ export function listDelegations(limit = 50) {
31
+ const data = readJson(join(meshHome(), 'delegations.json'), { tasks: {} })
32
+ const localNode = readJson(join(meshHome(), 'config.json'), {})?.node?.name || ''
33
+ const tasks = Object.values(data?.tasks || {}).map((task) => {
34
+ const start = Date.parse(task.createdAt || '')
35
+ const end = Date.parse(task.updatedAt || '') || Date.now()
36
+ const from = task.from || ''
37
+ return {
38
+ taskId: task.taskId || '',
39
+ conversationId: task.conversationId || '',
40
+ sessionId: task.sessionId || '',
41
+ from,
42
+ fromLabel: nodeLabel(from),
43
+ fromIsLocal: Boolean(from) && from === localNode,
44
+ fromInstance: task.fromInstance || '',
45
+ node: task.node || '',
46
+ nodeLabel: nodeLabel(task.node),
47
+ toIsLocal: Boolean(task.node) && task.node === localNode,
48
+ instance: task.instance || '',
49
+ workspace: task.workspace || '',
50
+ dedicated: Boolean(task.dedicated),
51
+ state: task.state || 'unknown',
52
+ startedAt: task.startedAt || '',
53
+ finishedAt: task.finishedAt || '',
54
+ isActive: ['working', 'submitted', 'accepted', 'question', 'provisioning', 'running'].indexOf(task.state || '') >= 0,
55
+ activeFresh: ['working', 'submitted', 'accepted', 'question', 'provisioning', 'running'].indexOf(task.state || '') >= 0 && (Date.now() - Date.parse(task.updatedAt || 0) < 12 * 3600 * 1000),
56
+ displayAt: task.finishedAt || task.updatedAt || '',
57
+ policy: task.requestedPolicy || task.policy || '',
58
+ goal: String(task.goal || ''),
59
+ createdAt: task.createdAt || '',
60
+ updatedAt: task.updatedAt || '',
61
+ durationSec: (() => {
62
+ if (!Number.isFinite(start)) return null
63
+ const value = Math.max(0, Math.round((end - start) / 1000))
64
+ return value > 12 * 3600 ? null : value // 台账 updatedAt 会被轮询刷新,超长值不可信
65
+ })(),
66
+ stalledReason: task.stalledReason || '',
67
+ lastSummary: task.summary || ''
68
+ }
69
+ })
70
+ tasks.sort((a, b) => {
71
+ if (a.activeFresh !== b.activeFresh) return a.activeFresh ? -1 : 1
72
+ return String(b.displayAt || b.updatedAt || '').localeCompare(String(a.displayAt || a.updatedAt || ''))
73
+ })
74
+ const byState = {}
75
+ for (const task of tasks) byState[task.state] = (byState[task.state] || 0) + 1
76
+ return { total: tasks.length, byState, localNode, tasks: tasks.slice(0, Math.max(1, Math.min(limit, 200))) }
77
+ }
78
+
79
+ /** 对端是 POSIX 时把 Windows 拼接产生的 \root\Code\... 归一化成 /root/workspace/...。 */
80
+ function posixPath(raw) {
81
+ const s = String(raw || "")
82
+ if (!s) return s
83
+ if (/^[A-Za-z]:[\\/]/.test(s)) return s // 本机盘符路径(Windows 对端)不动
84
+ if (s.startsWith("\\\\")) return s // UNC 路径不动
85
+ if (s.startsWith("\\") || s.includes("\\")) return s.replace(/\\/g, "/") // \root\Code -> /root/workspace
86
+ return s
87
+ }
88
+ /** 读对端某个 JSON 文件(只读 fs_read,路径来自本地台账 task.paths)。 */
89
+ async function readPeerJson(repo, node, filePath) {
90
+ if (!repo || !node || !filePath) return null
91
+ const args = JSON.stringify({ path: String(filePath) })
92
+ const { stdout } = await run(nodeExec(), [join(repo, 'src', 'cli.js'), 'peer', 'call', 'fs_read', '--to', String(node), '--args', args], { cwd: repo, timeout: 15_000, env: { ...process.env } })
93
+ const start = String(stdout || '').indexOf('{')
94
+ if (start < 0) return null
95
+ try {
96
+ const wrapper = JSON.parse(String(stdout).slice(start))
97
+ const payload = unwrapPeerCall(wrapper)
98
+ if (!payload || payload.ok === false) return null
99
+ const content = payload.content ?? payload.text ?? payload
100
+ if (typeof content === 'string') { try { return JSON.parse(content) } catch { return null } }
101
+ return content && typeof content === 'object' ? content : null
102
+ } catch {
103
+ return null
104
+ }
105
+ }
106
+
107
+ /** 从对端产物里算真实时间线(request.requestedAt → status.updatedAt)。 */
108
+ async function artifactTimeline(repo, node, taskId) {
109
+ const data = readJson(join(meshHome(), 'delegations.json'), { tasks: {} })
110
+ const task = data?.tasks?.[taskId]
111
+ const paths = task?.paths || {}
112
+ const requestLocal = paths.requestLocal || paths.request || ''
113
+ const statusRemote = paths.statusRemote || paths.status || ''
114
+ const [request, status] = await Promise.all([
115
+ Promise.resolve(requestLocal && !/^[A-Za-z]:\\\\/.test(requestLocal) === false ? readJson(requestLocal, null) : readJson(requestLocal, null)),
116
+ readPeerJson(repo, node, statusRemote)
117
+ ])
118
+ const requestedAt = request?.requestedAt || task?.createdAt || ''
119
+ const finishedAt = status?.updatedAt || status?.heartbeatAt || ''
120
+ const startMs = Date.parse(requestedAt)
121
+ const endMs = Date.parse(finishedAt)
122
+ const durationSec = Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs ? Math.round((endMs - startMs) / 1000) : null
123
+ return { requestedAt, finishedAt, durationSec, state: status?.state || '', summary: status?.summary || '', error: status?.error || '' }
124
+ }
125
+
126
+ /** 单任务状态 + 详情(只读;对端产物 + 会话元信息 + 时间线)。 */
127
+ export async function taskStatus(taskId) {
128
+ const repo = resolveRepo()
129
+ if (!repo) return { ok: false, error: 'dsh-mesh repo not found' }
130
+ if (!/^[A-Za-z0-9._:-]{4,120}$/.test(String(taskId || ''))) return { ok: false, error: 'invalid taskId' }
131
+ const registry = readJson(join(meshHome(), 'delegations.json'), { tasks: {} })
132
+ const entry = registry?.tasks?.[String(taskId)] || null
133
+ const node = entry?.node || ''
134
+ const paths = entry?.paths || {}
135
+
136
+ const { error, stdout, stderr } = await run(nodeExec(), [join(repo, 'scripts', 'mesh-delegate.mjs'), 'status', String(taskId)], {
137
+ cwd: repo,
138
+ timeout: 45_000,
139
+ env: { ...process.env }
140
+ })
141
+ const start = String(stdout || '').indexOf('{')
142
+
143
+ // 对端产物(status.json / result.json)+ 本机 request.json + 对端会话元信息,全部并发
144
+ const [requestJson, statusJson, resultJson, sessionMeta] = node
145
+ ? await Promise.all([
146
+ Promise.resolve(paths.requestLocal ? readJson(paths.requestLocal, null) : null),
147
+ readPeerJson(repo, node, posixPath(paths.statusRemote || paths.status)),
148
+ readPeerJson(repo, node, posixPath(paths.resultRemote || paths.result)),
149
+ entry?.sessionId ? sessionHistory(node, entry.sessionId, 1).then((r) => (r.ok ? { totalMessages: r.payload?.totalMessages ?? 0, frameCount: r.payload?.frameCount ?? 0, bytes: r.payload?.bytes ?? 0, title: r.payload?.title || '' } : null)).catch(() => null) : Promise.resolve(null)
150
+ ])
151
+ : [null, null, null, null]
152
+
153
+ const seconds = (from, to) => {
154
+ const a = Date.parse(from || '')
155
+ const b = Date.parse(to || '')
156
+ return Number.isFinite(a) && Number.isFinite(b) && b >= a ? Math.round((b - a) / 1000) : null
157
+ }
158
+ const requestedAt = requestJson?.requestedAt || entry?.createdAt || ''
159
+ const startedAt = statusJson?.startedAt || statusJson?.heartbeatAt || statusJson?.updatedAt || ''
160
+ const finishedAt = statusJson?.updatedAt || resultJson?.completedAt || ((statusJson?.state === 'done' || statusJson?.state === 'failed' || statusJson?.state === 'cancelled') ? statusJson?.heartbeatAt || '' : '')
161
+ const heartbeatAt = statusJson?.heartbeatAt || ''
162
+ const detail = {
163
+ request: requestJson,
164
+ status: statusJson,
165
+ result: resultJson,
166
+ session: sessionMeta,
167
+ timeline: {
168
+ requestedAt,
169
+ startedAt,
170
+ heartbeatAt,
171
+ finishedAt,
172
+ responseSec: seconds(requestedAt, heartbeatAt || startedAt),
173
+ execSec: seconds(startedAt, finishedAt),
174
+ totalSec: seconds(requestedAt, finishedAt)
175
+ },
176
+ // 对端做过哪些操作 / 权限范围(来自 result.json 与 request.json 的协议字段)
177
+ operations: {
178
+ commandsRun: Array.isArray(resultJson?.commandsRun) ? resultJson.commandsRun : [],
179
+ evidence: Array.isArray(resultJson?.evidence) ? resultJson.evidence : [],
180
+ notes: resultJson?.notes || resultJson?.readonlyEvidence || '',
181
+ readonly: resultJson?.readonly === true,
182
+ scope: requestJson?.scope || '',
183
+ acceptance: requestJson?.acceptance || '',
184
+ sessionPolicy: requestJson?.sessionPolicy || '',
185
+ targetInstance: requestJson?.instance || entry?.instance || '',
186
+ workspace: requestJson?.workspace || entry?.workspace || ''
187
+ },
188
+ artifacts: statusJson && requestJson
189
+ ? { requestedAt, finishedAt, durationSec: seconds(requestedAt, finishedAt), state: statusJson.state || '', summary: statusJson.summary || '', error: statusJson.error || '' }
190
+ : null
191
+ }
192
+
193
+ if (start < 0) {
194
+ return {
195
+ ok: true,
196
+ degraded: true,
197
+ error: error || 'mesh-delegate status 未返回 JSON(对端可能离线或超时)',
198
+ status: entry
199
+ ? { state: entry.state, sessionState: '', staleSeconds: null, error: 'status.json 未取到', summary: entry.goal ? '(台账)' + String(entry.goal).slice(0, 200) : '' }
200
+ : { state: 'unknown', sessionState: '', staleSeconds: null, error: '台账里也没有该任务' },
201
+ artifacts: detail.artifacts,
202
+ detail
203
+ }
204
+ }
205
+ try {
206
+ const status = JSON.parse(String(stdout).slice(start))
207
+ return { ok: true, status, artifacts: detail.artifacts, detail }
208
+ } catch {
209
+ return { ok: false, error: 'task status JSON parse failed', stderr: String(stderr || '').slice(-400) }
210
+ }
211
+ }
212
+
213
+ /** 对端会话正文(只读;走 session_history,最多 200 条消息)。 */
214
+ export async function sessionHistory(node, sessionId, limit = 40) {
215
+ const repo = resolveRepo()
216
+ if (!repo) return { ok: false, error: 'dsh-mesh repo not found' }
217
+ if (!/^[A-Za-z0-9._:-]{1,40}$/.test(String(node || ''))) return { ok: false, error: 'invalid node' }
218
+ if (!/^[A-Za-z0-9._-]{8,120}$/.test(String(sessionId || ''))) return { ok: false, error: 'invalid sessionId' }
219
+ const lines = Math.min(Math.max(Number(limit) || 40, 1), 200)
220
+ const args = JSON.stringify({ sessionId: String(sessionId), limit: lines })
221
+ const { error, stdout, stderr } = await run(
222
+ nodeExec(),
223
+ [join(repo, 'src', 'cli.js'), 'peer', 'call', 'session_history', '--to', String(node), '--args', args],
224
+ { cwd: repo, timeout: 60_000, env: { ...process.env } }
225
+ )
226
+ const start = String(stdout || '').indexOf('{')
227
+ if (start >= 0) {
228
+ try {
229
+ const wrapper = JSON.parse(String(stdout).slice(start))
230
+ const payload = unwrapPeerCall(wrapper)
231
+ if (payload?.ok === true) return { ok: true, payload }
232
+ return { ok: false, error: payload?.message || 'session_history failed', payload }
233
+ } catch {
234
+ /* fall through */
235
+ }
236
+ }
237
+ return { ok: false, error: error || 'session_history returned no JSON', stderr: String(stderr || '').slice(-400) }
238
+ }
239
+
240
+ export { cliJson }