@mzzsfy/dsh-rs-workflow 0.2.4 → 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/README.md +14 -49
- package/cordis.patch.yml +8 -17
- package/lib/board.mjs +355 -0
- package/lib/driver/approve.mjs +126 -0
- package/lib/driver/control.mjs +46 -0
- package/lib/driver/index.mjs +454 -0
- package/lib/driver/prompts.mjs +102 -0
- package/lib/driver/runner.mjs +173 -0
- package/lib/driver/scheduler.mjs +159 -0
- package/lib/index.js +30 -213
- package/lib/orchestrator.mjs +428 -0
- package/lib/planner-gate.mjs +195 -0
- package/lib/release.mjs +183 -0
- package/lib/settings-schema.mjs +73 -0
- package/lib/spec.mjs +161 -0
- package/lib/storage.mjs +35 -0
- package/lib/store.mjs +255 -0
- package/lib/template-tool.mjs +161 -0
- package/lib/template.mjs +341 -0
- package/package.json +8 -6
- package/src/client.js +1269 -0
- package/lib/preset-sync.mjs +0 -227
- package/preset/rs-workflow/agent.cordis.yml +0 -196
- package/preset/rs-workflow/preset.yml +0 -5
- package/preset/rs-workflow/skills/rs-workflow/SKILL.md +0 -110
- package/preset/rs-workflow/skills/rs-workflow/references/engine.js +0 -1446
- package/preset/rs-workflow/skills/rs-workflow/references/templates.md +0 -104
- package/preset/rs-workflow/skills/rs-workflow/slots.json5 +0 -53
package/src/client.js
ADDED
|
@@ -0,0 +1,1269 @@
|
|
|
1
|
+
// rs-workflow client 半区:会话页签「若水编排」(gui-session,唯一运行记录视图)+
|
|
2
|
+
// 设置页分区「若水工作流」:流程模板(gui-editor)/配置(gui-config)。
|
|
3
|
+
// 自注册 __ModuleLoader__.load(对齐 v3);数据经 /api/rsww/* 读写。
|
|
4
|
+
// 类名前缀 rsww-;颜色仅取官方 alias token;开关遵循仓库规约(track+thumb,锚定 checkbox)。
|
|
5
|
+
|
|
6
|
+
window.__ModuleLoader__.load({
|
|
7
|
+
id: '@mzzsfy/dsh-rs-workflow',
|
|
8
|
+
factory(require) {
|
|
9
|
+
const React = require('react')
|
|
10
|
+
const { useState, useEffect, useCallback } = React
|
|
11
|
+
|
|
12
|
+
const API = '/api/rsww/'
|
|
13
|
+
const REFRESH_MS = 5 * 1000
|
|
14
|
+
const LIVE_REFRESH_MS = 2 * 1000
|
|
15
|
+
const VIEW_ID = 'rsww-flow'
|
|
16
|
+
const SETTLED_COUNT = 8
|
|
17
|
+
const ARMED_TIMEOUT_MS = 3 * 1000
|
|
18
|
+
|
|
19
|
+
const NAV_ICON = { '若水工作流': 'flow', 'dsh-rs-workflow': 'flow' }
|
|
20
|
+
if (window.__navicIcons !== undefined) window.__navicIcons.register(NAV_ICON)
|
|
21
|
+
else if (Array.isArray(window.__navicIconQueue)) window.__navicIconQueue.push(NAV_ICON)
|
|
22
|
+
else window.__navicIconQueue = [NAV_ICON]
|
|
23
|
+
|
|
24
|
+
const STATUS_META = {
|
|
25
|
+
running: { label: '运行中', tone: 'business' },
|
|
26
|
+
waiting_approval: { label: '待审批', tone: 'warn' },
|
|
27
|
+
paused: { label: '已暂停', tone: 'warn' },
|
|
28
|
+
completed: { label: '已完成', tone: 'success' },
|
|
29
|
+
cancelled: { label: '已取消', tone: 'mute' },
|
|
30
|
+
failed: { label: '失败', tone: 'error' },
|
|
31
|
+
blocked: { label: '受阻', tone: 'error' },
|
|
32
|
+
}
|
|
33
|
+
const TERMINAL_STATES = ['completed', 'cancelled', 'failed', 'blocked']
|
|
34
|
+
const EVENT_LABELS = { dispatch: '派发', submit: '产出', fail: '失败', skip: '跳过', control: '控制' }
|
|
35
|
+
|
|
36
|
+
const CSS = `
|
|
37
|
+
.rsww-root{max-width:860px;margin:0 auto;display:flex;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary,#0f1115)}
|
|
38
|
+
.rsww-flow{max-width:860px;margin:0 auto;display:flex;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary,#0f1115);padding-top:10px}
|
|
39
|
+
.rsww-head{display:flex;flex-direction:column;gap:2px;padding:4px 2px 0}
|
|
40
|
+
.rsww-head__title{font:600 16px/24px var(--dsw-font-family,-apple-system,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif)}
|
|
41
|
+
.rsww-head__caption{font:400 var(--dsw-font-xs-13,13px/20px sans-serif);color:var(--dsw-alias-label-tertiary,#81858c)}
|
|
42
|
+
.rsww-btn{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-secondary,#61666b);font:500 var(--dsw-font-xs-13,13px/20px sans-serif);border-radius:8px;padding:5px 12px;cursor:pointer;transition:background .15s,border-color .15s,color .15s}
|
|
43
|
+
.rsww-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));color:var(--dsw-alias-label-primary,#0f1115)}
|
|
44
|
+
.rsww-btn--danger{color:var(--dsw-alias-state-error-primary,#ec1313)}
|
|
45
|
+
.rsww-btn--danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,rgba(236,19,19,.06));border-color:var(--dsw-alias-state-error-primary,#ec1313)}
|
|
46
|
+
.rsww-btn:disabled{opacity:.45;cursor:default}
|
|
47
|
+
.rsww-badge{display:inline-flex;align-items:center;gap:5px;font:500 var(--dsw-font-xxs-12,12px/18px sans-serif);border-radius:999px;padding:2px 9px;white-space:nowrap}
|
|
48
|
+
.rsww-badge--business{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4176e6) 12%,transparent);color:var(--dsw-alias-state-business-primary,#4176e6)}
|
|
49
|
+
.rsww-badge--success{background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 12%,transparent);color:var(--dsw-alias-state-success-primary,#16a34a)}
|
|
50
|
+
.rsww-badge--error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#ec1313) 10%,transparent);color:var(--dsw-alias-state-error-primary,#ec1313)}
|
|
51
|
+
.rsww-badge--warn{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 14%,transparent);color:var(--dsw-alias-state-warn-primary,#b45309)}
|
|
52
|
+
.rsww-badge--mute{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));color:var(--dsw-alias-label-tertiary,#81858c)}
|
|
53
|
+
.rsww-dot{width:6px;height:6px;border-radius:50%;flex:none;background:currentColor}
|
|
54
|
+
.rsww-card{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:10px;background:var(--dsw-alias-bg-base,#fff);padding:12px 14px;display:flex;flex-direction:column;gap:6px}
|
|
55
|
+
.rsww-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
|
|
56
|
+
.rsww-req{font:600 var(--dsw-font-s-14,14px/22px sans-serif);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:180px}
|
|
57
|
+
.rsww-group{display:flex;flex-direction:column;gap:8px}
|
|
58
|
+
.rsww-group__head{display:flex;align-items:baseline;gap:8px;padding:2px 2px 0}
|
|
59
|
+
.rsww-group__title{font:600 var(--dsw-font-xs-13,13px/20px sans-serif);color:var(--dsw-alias-label-secondary,#61666b)}
|
|
60
|
+
.rsww-group__count{font:400 var(--dsw-font-xxs-12,12px/18px sans-serif);color:var(--dsw-alias-label-caption,#adb2b8)}
|
|
61
|
+
.rsww-section{display:flex;flex-direction:column;gap:4px}
|
|
62
|
+
.rsww-label{font:600 var(--dsw-font-xxs-12,12px/18px sans-serif);color:var(--dsw-alias-label-tertiary,#81858c);letter-spacing:.02em}
|
|
63
|
+
.rsww-text{font:400 var(--dsw-font-xs-13,13px/20px sans-serif);line-height:1.6;white-space:pre-wrap;word-break:break-word;color:var(--dsw-alias-label-primary,#0f1115)}
|
|
64
|
+
.rsww-mono{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px}
|
|
65
|
+
.rsww-error{font:400 var(--dsw-font-xs-13,13px/20px sans-serif);color:var(--dsw-alias-state-error-primary,#ec1313)}
|
|
66
|
+
.rsww-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:44px 0 40px;color:var(--dsw-alias-label-tertiary,#81858c)}
|
|
67
|
+
.rsww-empty__title{font:500 var(--dsw-font-s-14,14px/22px sans-serif);color:var(--dsw-alias-label-secondary,#61666b)}
|
|
68
|
+
.rsww-empty__hint{font:400 var(--dsw-font-xs-13,13px/20px sans-serif);text-align:center;max-width:420px}
|
|
69
|
+
.rsww-input{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:8px;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#0f1115);font:400 var(--dsw-font-xs-13,13px/20px sans-serif);padding:6px 10px;transition:border-color .15s}
|
|
70
|
+
.rsww-input:focus{outline:none;border-color:var(--dsw-alias-state-business-primary,#4176e6)}
|
|
71
|
+
.rsww-input--wide{flex:1;min-width:220px}
|
|
72
|
+
.rsww-steps{display:flex;flex-direction:column;gap:2px;margin:0;padding:0;list-style:none;max-height:420px;overflow:auto}
|
|
73
|
+
.rsww-step{display:flex;align-items:baseline;gap:8px;font:400 var(--dsw-font-xs-13,13px/20px sans-serif);line-height:1.55;padding:4px 6px;border-radius:6px}
|
|
74
|
+
.rsww-step:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}
|
|
75
|
+
.rsww-step__node{font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary,#81858c);flex:none;min-width:88px}
|
|
76
|
+
.rsww-step__sum{color:var(--dsw-alias-label-primary,#0f1115);word-break:break-word;flex:1;min-width:0}
|
|
77
|
+
.rsww-step--fail .rsww-step__sum{color:var(--dsw-alias-state-error-primary,#ec1313)}
|
|
78
|
+
.rsww-full{white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;line-height:1.6;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:8px;padding:8px;margin-top:4px;max-height:300px;overflow:auto;background:var(--dsw-alias-bg-module-platform,#f5f6f7)}
|
|
79
|
+
.rsww-switch{display:inline-flex;align-items:center;gap:6px;cursor:pointer;user-select:none}
|
|
80
|
+
.rsww-switch input[type="checkbox"]{position:absolute;opacity:0;width:1px;height:1px}
|
|
81
|
+
.rsww-switch__track{width:30px;height:17px;border-radius:999px;background:var(--dsw-alias-border-l3,rgba(0,0,0,.12));position:relative;transition:background .15s;flex:none}
|
|
82
|
+
.rsww-switch__thumb{position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;background:#fff;transition:transform .15s;box-shadow:0 1px 2px rgba(0,0,0,.2)}
|
|
83
|
+
.rsww-switch input[type="checkbox"]:checked + .rsww-switch__track{background:var(--dsw-alias-state-business-primary,#4176e6)}
|
|
84
|
+
.rsww-switch input[type="checkbox"]:checked + .rsww-switch__track .rsww-switch__thumb{transform:translateX(13px)}
|
|
85
|
+
.rsww-switch input[type="checkbox"]:focus-visible + .rsww-switch__track{outline:2px solid color-mix(in srgb,var(--dsw-alias-state-business-primary,#4176e6) 70%,white);outline-offset:1px}
|
|
86
|
+
.rsww-pills{display:inline-flex;gap:4px;padding:3px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:9px;background:var(--dsw-alias-bg-module-platform,#f5f6f7);width:max-content}
|
|
87
|
+
.rsww-pill{border:none;background:transparent;color:var(--dsw-alias-label-secondary,#61666b);font:500 var(--dsw-font-xs-13,13px/20px sans-serif);border-radius:6px;padding:4px 12px;cursor:pointer;white-space:nowrap}
|
|
88
|
+
.rsww-pill--on{background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#0f1115);box-shadow:0 1px 3px rgba(0,0,0,.1)}
|
|
89
|
+
.rsww-toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
90
|
+
.rsww-detail{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:10px;background:var(--dsw-alias-bg-base,#fff);padding:12px 14px;display:flex;flex-direction:column;gap:10px}
|
|
91
|
+
.rsww-modal{position:fixed;inset:0;z-index:60;display:flex;align-items:flex-start;justify-content:center;background:color-mix(in srgb,var(--dsw-alias-label-primary,#0f1115) 40%,transparent);padding:40px 16px;overflow:auto}
|
|
92
|
+
.rsww-modal__panel{width:100%;max-width:860px;background:var(--dsw-alias-bg-base,#fff);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:10px;box-shadow:0 8px 40px color-mix(in srgb,var(--dsw-alias-label-primary,#0f1115) 18%,transparent)}
|
|
93
|
+
.rsww-card--err{border-color:var(--dsw-alias-state-error-primary,#ec1313)}
|
|
94
|
+
.rsww-input--err{border-color:var(--dsw-alias-state-error-primary,#ec1313)}
|
|
95
|
+
.rsww-tree__row{display:flex;align-items:center;gap:8px;min-width:0;flex-wrap:wrap;font:400 var(--dsw-font-xs-13,13px/20px sans-serif)}
|
|
96
|
+
.rsww-note{font:400 var(--dsw-font-xxs-12,12px/18px sans-serif);color:var(--dsw-alias-label-caption,#adb2b8)}
|
|
97
|
+
.rsww-kv{display:flex;flex-direction:column;gap:6px}
|
|
98
|
+
.rsww-kv__row{display:flex;gap:6px;align-items:center;min-width:0}
|
|
99
|
+
.rsww-stack{display:flex;flex-direction:column;gap:10px}
|
|
100
|
+
.rsww-textarea{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:8px;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#0f1115);font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;line-height:1.6;padding:8px 10px;min-height:110px;resize:vertical;width:100%;box-sizing:border-box}
|
|
101
|
+
.rsww-textarea:focus{outline:none;border-color:var(--dsw-alias-state-business-primary,#4176e6)}
|
|
102
|
+
`
|
|
103
|
+
|
|
104
|
+
async function request(path) {
|
|
105
|
+
let res
|
|
106
|
+
try { res = await fetch(API + path) } catch (e) { return { ok: false, error: String((e && e.message) || e) } }
|
|
107
|
+
let json = null
|
|
108
|
+
try { json = await res.json() } catch { json = null }
|
|
109
|
+
if (!res.ok) return { ok: false, error: json && json.error ? json.error : '请求失败(' + res.status + ')' }
|
|
110
|
+
return { ok: true, data: json }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function post(path, body) {
|
|
114
|
+
let res
|
|
115
|
+
try {
|
|
116
|
+
res = await fetch(API + path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body || {}) })
|
|
117
|
+
} catch (e) { return { ok: false, error: String((e && e.message) || e) } }
|
|
118
|
+
let json = null
|
|
119
|
+
try { json = await res.json() } catch { json = null }
|
|
120
|
+
if (!res.ok) return { ok: false, error: json && json.error ? json.error : '请求失败(' + res.status + ')', data: json }
|
|
121
|
+
return { ok: true, data: json }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function statusMeta(status) { return STATUS_META[status] || { label: status || '-', tone: 'mute' } }
|
|
125
|
+
|
|
126
|
+
function relativeTime(ts) {
|
|
127
|
+
if (typeof ts !== 'number' || !(ts > 0)) return '-'
|
|
128
|
+
const delta = Date.now() - ts
|
|
129
|
+
const abs = Math.abs(delta)
|
|
130
|
+
const MINUTE = 60 * 1000
|
|
131
|
+
const HOUR = 60 * MINUTE
|
|
132
|
+
const DAY = 24 * HOUR
|
|
133
|
+
const suffix = delta >= 0 ? '前' : '后'
|
|
134
|
+
if (abs < MINUTE) return Math.max(1, Math.round(abs / 1000)) + ' 秒' + suffix
|
|
135
|
+
if (abs < HOUR) return Math.round(abs / MINUTE) + ' 分钟' + suffix
|
|
136
|
+
if (abs < DAY) return Math.round(abs / HOUR) + ' 小时' + suffix
|
|
137
|
+
return Math.round(abs / DAY) + ' 天' + suffix
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function Badge({ tone, children, dot, title }) {
|
|
141
|
+
return React.createElement('span', { className: 'rsww-badge rsww-badge--' + tone, title },
|
|
142
|
+
dot ? React.createElement('span', { className: 'rsww-dot' }) : null, children)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function StatusBadge({ status }) {
|
|
146
|
+
const meta = statusMeta(status)
|
|
147
|
+
// 待审批以裁决图标提示段边界等待裁决(gui-session 修订 2)
|
|
148
|
+
const icon = status === 'waiting_approval' ? '⚖ ' : ''
|
|
149
|
+
return React.createElement(Badge, { tone: meta.tone, dot: true }, icon + meta.label)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function EmptyState({ title, hint }) {
|
|
153
|
+
return React.createElement('div', { className: 'rsww-empty' },
|
|
154
|
+
React.createElement('span', { className: 'rsww-empty__title' }, title),
|
|
155
|
+
hint ? React.createElement('span', { className: 'rsww-empty__hint' }, hint) : null)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function Switch(props) {
|
|
159
|
+
return React.createElement('label', { className: 'rsww-switch' },
|
|
160
|
+
React.createElement('input', { type: 'checkbox', checked: props.checked, onChange: props.onChange, 'aria-label': props.ariaLabel }),
|
|
161
|
+
React.createElement('span', { className: 'rsww-switch__track' }, React.createElement('span', { className: 'rsww-switch__thumb' })),
|
|
162
|
+
React.createElement('span', { className: 'rsww-text' }, props.label))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 产出/指令/错误全文展开(痛点 7/18:零截断,默认折叠)
|
|
166
|
+
// 剪贴板写入:clipboard API 优先;非安全上下文(LAN HTTP)退化 execCommand
|
|
167
|
+
const copyText = async (text) => {
|
|
168
|
+
if (navigator.clipboard?.writeText) {
|
|
169
|
+
try { await navigator.clipboard.writeText(text); return true } catch { /* 权限拒绝时走兜底 */ }
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const ta = document.createElement('textarea')
|
|
173
|
+
ta.value = text
|
|
174
|
+
ta.style.position = 'fixed'
|
|
175
|
+
ta.style.opacity = '0'
|
|
176
|
+
document.body.appendChild(ta)
|
|
177
|
+
ta.select()
|
|
178
|
+
const done = document.execCommand('copy')
|
|
179
|
+
ta.remove()
|
|
180
|
+
return done
|
|
181
|
+
} catch { return false }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function CopyButton({ text, label = '复制' }) {
|
|
185
|
+
const [done, setDone] = useState(false)
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (!done) return undefined
|
|
188
|
+
const t = setTimeout(() => setDone(false), ARMED_TIMEOUT_MS)
|
|
189
|
+
return () => clearTimeout(t)
|
|
190
|
+
}, [done])
|
|
191
|
+
return React.createElement('button', { className: 'rsww-btn', style: { padding: '2px 8px' }, type: 'button',
|
|
192
|
+
onClick: async () => { if (await copyText(text)) setDone(true) } },
|
|
193
|
+
done ? '已复制' : label)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function OutputFullText({ label, text, tone }) {
|
|
197
|
+
const [open, setOpen] = useState(false)
|
|
198
|
+
if (!text) return null
|
|
199
|
+
return React.createElement('div', {},
|
|
200
|
+
React.createElement('button', { className: 'rsww-btn', style: { padding: '2px 8px' }, onClick: () => setOpen((v) => !v) },
|
|
201
|
+
(open ? '收起 ' : '展开 ') + label),
|
|
202
|
+
open ? React.createElement('div', { className: 'rsww-full', style: tone === 'error' ? { color: 'var(--dsw-alias-state-error-primary,#ec1313)' } : null }, text) : null,
|
|
203
|
+
open ? React.createElement(CopyButton, { text }) : null)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 步骤事件账行:按 store.step 落盘顺序渲染全量 body;body 判空兜底(空账/异常事件不崩页签)
|
|
207
|
+
// v5:裁决类 control 带 by 来源徽标(真人/代审,代审 reason 悬浮全文)
|
|
208
|
+
function StreamRow({ stepId, instance, event, body }) {
|
|
209
|
+
const safeBody = body || {}
|
|
210
|
+
const tag = stepId + (instance && instance !== '-' ? '#' + instance : '')
|
|
211
|
+
const kind = EVENT_LABELS[event] || event
|
|
212
|
+
const isVerdict = event === 'control' && (safeBody.kind === 'approve' || safeBody.kind === 'reject')
|
|
213
|
+
let summary = ''
|
|
214
|
+
if (event === 'dispatch') summary = safeBody.callLabel ? String(safeBody.callLabel) : '→ ' + tag
|
|
215
|
+
else if (event === 'submit') summary = '产出已提交'
|
|
216
|
+
else if (event === 'fail') summary = safeBody.error || '失败'
|
|
217
|
+
else if (event === 'control') summary = (safeBody.kind === 'message' ? (safeBody.inject ? '[纠偏注入] ' : '[排队] ') : '[' + safeBody.kind + '] ') + (safeBody.text || '')
|
|
218
|
+
else summary = safeBody.reason || ''
|
|
219
|
+
return React.createElement('li', { className: 'rsww-step' + (event === 'fail' ? ' rsww-step--fail' : '') },
|
|
220
|
+
React.createElement('span', { className: 'rsww-step__node' }, tag + ' · ' + kind),
|
|
221
|
+
isVerdict ? React.createElement(Badge, { tone: safeBody.by === 'main-agent' ? 'business' : 'warn', title: safeBody.reason || undefined },
|
|
222
|
+
safeBody.by === 'main-agent' ? '代审' : '真人') : null,
|
|
223
|
+
React.createElement('span', { className: 'rsww-step__sum' },
|
|
224
|
+
isVerdict ? (safeBody.kind === 'approve' ? '通过' : '驳回') + (safeBody.reason || '') : summary,
|
|
225
|
+
event === 'dispatch' && safeBody.prompt ? React.createElement(OutputFullText, { label: '指令全文', text: safeBody.prompt }) : null,
|
|
226
|
+
event === 'submit' && safeBody.outputs ? React.createElement(OutputFullText, { label: '产出全文', text: JSON.stringify(safeBody.outputs, null, 2) }) : null,
|
|
227
|
+
event === 'fail' ? React.createElement(OutputFullText, { label: '错误详情', text: safeBody.error || '' }) : null))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 控制条:按状态显隐按钮 + 审批裁决行(waiting_approval,提交带 by:user)+ 末端消息输入行(勾选「纠偏注入」)
|
|
231
|
+
// v5:waiting_approval 双入口提示(页签裁决与主循环裁决先到先得);paused 恢复后提示待代理推进;
|
|
232
|
+
// 驳回两步(先填审批意见再确认,意见入 redo.comments 服务重做说明),意见可空
|
|
233
|
+
function ControlBar({ run }) {
|
|
234
|
+
const [text, setText] = useState('')
|
|
235
|
+
const [inject, setInject] = useState(false)
|
|
236
|
+
const [armed, setArmed] = useState(false)
|
|
237
|
+
const [resumed, setResumed] = useState(false)
|
|
238
|
+
const [rejecting, setRejecting] = useState(false)
|
|
239
|
+
const [rejectText, setRejectText] = useState('')
|
|
240
|
+
const [error, setError] = useState('')
|
|
241
|
+
const live = run.status === 'running' || run.status === 'paused' || run.status === 'waiting_approval'
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
if (!armed) return undefined
|
|
244
|
+
const timer = setTimeout(() => setArmed(false), ARMED_TIMEOUT_MS)
|
|
245
|
+
return () => clearTimeout(timer)
|
|
246
|
+
}, [armed])
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (run.status !== 'waiting_approval') { setRejecting(false); setRejectText('') }
|
|
249
|
+
}, [run.status])
|
|
250
|
+
if (!live) return null
|
|
251
|
+
const control = async (kind, extra) => {
|
|
252
|
+
setError('')
|
|
253
|
+
const outcome = await post('control', { runId: run.runId, kind, ...(extra || {}) })
|
|
254
|
+
if (!outcome.ok) { setError(outcome.error); return false }
|
|
255
|
+
if (outcome.data && outcome.data.ok === false) { setError('未受理(状态已变化或已被处理)'); return false }
|
|
256
|
+
if (kind === 'resume') setResumed(true)
|
|
257
|
+
return true
|
|
258
|
+
}
|
|
259
|
+
const send = async () => {
|
|
260
|
+
if (!text.trim()) return
|
|
261
|
+
setError('')
|
|
262
|
+
const outcome = await post('control', { runId: run.runId, kind: 'message', text: text.trim(), inject })
|
|
263
|
+
if (!outcome.ok) { setError(outcome.error); return }
|
|
264
|
+
if (outcome.data && outcome.data.ok === false) { setError('未受理(状态已变化或已被处理)'); return }
|
|
265
|
+
setText('')
|
|
266
|
+
}
|
|
267
|
+
const confirmReject = async () => {
|
|
268
|
+
const accepted = await control('reject', { by: 'user', reason: rejectText.trim() })
|
|
269
|
+
if (accepted) { setRejecting(false); setRejectText('') }
|
|
270
|
+
}
|
|
271
|
+
const verdictButtons = run.status === 'waiting_approval'
|
|
272
|
+
? React.createElement('div', { className: 'rsww-section' },
|
|
273
|
+
React.createElement('div', { className: 'rsww-row' },
|
|
274
|
+
React.createElement('span', { className: 'rsww-note' }, '页签裁决与主循环裁决(代审或 ask_user 转呈)先到先得'),
|
|
275
|
+
React.createElement('button', { className: 'rsww-btn', onClick: () => control('approve', { by: 'user' }) }, '通过'),
|
|
276
|
+
rejecting
|
|
277
|
+
? null
|
|
278
|
+
: React.createElement('button', { className: 'rsww-btn rsww-btn--danger', onClick: () => setRejecting(true) }, '驳回')),
|
|
279
|
+
rejecting ? React.createElement('div', { className: 'rsww-row' },
|
|
280
|
+
React.createElement('input', {
|
|
281
|
+
className: 'rsww-input rsww-input--wide', value: rejectText,
|
|
282
|
+
placeholder: '驳回意见(可空;将作为审批意见注入重做批次)',
|
|
283
|
+
autoFocus: true,
|
|
284
|
+
onChange: (e) => setRejectText(e.target.value),
|
|
285
|
+
onKeyDown: (e) => { if (e.key === 'Enter') confirmReject() },
|
|
286
|
+
}),
|
|
287
|
+
React.createElement('button', { className: 'rsww-btn rsww-btn--danger', onClick: confirmReject }, '确认驳回'),
|
|
288
|
+
React.createElement('button', { className: 'rsww-btn', onClick: () => { setRejecting(false); setRejectText('') } }, '取消')) : null)
|
|
289
|
+
: null
|
|
290
|
+
return React.createElement('div', { className: 'rsww-section' },
|
|
291
|
+
React.createElement('div', { className: 'rsww-row' },
|
|
292
|
+
run.status === 'running' ? React.createElement('button', { className: 'rsww-btn', onClick: () => control('pause') }, '暂停') : null,
|
|
293
|
+
run.status === 'paused' ? React.createElement('button', { className: 'rsww-btn', onClick: () => control('resume') }, '恢复') : null,
|
|
294
|
+
armed
|
|
295
|
+
? React.createElement('button', { className: 'rsww-btn rsww-btn--danger', onClick: () => control('cancel') }, '确认取消')
|
|
296
|
+
: React.createElement('button', { className: 'rsww-btn rsww-btn--danger', onClick: () => setArmed(true) }, '取消')),
|
|
297
|
+
verdictButtons,
|
|
298
|
+
resumed && run.status === 'paused' ? React.createElement('div', { className: 'rsww-row' },
|
|
299
|
+
React.createElement('span', { className: 'rsww-note' }, '已恢复,待代理推进,请在会话中发言唤醒')) : null,
|
|
300
|
+
React.createElement('div', { className: 'rsww-row' },
|
|
301
|
+
React.createElement('input', {
|
|
302
|
+
className: 'rsww-input rsww-input--wide', value: text, placeholder: '追加消息(运行中入队,下一步进入编排)',
|
|
303
|
+
onChange: (e) => setText(e.target.value),
|
|
304
|
+
onKeyDown: (e) => { if (e.key === 'Enter') send() },
|
|
305
|
+
}),
|
|
306
|
+
React.createElement('button', { className: 'rsww-btn', onClick: send, disabled: !text.trim() }, '发送'),
|
|
307
|
+
React.createElement(Switch, { checked: inject, onChange: (e) => setInject(e.target.checked), label: '纠偏注入', ariaLabel: '纠偏注入' })),
|
|
308
|
+
error ? React.createElement('span', { className: 'rsww-error' }, error) : null)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 单 run 卡片:详情自轮询(运行中 2s;落定仅挂载一次);终态卡内嵌重跑/续跑/删除操作
|
|
312
|
+
function RunCard({ run, onChanged }) {
|
|
313
|
+
const [detail, setDetail] = useState(null)
|
|
314
|
+
const [fetchError, setFetchError] = useState(false)
|
|
315
|
+
const live = run.status === 'running' || run.status === 'paused' || run.status === 'waiting_approval'
|
|
316
|
+
const reload = useCallback(() => request('run?id=' + encodeURIComponent(run.runId)).then((outcome) => {
|
|
317
|
+
if (outcome.ok) { setDetail(outcome.data); setFetchError(false) } else setFetchError(true)
|
|
318
|
+
}), [run.runId])
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
let alive = true
|
|
321
|
+
const load = () => reload().then(() => { if (!alive) return })
|
|
322
|
+
load()
|
|
323
|
+
const timer = live ? setInterval(load, LIVE_REFRESH_MS) : null
|
|
324
|
+
return () => { alive = false; if (timer) clearInterval(timer) }
|
|
325
|
+
}, [reload, live])
|
|
326
|
+
const view = detail || run
|
|
327
|
+
const queued = Array.isArray(view.queued) ? view.queued : []
|
|
328
|
+
const warnings = Array.isArray(view.warnings) ? view.warnings : []
|
|
329
|
+
const stepEvents = []
|
|
330
|
+
const steps = view.stepsTrace || {}
|
|
331
|
+
for (const [stepId, byInstance] of Object.entries(steps)) {
|
|
332
|
+
for (const [instance, events] of Object.entries(byInstance || {})) {
|
|
333
|
+
for (const e of events || []) stepEvents.push({ stepId, instance, ...e })
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
stepEvents.sort((a, b) => (a.at || 0) - (b.at || 0))
|
|
337
|
+
const requestText = String(view.request || '').split(/<\/?system-reminder>/i)[0].trim() || '(见完整记录)'
|
|
338
|
+
const settled = TERMINAL_STATES.includes(view.status)
|
|
339
|
+
// v5:待审批摘要(waiting 负载:审批步/目标/要点口径/轮次);plan.source 规划标识;warnings 徽标悬浮全文
|
|
340
|
+
const waiting = view.waiting && typeof view.waiting === 'object' ? view.waiting : null
|
|
341
|
+
return React.createElement('div', { className: 'rsww-card' },
|
|
342
|
+
React.createElement('div', { className: 'rsww-row' },
|
|
343
|
+
React.createElement(StatusBadge, { status: view.status }),
|
|
344
|
+
view.templateId ? React.createElement(Badge, { tone: 'mute' }, view.templateId) : null,
|
|
345
|
+
view.plan && view.plan.source === 'model' ? React.createElement(Badge, { tone: 'business' }, '模型规划') : null,
|
|
346
|
+
view.plan && view.plan.source === 'fallback' ? React.createElement(Badge, { tone: 'mute' }, '模板全序') : null,
|
|
347
|
+
warnings.length ? React.createElement('span', { title: warnings.map((w) => (w && w.message) || String(w)).join('\n') },
|
|
348
|
+
React.createElement(Badge, { tone: 'warn' }, '警告 ' + warnings.length)) : null,
|
|
349
|
+
React.createElement('span', { className: 'rsww-req', title: view.request }, requestText),
|
|
350
|
+
React.createElement('span', { className: 'rsww-text' }, relativeTime(view.updatedAt || view.createdAt))),
|
|
351
|
+
waiting ? React.createElement('div', { className: 'rsww-section' },
|
|
352
|
+
React.createElement('span', { className: 'rsww-label' }, '待审批 · ' + (waiting.stepId || '-')),
|
|
353
|
+
React.createElement('span', { className: 'rsww-text' },
|
|
354
|
+
'目标:' + ((waiting.target && (waiting.target.label || waiting.target.id)) || waiting.target || '-'),
|
|
355
|
+
typeof waiting.rounds === 'number' ? ' · 第 ' + waiting.rounds + ' 轮' : ''),
|
|
356
|
+
waiting.brief ? React.createElement('span', { className: 'rsww-text' }, '要点:' + waiting.brief) : null,
|
|
357
|
+
waiting.comments ? React.createElement('span', { className: 'rsww-text' }, '上轮意见:' + waiting.comments) : null) : null,
|
|
358
|
+
React.createElement(ControlBar, { run: view }),
|
|
359
|
+
queued.length ? React.createElement('div', { className: 'rsww-section' },
|
|
360
|
+
React.createElement('span', { className: 'rsww-label' }, '排队消息'),
|
|
361
|
+
queued.map((q, i) => React.createElement('span', { className: 'rsww-text', key: i }, '[' + (i + 1) + '] ' + q))) : null,
|
|
362
|
+
stepEvents.length ? React.createElement('div', { className: 'rsww-section' },
|
|
363
|
+
React.createElement('span', { className: 'rsww-label' }, '批次节点流'),
|
|
364
|
+
React.createElement('ul', { className: 'rsww-steps' },
|
|
365
|
+
stepEvents.map((e, i) => React.createElement(StreamRow, { key: i, ...e })))) : null,
|
|
366
|
+
settled && view.summary ? React.createElement('div', { className: 'rsww-section' },
|
|
367
|
+
React.createElement('span', { className: 'rsww-label' }, '结论'),
|
|
368
|
+
React.createElement('span', { className: 'rsww-text' }, view.summary),
|
|
369
|
+
view.plan ? React.createElement('span', { className: 'rsww-note' },
|
|
370
|
+
' · ' + (view.plan.source === 'model' ? '模型规划' : '模板全序')) : null) : null,
|
|
371
|
+
settled && detail ? React.createElement(DetailActions, { detail, onReload: reload, onRemoved: onChanged }) : null,
|
|
372
|
+
fetchError ? React.createElement('span', { className: 'rsww-error' }, '详情拉取失败,下轮自动重试') : null)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function FlowView({ getSessionId }) {
|
|
376
|
+
const [runs, setRuns] = useState(null)
|
|
377
|
+
const [error, setError] = useState('')
|
|
378
|
+
const reload = useCallback(async () => {
|
|
379
|
+
const outcome = await request('runs')
|
|
380
|
+
if (outcome.ok) { setRuns(outcome.data.runs || []); setError('') } else setError(outcome.error)
|
|
381
|
+
}, [])
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
reload()
|
|
384
|
+
const timer = setInterval(reload, REFRESH_MS)
|
|
385
|
+
return () => clearInterval(timer)
|
|
386
|
+
}, [reload])
|
|
387
|
+
if (error && !runs) return React.createElement('div', { className: 'rsww-flow' }, React.createElement('span', { className: 'rsww-error' }, error))
|
|
388
|
+
// 只呈现本会话的 run:页签语义=本会话编排在场;跨会话检索交给宿主会话搜索
|
|
389
|
+
const mine = (runs || []).filter((r) => r.sessionId === getSessionId())
|
|
390
|
+
const running = mine.filter((r) => r.status === 'running' || r.status === 'paused' || r.status === 'waiting_approval')
|
|
391
|
+
const settled = mine.filter((r) => !running.includes(r)).slice(0, SETTLED_COUNT)
|
|
392
|
+
const group = (title, count, items) => React.createElement('div', { className: 'rsww-group' },
|
|
393
|
+
React.createElement('div', { className: 'rsww-group__head' },
|
|
394
|
+
React.createElement('span', { className: 'rsww-group__title' }, title),
|
|
395
|
+
React.createElement('span', { className: 'rsww-group__count' }, count)),
|
|
396
|
+
items.map((run) => React.createElement(RunCard, { key: run.runId, run, onChanged: reload })))
|
|
397
|
+
return React.createElement('div', { className: 'rsww-flow' },
|
|
398
|
+
React.createElement('style', { 'data-plugin': '@mzzsfy/dsh-rs-workflow', dangerouslySetInnerHTML: { __html: CSS } }),
|
|
399
|
+
error ? React.createElement('span', { className: 'rsww-error' }, error) : null,
|
|
400
|
+
mine.length === 0 ? EmptyState({ title: '本会话还没有运行记录', hint: '在若水模式下发送需求后,编排进度会实时出现在这里。' }) : null,
|
|
401
|
+
running.length ? group('进行中', running.length + ' 次', running) : null,
|
|
402
|
+
settled.length ? group('已落定', '近 ' + settled.length + ' 次', settled) : null)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── 设置页共享件:pill 组/行编辑/钳制(gui-center/gui-editor/gui-config 同源) ──
|
|
406
|
+
const h = React.createElement
|
|
407
|
+
const SLOT_KEYS = ['planner', 'executor', 'reviewer', 'executor-loop', 'reviewer-approve', 'executor-escalate']
|
|
408
|
+
const BUDGET_KEYS = ['maxStepFail', 'approveRounds', 'escalateLimit']
|
|
409
|
+
const BUDGET_MIN = 1
|
|
410
|
+
const BUDGET_MAX = 10
|
|
411
|
+
|
|
412
|
+
function PillGroup({ value, options, onChange, tab, ariaLabel }) {
|
|
413
|
+
return h('div', { className: 'rsww-pills', role: tab ? 'tablist' : undefined, 'aria-label': ariaLabel },
|
|
414
|
+
options.map((o) => h('button', {
|
|
415
|
+
key: o.key, type: 'button', role: tab ? 'tab' : undefined,
|
|
416
|
+
className: 'rsww-pill' + (value === o.key ? ' rsww-pill--on' : ''),
|
|
417
|
+
'aria-pressed': value === o.key, onClick: () => onChange(o.key),
|
|
418
|
+
}, o.label)))
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function clampRange(v, min, max) {
|
|
422
|
+
const n = Math.round(Number(v))
|
|
423
|
+
if (!Number.isFinite(n)) return min
|
|
424
|
+
return Math.min(max, Math.max(min, n))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 键值行编辑(顶层 inputs/outputs/flow input/续跑 inputs 同款);空名行在序列化侧剔除
|
|
428
|
+
function KvRows({ rows, onChange, kPh, vPh, kMono }) {
|
|
429
|
+
const set = (i, part, v) => onChange(rows.map((r, j) => (j === i ? { k: r.k, v: r.v, [part]: v } : r)))
|
|
430
|
+
return h('div', { className: 'rsww-kv' },
|
|
431
|
+
rows.map((r, i) => h('div', { className: 'rsww-kv__row', key: i },
|
|
432
|
+
h('input', { className: 'rsww-input' + (kMono ? ' rsww-mono' : ''), value: r.k, placeholder: kPh, onChange: (e) => set(i, 'k', e.target.value) }),
|
|
433
|
+
h('input', { className: 'rsww-input rsww-input--wide', value: r.v, placeholder: vPh, onChange: (e) => set(i, 'v', e.target.value) }),
|
|
434
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: () => onChange(rows.filter((_, j) => j !== i)) }, '删除'))),
|
|
435
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: () => onChange(rows.concat({ k: '', v: '' })) }, '添加一行'))
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function StrRows({ items, onChange, ph, min }) {
|
|
439
|
+
return h('div', { className: 'rsww-kv' },
|
|
440
|
+
items.map((v, i) => h('div', { className: 'rsww-kv__row', key: i },
|
|
441
|
+
h('input', { className: 'rsww-input rsww-mono rsww-input--wide', value: v, placeholder: ph, onChange: (e) => onChange(items.map((x, j) => (j === i ? e.target.value : x))) }),
|
|
442
|
+
h('button', { className: 'rsww-btn', type: 'button', disabled: min !== undefined && items.length <= min, onClick: () => onChange(items.filter((_, j) => j !== i)) }, '删除'))),
|
|
443
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: () => onChange(items.concat('')) }, '添加一行'))
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 危险操作两段确认(armed 超时自动解除)
|
|
447
|
+
function ArmedButton({ label, confirmLabel, onConfirm, disabled }) {
|
|
448
|
+
const [armed, setArmed] = useState(false)
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (!armed) return undefined
|
|
451
|
+
const t = setTimeout(() => setArmed(false), ARMED_TIMEOUT_MS)
|
|
452
|
+
return () => clearTimeout(t)
|
|
453
|
+
}, [armed])
|
|
454
|
+
return h('button', { className: 'rsww-btn rsww-btn--danger', type: 'button', disabled: disabled || undefined,
|
|
455
|
+
onClick: () => { if (armed) { setArmed(false); onConfirm() } else setArmed(true) } }, armed ? confirmLabel : label)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── run 终态操作与重跑/续跑选择器(gui-session 卡片内使用) ──
|
|
459
|
+
const STEP_TONES = { pending: 'mute', running: 'business', done: 'success', failed: 'error', skipped: 'mute' }
|
|
460
|
+
const STEP_STATUS_LABELS = { pending: '待执行', running: '执行中', done: '已完成', failed: '失败', skipped: '已跳过' }
|
|
461
|
+
|
|
462
|
+
function StepBadge({ status }) {
|
|
463
|
+
return h(Badge, { tone: STEP_TONES[status] || 'mute' }, STEP_STATUS_LABELS[status] || status || '-')
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function ResumePicker({ detail, init, onClose, onResumed }) {
|
|
467
|
+
const macroAll = (detail.state && detail.state.steps) || {}
|
|
468
|
+
const ids = Object.keys(macroAll)
|
|
469
|
+
const breakpoint = ids.find((id) => macroAll[id] && macroAll[id].status !== 'done') || ''
|
|
470
|
+
const radioName = 'rsww-resume-' + detail.runId
|
|
471
|
+
const [sel, setSel] = useState(init === 'resume' ? breakpoint : '')
|
|
472
|
+
const [rows, setRows] = useState(() => Object.entries(detail.inputs || {}).map(([k, v]) => ({ k, v: String(v) })))
|
|
473
|
+
const [busy, setBusy] = useState(false)
|
|
474
|
+
const [error, setError] = useState('')
|
|
475
|
+
const [newRunId, setNewRunId] = useState('')
|
|
476
|
+
const confirm = async () => {
|
|
477
|
+
setBusy(true); setError('')
|
|
478
|
+
const inputs = {}
|
|
479
|
+
for (const r of rows) if (r.k.trim() !== '' && r.v.trim() !== '') inputs[r.k.trim()] = r.v
|
|
480
|
+
const body = { runId: detail.runId }
|
|
481
|
+
if (sel !== '') body.fromStepId = sel
|
|
482
|
+
if (Object.keys(inputs).length > 0) body.inputs = inputs
|
|
483
|
+
const o = await post('resume-from', body)
|
|
484
|
+
setBusy(false)
|
|
485
|
+
if (!o.ok) { setError(o.error); return }
|
|
486
|
+
setNewRunId((o.data && o.data.runId) || '')
|
|
487
|
+
onResumed()
|
|
488
|
+
}
|
|
489
|
+
return h('div', { className: 'rsww-card rsww-stack' },
|
|
490
|
+
h('div', { className: 'rsww-row' },
|
|
491
|
+
h('span', { className: 'rsww-label' }, '重跑/续跑'),
|
|
492
|
+
h('span', { className: 'rsww-note' }, 'inputs 留空 = 不覆盖;成功后生成新 run,已完成步骤作种子')),
|
|
493
|
+
h('label', { className: 'rsww-tree__row' },
|
|
494
|
+
h('input', { type: 'radio', name: radioName, checked: sel === '', onChange: () => setSel('') }),
|
|
495
|
+
h('span', { className: 'rsww-text' }, '从头重跑(不携带 fromStepId)')),
|
|
496
|
+
ids.map((id) => h('label', { className: 'rsww-tree__row', key: id },
|
|
497
|
+
h('input', { type: 'radio', name: radioName, checked: sel === id, onChange: () => setSel(id) }),
|
|
498
|
+
h('span', { className: 'rsww-mono' }, id),
|
|
499
|
+
h(StepBadge, { status: macroAll[id] && macroAll[id].status }),
|
|
500
|
+
id === breakpoint ? h('span', { className: 'rsww-note' }, '断点') : null)),
|
|
501
|
+
h(KvRows, { rows, onChange: setRows, kPh: 'input 名', vPh: '值(留空该行不覆盖)', kMono: true }),
|
|
502
|
+
error ? h('span', { className: 'rsww-error' }, error) : null,
|
|
503
|
+
newRunId ? h('span', { className: 'rsww-text' }, '新 run:', h('span', { className: 'rsww-mono' }, ' ' + newRunId)) : null,
|
|
504
|
+
h('div', { className: 'rsww-row' },
|
|
505
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: confirm, disabled: busy }, busy ? '提交中' : '确认'),
|
|
506
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: onClose }, '关闭')))
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// 终态操作行:重跑/断点续跑/删除(RunCard 落定卡内嵌;删除成功经 onRemoved 通知列表刷新)
|
|
510
|
+
function DetailActions({ detail, onReload, onRemoved }) {
|
|
511
|
+
const [picker, setPicker] = useState('')
|
|
512
|
+
const [error, setError] = useState('')
|
|
513
|
+
const remove = async () => {
|
|
514
|
+
setError('')
|
|
515
|
+
const o = await post('run-remove', { runId: detail.runId })
|
|
516
|
+
if (!o.ok) { setError(/404/.test(o.error) ? '宿主不支持删除路由(404),记录保留' : o.error); return }
|
|
517
|
+
onRemoved()
|
|
518
|
+
}
|
|
519
|
+
return h('div', { className: 'rsww-section' },
|
|
520
|
+
h('div', { className: 'rsww-row' },
|
|
521
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: () => setPicker(picker === 'rerun' ? '' : 'rerun') }, '重跑'),
|
|
522
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: () => setPicker(picker === 'resume' ? '' : 'resume') }, '断点续跑'),
|
|
523
|
+
h(ArmedButton, { label: '删除', confirmLabel: '确认删除', onConfirm: remove })),
|
|
524
|
+
error ? h('span', { className: 'rsww-error' }, error) : null,
|
|
525
|
+
picker !== '' ? h(ResumePicker, { detail, init: picker, onClose: () => setPicker(''), onResumed: onReload }) : null)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ── gui-editor 模板编辑器:权威态=编辑模型,文本派生+显式应用(host dryRun 权威校验) ──
|
|
529
|
+
const TYPE_OPTIONS = [{ key: 'ai', label: '普通' }, { key: 'approve', label: 'approve' }, { key: 'flow', label: 'flow' }]
|
|
530
|
+
const MODE_OPTIONS = [{ key: 'sequential', label: '顺序' }, { key: 'parallel', label: '并行' }]
|
|
531
|
+
const TEMPLATE_ID_RE = /^[a-z][a-z0-9-]*$/
|
|
532
|
+
const asStr = (v) => (typeof v === 'string' ? v : '')
|
|
533
|
+
const asArr = (v) => (Array.isArray(v) ? v.slice() : [])
|
|
534
|
+
const objToRows = (v) => Object.entries(v && typeof v === 'object' && !Array.isArray(v) ? v : {}).map(([k, x]) => ({ k, v: typeof x === 'string' ? x : JSON.stringify(x) }))
|
|
535
|
+
const rowsToObj = (rows) => Object.fromEntries((rows || []).filter((r) => r.k.trim() !== '').map((r) => [r.k.trim(), r.v]))
|
|
536
|
+
const outNamesOf = (s) => s.outputs.map((r) => r.k.trim()).filter((n) => n !== '')
|
|
537
|
+
|
|
538
|
+
const newStep = () => ({ id: '', label: '', type: 'ai', slot: '', prompt: '', outputs: [], listOutputs: [], load: [], after: [], maxFail: '', for_each: '', mode: '', target: '', rounds: '', onExhausted: '', flow: '', input: [] })
|
|
539
|
+
|
|
540
|
+
function normalizeStep(s) {
|
|
541
|
+
const base = newStep()
|
|
542
|
+
if (!s || typeof s !== 'object') return base
|
|
543
|
+
return {
|
|
544
|
+
...base,
|
|
545
|
+
id: asStr(s.id), label: asStr(s.label),
|
|
546
|
+
type: ['ai', 'approve', 'flow'].includes(s.type) ? s.type : 'ai',
|
|
547
|
+
slot: asStr(s.slot), prompt: asStr(s.prompt),
|
|
548
|
+
outputs: objToRows(s.outputs), listOutputs: asArr(s.listOutputs), load: asArr(s.load), after: asArr(s.after),
|
|
549
|
+
maxFail: Number.isInteger(s.maxFail) ? s.maxFail : '',
|
|
550
|
+
for_each: asStr(s.for_each), mode: asStr(s.mode),
|
|
551
|
+
target: asStr(s.target), rounds: Number.isInteger(s.rounds) ? s.rounds : '',
|
|
552
|
+
onExhausted: asStr(s.onExhausted), flow: asStr(s.flow), input: objToRows(s.input),
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// 序列化裁剪:空值省略;type=ai 省略(type 缺省即 ai);enabled 不入文(校验器禁)
|
|
557
|
+
function stepOut(s) {
|
|
558
|
+
const o = { id: s.id }
|
|
559
|
+
if (s.label !== '') o.label = s.label
|
|
560
|
+
if (s.type !== 'ai') o.type = s.type
|
|
561
|
+
if (s.slot !== '') o.slot = s.slot
|
|
562
|
+
if (s.type !== 'flow' && s.prompt !== '') o.prompt = s.prompt
|
|
563
|
+
if (s.after.length > 0) o.after = s.after
|
|
564
|
+
if (s.type === 'ai') {
|
|
565
|
+
if (outNamesOf(s).length > 0) o.outputs = rowsToObj(s.outputs)
|
|
566
|
+
if (s.listOutputs.length > 0) o.listOutputs = s.listOutputs
|
|
567
|
+
if (s.load.length > 0) o.load = s.load
|
|
568
|
+
if (s.maxFail !== '') o.maxFail = Number(s.maxFail)
|
|
569
|
+
if (s.for_each !== '') { o.for_each = s.for_each; o.mode = s.mode === 'parallel' ? 'parallel' : 'sequential' }
|
|
570
|
+
}
|
|
571
|
+
if (s.type === 'approve') {
|
|
572
|
+
if (s.target !== '') o.target = s.target
|
|
573
|
+
if (s.rounds !== '') o.rounds = Number(s.rounds)
|
|
574
|
+
if (s.onExhausted !== '') o.onExhausted = s.onExhausted
|
|
575
|
+
}
|
|
576
|
+
if (s.type === 'flow') {
|
|
577
|
+
if (s.flow !== '') o.flow = s.flow
|
|
578
|
+
if (s.input.some((r) => r.k.trim() !== '')) o.input = rowsToObj(s.input)
|
|
579
|
+
}
|
|
580
|
+
return o
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function serializeModel(m) {
|
|
584
|
+
return JSON.stringify({
|
|
585
|
+
id: m.id,
|
|
586
|
+
...(m.label !== '' ? { label: m.label } : {}),
|
|
587
|
+
...(m.description !== '' ? { description: m.description } : {}),
|
|
588
|
+
...(m.inputs.some((r) => r.k.trim() !== '') ? { inputs: rowsToObj(m.inputs) } : {}),
|
|
589
|
+
steps: m.steps.map(stepOut),
|
|
590
|
+
}, null, 2)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 文本可 JSON.parse 即建全模型;否则文本模式(synced=false,结构表单锁定,应用后回填)。
|
|
594
|
+
// 副本条目(id 空)仅继承结构,meta(id/label/description)由用户重填,防覆盖原模板
|
|
595
|
+
function modelFromEntry(entry) {
|
|
596
|
+
let parsed = null
|
|
597
|
+
try { parsed = JSON.parse(entry.json) } catch { parsed = null }
|
|
598
|
+
const meta = { id: entry.id || '', label: asStr(entry.label) || entry.id || '', description: asStr(entry.description), enabled: entry.enabled !== false }
|
|
599
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
600
|
+
return { model: { ...meta, inputs: [], steps: [] }, text: entry.json, synced: false }
|
|
601
|
+
}
|
|
602
|
+
const model = {
|
|
603
|
+
id: meta.id !== '' ? (asStr(parsed.id) || meta.id) : '',
|
|
604
|
+
label: meta.id !== '' ? (asStr(parsed.label) || meta.label) : meta.label,
|
|
605
|
+
description: meta.id !== '' ? (asStr(parsed.description) || meta.description) : meta.description,
|
|
606
|
+
enabled: meta.enabled,
|
|
607
|
+
inputs: objToRows(parsed.inputs),
|
|
608
|
+
steps: asArr(parsed.steps).map(normalizeStep),
|
|
609
|
+
}
|
|
610
|
+
return { model, text: serializeModel(model), synced: true }
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 前端即时校验(保存/应用前);step: 空 id 的错误平铺展示
|
|
614
|
+
function clientErrors(m) {
|
|
615
|
+
const errs = []
|
|
616
|
+
if (!TEMPLATE_ID_RE.test(m.id || '')) errs.push({ target: 'top:id', message: 'id 必填且匹配 ^[a-z][a-z0-9-]*$' })
|
|
617
|
+
if (m.steps.length === 0) errs.push({ target: 'top:steps', message: '至少一个步骤' })
|
|
618
|
+
const seen = new Set()
|
|
619
|
+
for (const s of m.steps) {
|
|
620
|
+
const at = s.id !== '' ? 'step:' + s.id : 'step:'
|
|
621
|
+
if (!TEMPLATE_ID_RE.test(s.id || '')) errs.push({ target: at, message: '步骤 id 必填且匹配 ^[a-z][a-z0-9-]*$' })
|
|
622
|
+
else if (seen.has(s.id)) errs.push({ target: at, message: '步骤 id 重复:' + s.id })
|
|
623
|
+
if (s.id !== '') seen.add(s.id)
|
|
624
|
+
if (s.type !== 'flow' && s.prompt.trim() === '') errs.push({ target: at, message: (s.id || '未命名步骤') + ':prompt 必填' })
|
|
625
|
+
if (s.type === 'ai' && outNamesOf(s).length === 0) errs.push({ target: at, message: (s.id || '未命名步骤') + ':outputs 产出契约必填' })
|
|
626
|
+
if (s.type === 'approve' && s.target === '') errs.push({ target: at, message: (s.id || '未命名步骤') + ':approve 步骤 target 必填' })
|
|
627
|
+
if (s.type === 'flow' && s.flow.trim() === '') errs.push({ target: at, message: (s.id || '未命名步骤') + ':flow 必填' })
|
|
628
|
+
}
|
|
629
|
+
return errs
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const stripForType = (s) => {
|
|
633
|
+
if (s.type === 'approve') return { ...s, outputs: [], listOutputs: [], load: [], for_each: '', mode: '', flow: '', input: [] }
|
|
634
|
+
if (s.type === 'flow') return { ...s, prompt: '', outputs: [], listOutputs: [], load: [], for_each: '', mode: '', target: '', rounds: '', onExhausted: '' }
|
|
635
|
+
return { ...s, target: '', rounds: '', onExhausted: '', flow: '', input: [] }
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function MetaForm({ model, update, synced, topErr }) {
|
|
639
|
+
const idErrs = topErr('id')
|
|
640
|
+
const field = (label, key, mono, disabled) => h('div', { className: 'rsww-kv__row' },
|
|
641
|
+
h('span', { className: 'rsww-label', style: { width: 64, flex: 'none' } }, label),
|
|
642
|
+
h('input', { className: 'rsww-input' + (mono ? ' rsww-mono' : '') + ' rsww-input--wide' + (key === 'id' && idErrs.length > 0 ? ' rsww-input--err' : ''),
|
|
643
|
+
value: model[key], disabled: disabled || undefined, onChange: (e) => update((m) => ({ ...m, [key]: e.target.value })) }))
|
|
644
|
+
return h('div', { className: 'rsww-kv' },
|
|
645
|
+
field('id', 'id', true, false),
|
|
646
|
+
idErrs.map((e, i) => h('span', { className: 'rsww-error', key: i }, e.message)),
|
|
647
|
+
field('显示名', 'label', false, false),
|
|
648
|
+
field('说明', 'description', false, false),
|
|
649
|
+
h(Switch, { checked: model.enabled, onChange: (e) => update((m) => ({ ...m, enabled: e.target.checked })), label: '启用(禁用后不可创建、重跑不可选)', ariaLabel: '启用模板' }),
|
|
650
|
+
synced ? null : h('span', { className: 'rsww-note' }, '此模板文本含 JSON5 扩展语法,结构表单锁定:请在文本视图点「应用」,经 host 校验通过后回填。'))
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function InputsForm({ model, update, topErr }) {
|
|
654
|
+
const errs = topErr('inputs')
|
|
655
|
+
return h('div', { className: 'rsww-section' },
|
|
656
|
+
h('span', { className: 'rsww-label' }, '顶层 inputs(prompt 内 {input.name} 引用)'),
|
|
657
|
+
errs.map((e, i) => h('span', { className: 'rsww-error', key: i }, e.message)),
|
|
658
|
+
h(KvRows, { rows: model.inputs, onChange: (rows) => update((m) => ({ ...m, inputs: rows })), kPh: 'name', vPh: '说明', kMono: true }))
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function ListOutSelect({ step, onChange }) {
|
|
662
|
+
const names = outNamesOf(step)
|
|
663
|
+
if (names.length === 0) return h('span', { className: 'rsww-note' }, 'listOutputs:先声明 outputs 后可选列表字段')
|
|
664
|
+
const toggle = (n) => onChange({ ...step, listOutputs: step.listOutputs.includes(n) ? step.listOutputs.filter((x) => x !== n) : step.listOutputs.concat(n) })
|
|
665
|
+
return h('div', { className: 'rsww-section' },
|
|
666
|
+
h('span', { className: 'rsww-label' }, 'listOutputs(列表型产出,供 for_each 引用)'),
|
|
667
|
+
names.map((n) => h('label', { className: 'rsww-switch', key: n },
|
|
668
|
+
h('input', { type: 'checkbox', checked: step.listOutputs.includes(n), onChange: () => toggle(n), 'aria-label': n }),
|
|
669
|
+
h('span', { className: 'rsww-switch__track' }, h('span', { className: 'rsww-switch__thumb' })),
|
|
670
|
+
h('span', { className: 'rsww-mono' }, n))))
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function AfterSelect({ step, otherIds, onChange }) {
|
|
674
|
+
if (otherIds.length === 0) return h('span', { className: 'rsww-note' }, 'after:暂无其他步骤可选')
|
|
675
|
+
const toggle = (id) => onChange({ ...step, after: step.after.includes(id) ? step.after.filter((x) => x !== id) : step.after.concat(id) })
|
|
676
|
+
return h('div', { className: 'rsww-section' },
|
|
677
|
+
h('span', { className: 'rsww-label' }, 'after(前置步骤;缺省=文档序前一步)'),
|
|
678
|
+
otherIds.map((id) => h('label', { className: 'rsww-switch', key: id },
|
|
679
|
+
h('input', { type: 'checkbox', checked: step.after.includes(id), onChange: () => toggle(id), 'aria-label': id }),
|
|
680
|
+
h('span', { className: 'rsww-switch__track' }, h('span', { className: 'rsww-switch__thumb' })),
|
|
681
|
+
h('span', { className: 'rsww-mono' }, id))))
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function ForEachRow({ step, sources, onChange }) {
|
|
685
|
+
return h('div', { className: 'rsww-section' },
|
|
686
|
+
h('div', { className: 'rsww-kv__row' },
|
|
687
|
+
h('span', { className: 'rsww-label', style: { width: 64, flex: 'none' } }, 'for_each'),
|
|
688
|
+
h('select', { className: 'rsww-input', value: step.for_each, onChange: (e) => onChange({ ...step, for_each: e.target.value }) },
|
|
689
|
+
h('option', { value: '' }, '(不循环)'),
|
|
690
|
+
sources.map((o) => h('option', { key: o, value: o }, o)))),
|
|
691
|
+
step.for_each !== '' ? h('div', { className: 'rsww-kv__row' },
|
|
692
|
+
h('span', { className: 'rsww-label', style: { width: 64, flex: 'none' } }, 'mode'),
|
|
693
|
+
h(PillGroup, { value: step.mode === 'parallel' ? 'parallel' : 'sequential', options: MODE_OPTIONS, onChange: (v) => onChange({ ...step, mode: v }), ariaLabel: '循环模式' })) : null)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function ApproveForm({ step, otherIds, onChange }) {
|
|
697
|
+
return h('div', { className: 'rsww-stack' },
|
|
698
|
+
h('div', { className: 'rsww-kv__row' },
|
|
699
|
+
h('span', { className: 'rsww-label', style: { width: 64, flex: 'none' } }, 'target'),
|
|
700
|
+
h('select', { className: 'rsww-input', value: step.target, onChange: (e) => onChange({ ...step, target: e.target.value }) },
|
|
701
|
+
h('option', { value: '' }, '(必选:被审批步骤)'),
|
|
702
|
+
otherIds.map((id) => h('option', { key: id, value: id }, id)))),
|
|
703
|
+
h('div', { className: 'rsww-kv__row' },
|
|
704
|
+
h('span', { className: 'rsww-label', style: { width: 64, flex: 'none' } }, 'rounds'),
|
|
705
|
+
h('input', { className: 'rsww-input', type: 'number', value: step.rounds, placeholder: '留空=预算 approveRounds',
|
|
706
|
+
onChange: (e) => onChange({ ...step, rounds: e.target.value === '' ? '' : Number(e.target.value) }),
|
|
707
|
+
onBlur: () => { if (step.rounds !== '') onChange({ ...step, rounds: clampRange(step.rounds, BUDGET_MIN, BUDGET_MAX) }) } })),
|
|
708
|
+
h('div', { className: 'rsww-kv__row' },
|
|
709
|
+
h('span', { className: 'rsww-label', style: { width: 64, flex: 'none' } }, 'onExhausted'),
|
|
710
|
+
h('select', { className: 'rsww-input', value: step.onExhausted, onChange: (e) => onChange({ ...step, onExhausted: e.target.value }) },
|
|
711
|
+
h('option', { value: '' }, '(缺省 blocked)'),
|
|
712
|
+
h('option', { value: 'blocked' }, 'blocked'),
|
|
713
|
+
otherIds.map((id) => h('option', { key: id, value: id }, id)))))
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function FlowForm({ step, templates, onChange }) {
|
|
717
|
+
return h('div', { className: 'rsww-stack' },
|
|
718
|
+
h('div', { className: 'rsww-kv__row' },
|
|
719
|
+
h('span', { className: 'rsww-label', style: { width: 64, flex: 'none' } }, 'flow'),
|
|
720
|
+
h('input', { className: 'rsww-input rsww-mono rsww-input--wide', list: 'rsww-tmpl-ids', value: step.flow,
|
|
721
|
+
placeholder: '模板 id 或 {步骤.产出}', onChange: (e) => onChange({ ...step, flow: e.target.value }) })),
|
|
722
|
+
h('div', { className: 'rsww-section' },
|
|
723
|
+
h('span', { className: 'rsww-label' }, 'input(子流程入参,单占位符引用)'),
|
|
724
|
+
h(KvRows, { rows: step.input, onChange: (rows) => onChange({ ...step, input: rows }), kPh: '入参名', vPh: '{步骤.产出}', kMono: true })))
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function StepCard({ step, index, model, errors, onChange, onRemove }) {
|
|
728
|
+
const [openSelf, setOpenSelf] = useState(false)
|
|
729
|
+
const [adv, setAdv] = useState(false)
|
|
730
|
+
const open = openSelf || errors.length > 0
|
|
731
|
+
const otherIds = [...new Set(model.steps.map((s) => s.id).filter((id) => id !== '' && id !== step.id))]
|
|
732
|
+
const sources = []
|
|
733
|
+
for (const s of model.steps) {
|
|
734
|
+
if (s.id !== '' && s.id !== step.id && s.type === 'ai') for (const out of s.listOutputs) sources.push(s.id + '.' + out)
|
|
735
|
+
}
|
|
736
|
+
const row = (label, control) => h('div', { className: 'rsww-kv__row' },
|
|
737
|
+
h('span', { className: 'rsww-label', style: { width: 64, flex: 'none' } }, label), control)
|
|
738
|
+
return h('div', { className: 'rsww-card' + (errors.length > 0 ? ' rsww-card--err' : '') },
|
|
739
|
+
h('div', { className: 'rsww-row' },
|
|
740
|
+
h('button', { className: 'rsww-btn', type: 'button', style: { padding: '2px 8px' }, onClick: () => setOpenSelf(!openSelf) },
|
|
741
|
+
(open ? '收起 ' : '展开 ') + (step.id !== '' ? step.id : '#' + (index + 1))),
|
|
742
|
+
h(Badge, { tone: 'mute' }, (TYPE_OPTIONS.find((t) => t.key === step.type) || TYPE_OPTIONS[0]).label),
|
|
743
|
+
h('span', { style: { flex: 1 } }),
|
|
744
|
+
h(ArmedButton, { label: '删除步骤', confirmLabel: '确认删除', onConfirm: onRemove })),
|
|
745
|
+
errors.map((e, i) => h('span', { className: 'rsww-error', key: i }, e.message)),
|
|
746
|
+
!open ? null : h('div', { className: 'rsww-stack' },
|
|
747
|
+
row('类型', h(PillGroup, { value: step.type, options: TYPE_OPTIONS, onChange: (t) => onChange(stripForType({ ...step, type: t })), ariaLabel: '步骤类型' })),
|
|
748
|
+
row('id', h('input', { className: 'rsww-input rsww-mono rsww-input--wide', value: step.id, placeholder: '步骤 id(小写字母开头)', onChange: (e) => onChange({ ...step, id: e.target.value }) })),
|
|
749
|
+
row('显示名', h('input', { className: 'rsww-input rsww-input--wide', value: step.label, placeholder: '可空', onChange: (e) => onChange({ ...step, label: e.target.value }) })),
|
|
750
|
+
step.type !== 'approve' ? row('slot', h('select', { className: 'rsww-input', value: step.slot, onChange: (e) => onChange({ ...step, slot: e.target.value }) },
|
|
751
|
+
h('option', { value: '' }, '(引擎缺省)'), SLOT_KEYS.map((k) => h('option', { key: k, value: k }, k)))) : null,
|
|
752
|
+
step.type !== 'flow' ? row('prompt', h('textarea', { className: 'rsww-textarea', value: step.prompt, rows: 4,
|
|
753
|
+
placeholder: '指令全文;占位符 {request}/{input.x}/{步骤.产出}{item}', onChange: (e) => onChange({ ...step, prompt: e.target.value }) })) : null,
|
|
754
|
+
step.type === 'ai' ? h('div', { className: 'rsww-section' },
|
|
755
|
+
h('span', { className: 'rsww-label' }, 'outputs 产出契约'),
|
|
756
|
+
h(KvRows, { rows: step.outputs, onChange: (rows) => onChange({ ...step, outputs: rows }), kPh: '字段名', vPh: '说明(结构化产出口径)', kMono: true })) : null,
|
|
757
|
+
step.type === 'ai' ? h(ListOutSelect, { step, onChange }) : null,
|
|
758
|
+
step.type === 'approve' ? h(ApproveForm, { step, otherIds, onChange }) : null,
|
|
759
|
+
step.type === 'flow' ? h(FlowForm, { step, onChange }) : null,
|
|
760
|
+
h('div', {},
|
|
761
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: () => setAdv(!adv) }, adv ? '收起高级' : '高级字段(after/for_each/load/maxFail)'),
|
|
762
|
+
!adv ? null : h('div', { className: 'rsww-stack', style: { marginTop: 6 } },
|
|
763
|
+
h(AfterSelect, { step, otherIds, onChange }),
|
|
764
|
+
step.type === 'ai' ? h(ForEachRow, { step, sources, onChange }) : null,
|
|
765
|
+
step.type === 'ai' ? h('div', { className: 'rsww-section' },
|
|
766
|
+
h('span', { className: 'rsww-label' }, 'load 资源'),
|
|
767
|
+
h(StrRows, { items: step.load, onChange: (items) => onChange({ ...step, load: items }), ph: 'skill:name 或 doc:name' })) : null,
|
|
768
|
+
step.type === 'ai' ? row('maxFail', h('input', { className: 'rsww-input', type: 'number', value: step.maxFail, placeholder: '留空=预算 maxStepFail',
|
|
769
|
+
onChange: (e) => onChange({ ...step, maxFail: e.target.value === '' ? '' : Number(e.target.value) }),
|
|
770
|
+
onBlur: () => { if (step.maxFail !== '') onChange({ ...step, maxFail: clampRange(step.maxFail, BUDGET_MIN, BUDGET_MAX) }) } })) : null))))
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function StepsForm({ model, update, allErrors }) {
|
|
774
|
+
const setSteps = (steps) => update((m) => ({ ...m, steps }))
|
|
775
|
+
return h('div', { className: 'rsww-stack' },
|
|
776
|
+
model.steps.map((s, i) => h(StepCard, {
|
|
777
|
+
key: i, step: s, index: i, model,
|
|
778
|
+
errors: allErrors.filter((e) => s.id !== '' && e.target === 'step:' + s.id),
|
|
779
|
+
onChange: (next) => setSteps(model.steps.map((x, j) => (j === i ? next : x))),
|
|
780
|
+
onRemove: () => setSteps(model.steps.filter((_, j) => j !== i)),
|
|
781
|
+
})),
|
|
782
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: () => setSteps(model.steps.concat(newStep())) }, '添加步骤'))
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function Json5Pane({ text, setText, onApply, onValidate, jsonErrs, saving }) {
|
|
786
|
+
return h('div', { className: 'rsww-stack' },
|
|
787
|
+
h('textarea', { className: 'rsww-textarea', value: text, rows: 18, spellCheck: false, onChange: (e) => setText(e.target.value) }),
|
|
788
|
+
h('div', { className: 'rsww-row' },
|
|
789
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: onApply, disabled: saving }, '应用(校验并回填表单)'),
|
|
790
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: onValidate, disabled: saving }, '校验')),
|
|
791
|
+
jsonErrs.map((e, i) => h('span', { className: 'rsww-error', key: i },
|
|
792
|
+
typeof e.line === 'number' ? '行 ' + e.line + ':' + e.message : e.message)))
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function TemplateEditor({ entry, isNew, templates, onClose, onSaved }) {
|
|
796
|
+
const init = isNew
|
|
797
|
+
? { model: { id: '', label: '', description: '', enabled: true, inputs: [], steps: [newStep()] }, text: '', synced: true }
|
|
798
|
+
: modelFromEntry(entry)
|
|
799
|
+
const [model, setModel] = useState(init.model)
|
|
800
|
+
const [text, setText] = useState(() => init.text !== '' ? init.text : serializeModel(init.model))
|
|
801
|
+
const [synced, setSynced] = useState(init.synced)
|
|
802
|
+
const [view, setView] = useState(init.synced ? 'struct' : 'json')
|
|
803
|
+
const [saveErrors, setSaveErrors] = useState([])
|
|
804
|
+
const [saving, setSaving] = useState(false)
|
|
805
|
+
const [note, setNote] = useState('')
|
|
806
|
+
const [filling, setFilling] = useState(false)
|
|
807
|
+
// 文本含 JSON5 扩展语法时 client 不解析(解析权威在 host):经 template 只读路由取 host 解析结果回填表单
|
|
808
|
+
useEffect(() => {
|
|
809
|
+
if (init.synced || isNew) return
|
|
810
|
+
let live = true
|
|
811
|
+
setFilling(true)
|
|
812
|
+
request('template?id=' + encodeURIComponent(entry.id)).then((o) => {
|
|
813
|
+
if (!live) return
|
|
814
|
+
setFilling(false)
|
|
815
|
+
const parsed = o.ok && o.data && o.data.parsed
|
|
816
|
+
if (!parsed || typeof parsed !== 'object') { setNote('结构表单不可用:host 解析失败,可在文本视图编辑并「应用」。'); return }
|
|
817
|
+
const m = {
|
|
818
|
+
id: asStr(parsed.id) || entry.id || '',
|
|
819
|
+
label: asStr(parsed.label), description: asStr(parsed.description),
|
|
820
|
+
enabled: entry.enabled !== false,
|
|
821
|
+
inputs: objToRows(parsed.inputs),
|
|
822
|
+
steps: asArr(parsed.steps).map(normalizeStep),
|
|
823
|
+
}
|
|
824
|
+
setModel(m); setSynced(true); setView('struct')
|
|
825
|
+
setNote('已按 host 解析结果载入结构表单;保存以「应用」后的文本为准。')
|
|
826
|
+
})
|
|
827
|
+
return () => { live = false }
|
|
828
|
+
}, [isNew, entry && entry.id])
|
|
829
|
+
// 未回填(文本模式)时表单编辑不重写文本,json 正文始终权威
|
|
830
|
+
const update = (fn) => {
|
|
831
|
+
if (!synced) { setModel((prev) => fn(prev)); return }
|
|
832
|
+
setModel((prev) => {
|
|
833
|
+
const next = fn(prev)
|
|
834
|
+
setText(serializeModel(next))
|
|
835
|
+
return next
|
|
836
|
+
})
|
|
837
|
+
}
|
|
838
|
+
const errsOf = (o) => (o.data && Array.isArray(o.data.errors) && o.data.errors.length > 0 ? o.data.errors : [{ target: 'json', message: o.error }])
|
|
839
|
+
const allErrors = clientErrors(model).filter(() => synced).concat(saveErrors)
|
|
840
|
+
const topErr = (field) => allErrors.filter((e) => e.target === 'top:' + field)
|
|
841
|
+
const jsonErrs = allErrors.filter((e) => e.target === 'json')
|
|
842
|
+
const flatErrs = allErrors.filter((e) => e.target === 'step:')
|
|
843
|
+
const dryRun = async () => {
|
|
844
|
+
setSaving(true); setNote(''); setSaveErrors([])
|
|
845
|
+
const o = await post('template-save', { id: model.id, label: model.label, description: model.description, enabled: model.enabled, json: text, dryRun: true })
|
|
846
|
+
setSaving(false)
|
|
847
|
+
if (!o.ok) { setSaveErrors(errsOf(o)); return null }
|
|
848
|
+
return true
|
|
849
|
+
}
|
|
850
|
+
const applyText = async () => {
|
|
851
|
+
if (await dryRun() === null) { setNote('校验失败,文本与表单均未修改'); return }
|
|
852
|
+
let parsed = null
|
|
853
|
+
try { parsed = JSON.parse(text) } catch { parsed = null }
|
|
854
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
855
|
+
setNote('校验通过;文本含 JSON5 扩展语法,表单未回填(保存仍以文本为准)')
|
|
856
|
+
return
|
|
857
|
+
}
|
|
858
|
+
const m = {
|
|
859
|
+
id: asStr(parsed.id) || model.id,
|
|
860
|
+
label: asStr(parsed.label), description: asStr(parsed.description),
|
|
861
|
+
enabled: model.enabled, inputs: objToRows(parsed.inputs),
|
|
862
|
+
steps: asArr(parsed.steps).map(normalizeStep),
|
|
863
|
+
}
|
|
864
|
+
setModel(m); setText(serializeModel(m)); setSynced(true)
|
|
865
|
+
setNote('已应用:表单按文本回填')
|
|
866
|
+
}
|
|
867
|
+
const validateText = async () => {
|
|
868
|
+
if (await dryRun() !== null) setNote('校验通过(未落盘)')
|
|
869
|
+
else setNote('校验失败')
|
|
870
|
+
}
|
|
871
|
+
const doSave = async (andRelease) => {
|
|
872
|
+
setNote(''); setSaveErrors([])
|
|
873
|
+
const local = synced ? clientErrors(model) : []
|
|
874
|
+
if (local.length > 0) { setSaveErrors(local); return }
|
|
875
|
+
setSaving(true)
|
|
876
|
+
const o = await post('template-save', { id: model.id, label: model.label, description: model.description, enabled: model.enabled, json: text })
|
|
877
|
+
if (!o.ok) { setSaving(false); setSaveErrors(errsOf(o)); setNote('保存被拒绝'); return }
|
|
878
|
+
if (!andRelease) { setSaving(false); onSaved(''); return }
|
|
879
|
+
const r = await post('release', { id: model.id })
|
|
880
|
+
setSaving(false)
|
|
881
|
+
onSaved(r.ok ? '' : '已保存;创建失败:' + r.error)
|
|
882
|
+
}
|
|
883
|
+
return h(Modal, { title: (isNew ? '新建模板' : '编辑模板 · ' + (entry.id || '(副本:待定 id)')), onClose },
|
|
884
|
+
h('datalist', { id: 'rsww-tmpl-ids' }, templates.map((t) => h('option', { key: t.id, value: t.id }))),
|
|
885
|
+
h(MetaForm, { model, update, synced, topErr }),
|
|
886
|
+
synced ? h(InputsForm, { model, update, topErr }) : null,
|
|
887
|
+
h(PillGroup, { tab: true, ariaLabel: '视图', value: view, options: [{ key: 'struct', label: '结构' }, { key: 'json', label: 'JSON5' }], onChange: setView }),
|
|
888
|
+
view === 'struct'
|
|
889
|
+
? (synced ? h(StepsForm, { model, update, allErrors }) : h('span', { className: 'rsww-note' }, '结构表单已锁定:先在 JSON5 视图点「应用」回填。'))
|
|
890
|
+
: h(Json5Pane, { text, setText, onApply: applyText, onValidate: validateText, jsonErrs, saving }),
|
|
891
|
+
flatErrs.map((e, i) => h('span', { className: 'rsww-error', key: 'flat' + i }, e.message)),
|
|
892
|
+
h('div', { className: 'rsww-row' },
|
|
893
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: () => doSave(false), disabled: saving }, '保存'),
|
|
894
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: () => doSave(true), disabled: saving }, '保存并创建'),
|
|
895
|
+
note ? h('span', { className: 'rsww-note' }, note) : null))
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function TemplateCard({ t, released, busy, onView, onEdit, onCopy, onRelease, onUnrelease, onDelete }) {
|
|
899
|
+
const builtin = t.builtin === true
|
|
900
|
+
const disabled = t.enabled === false
|
|
901
|
+
return h('div', { className: 'rsww-card' },
|
|
902
|
+
h('div', { className: 'rsww-row' },
|
|
903
|
+
h('span', { className: 'rsww-text', style: { fontWeight: 600 } }, t.label || t.id),
|
|
904
|
+
h('span', { className: 'rsww-mono rsww-note' }, t.id),
|
|
905
|
+
builtin ? h(Badge, { tone: 'business' }, '内置') : null,
|
|
906
|
+
disabled ? h(Badge, { tone: 'mute' }, '已禁用') : null,
|
|
907
|
+
released.includes(t.id) ? h(Badge, { tone: 'success' }, '已创建') : null),
|
|
908
|
+
t.description ? h('span', { className: 'rsww-note' }, t.description) : null,
|
|
909
|
+
h('div', { className: 'rsww-row' },
|
|
910
|
+
h('button', { className: 'rsww-btn', type: 'button', disabled: busy || undefined, onClick: onView }, '详情'),
|
|
911
|
+
builtin
|
|
912
|
+
? h('button', { className: 'rsww-btn', type: 'button', disabled: busy || undefined, onClick: onCopy }, '另存为副本')
|
|
913
|
+
: h('button', { className: 'rsww-btn', type: 'button', disabled: busy || undefined, onClick: onEdit }, '编辑'),
|
|
914
|
+
h(ArmedButton, { label: builtin ? '禁用' : '删除', confirmLabel: builtin ? '确认禁用' : '确认删除', disabled: busy, onConfirm: onDelete }),
|
|
915
|
+
!disabled && released.includes(t.id) ? h(ArmedButton, { label: '移除模式', confirmLabel: '确认移除', disabled: busy, onConfirm: onUnrelease }) : null,
|
|
916
|
+
!disabled && !released.includes(t.id) ? h('button', { className: 'rsww-btn', type: 'button', disabled: busy || undefined, onClick: onRelease }, '创建到 dsh') : null))
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// ── 只读详情弹窗:所有模板可点开;结构视图(JSON.parse 可达时)+JSON5 原文+一键检验 ──
|
|
920
|
+
function Modal({ title, onClose, children }) {
|
|
921
|
+
return h('div', { className: 'rsww-modal', onClick: (e) => { if (e.target === e.currentTarget) onClose() } },
|
|
922
|
+
h('div', { className: 'rsww-modal__panel', role: 'dialog', 'aria-label': title },
|
|
923
|
+
h('div', { className: 'rsww-row' },
|
|
924
|
+
h('span', { className: 'rsww-head__title' }, title),
|
|
925
|
+
h('span', { style: { flex: 1 } }),
|
|
926
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: onClose }, '关闭')),
|
|
927
|
+
children))
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function ViewerStep({ step }) {
|
|
931
|
+
const [open, setOpen] = useState(false)
|
|
932
|
+
const typeName = (TYPE_OPTIONS.find((t) => t.key === step.type) || TYPE_OPTIONS[0]).label
|
|
933
|
+
const row = (label, text) => (text !== '' && text != null) ? h('div', { className: 'rsww-kv__row' },
|
|
934
|
+
h('span', { className: 'rsww-label', style: { width: 84, flex: 'none' } }, label),
|
|
935
|
+
h('span', { className: 'rsww-text', style: { whiteSpace: 'pre-wrap' } }, String(text))) : null
|
|
936
|
+
const kvRows = (rows) => h('div', { className: 'rsww-kv' },
|
|
937
|
+
rows.map((kv, i) => h('div', { className: 'rsww-kv__row', key: i },
|
|
938
|
+
h('span', { className: 'rsww-mono' }, kv.k),
|
|
939
|
+
kv.v ? h('span', { className: 'rsww-note' }, kv.v) : null)))
|
|
940
|
+
return h('div', { className: 'rsww-card' },
|
|
941
|
+
h('div', { className: 'rsww-row' },
|
|
942
|
+
h('button', { className: 'rsww-btn', type: 'button', style: { padding: '2px 8px' }, onClick: () => setOpen(!open) },
|
|
943
|
+
(open ? '收起 ' : '展开 ') + (step.id !== '' ? step.id : '#')),
|
|
944
|
+
h(Badge, { tone: 'mute' }, typeName),
|
|
945
|
+
step.type === 'ai' && outNamesOf(step).length > 0 ? h('span', { className: 'rsww-note' }, outNamesOf(step).join('/')) : null),
|
|
946
|
+
!open ? null : h('div', { className: 'rsww-stack' },
|
|
947
|
+
row('显示名', step.label),
|
|
948
|
+
row('slot', step.slot),
|
|
949
|
+
step.type !== 'flow' ? row('prompt', step.prompt) : null,
|
|
950
|
+
step.type === 'ai' && step.outputs.length > 0 ? h('div', { className: 'rsww-section' },
|
|
951
|
+
h('span', { className: 'rsww-label' }, 'outputs 产出契约'),
|
|
952
|
+
kvRows(step.outputs),
|
|
953
|
+
step.listOutputs.length > 0 ? h('span', { className: 'rsww-note' }, 'listOutputs: ' + step.listOutputs.join(', ')) : null) : null,
|
|
954
|
+
step.type === 'approve' ? h('div', { className: 'rsww-stack' },
|
|
955
|
+
row('target', step.target), row('rounds', step.rounds), row('onExhausted', step.onExhausted || '(缺省 blocked)')) : null,
|
|
956
|
+
step.type === 'flow' ? h('div', { className: 'rsww-stack' },
|
|
957
|
+
row('flow', step.flow),
|
|
958
|
+
step.input.length > 0 ? h('div', { className: 'rsww-section' },
|
|
959
|
+
h('span', { className: 'rsww-label' }, 'input(子流程入参)'), kvRows(step.input)) : null) : null,
|
|
960
|
+
step.after.length > 0 ? row('after', step.after.join(', ')) : null,
|
|
961
|
+
step.for_each !== '' ? h('div', { className: 'rsww-stack' },
|
|
962
|
+
row('for_each', step.for_each), row('mode', step.mode === 'parallel' ? 'parallel' : 'sequential')) : null,
|
|
963
|
+
step.load.length > 0 ? row('load', step.load.join(', ')) : null,
|
|
964
|
+
step.maxFail !== '' ? row('maxFail', step.maxFail) : null))
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function TemplateViewer({ entry, onClose }) {
|
|
968
|
+
const [data, setData] = useState(null)
|
|
969
|
+
const [loadErr, setLoadErr] = useState('')
|
|
970
|
+
const [showJson, setShowJson] = useState(false)
|
|
971
|
+
const [checking, setChecking] = useState(false)
|
|
972
|
+
const [checkNote, setCheckNote] = useState('')
|
|
973
|
+
const [checkErrs, setCheckErrs] = useState([])
|
|
974
|
+
const reload = useCallback(() => request('template?id=' + encodeURIComponent(entry.id)).then((o) => {
|
|
975
|
+
if (o.ok) { setData(o.data); setLoadErr('') } else setLoadErr(o.error)
|
|
976
|
+
}), [entry.id])
|
|
977
|
+
useEffect(() => { reload() }, [reload])
|
|
978
|
+
const validate = async () => {
|
|
979
|
+
setChecking(true); setCheckNote(''); setCheckErrs([])
|
|
980
|
+
const o = await post('template-save', { id: entry.id, label: entry.label, description: entry.description, enabled: entry.enabled !== false, json: entry.json, dryRun: true })
|
|
981
|
+
setChecking(false)
|
|
982
|
+
if (o.ok) { setCheckNote('校验通过(未落盘)'); return }
|
|
983
|
+
setCheckErrs(o.data && Array.isArray(o.data.errors) && o.data.errors.length > 0 ? o.data.errors : [{ target: 'json', message: o.error }])
|
|
984
|
+
}
|
|
985
|
+
const parsed = data && data.parsed
|
|
986
|
+
const model = parsed ? { steps: asArr(parsed.steps).map(normalizeStep), inputs: objToRows(parsed.inputs) } : null
|
|
987
|
+
const badge = (tone, text) => h(Badge, { tone }, text)
|
|
988
|
+
return h(Modal, { title: '模板详情', onClose },
|
|
989
|
+
h('div', { className: 'rsww-row' },
|
|
990
|
+
h('span', { className: 'rsww-mono rsww-note' }, entry.id),
|
|
991
|
+
entry.builtin === true ? badge('business', '内置') : null,
|
|
992
|
+
entry.enabled === false ? badge('mute', '已禁用') : null,
|
|
993
|
+
h('span', { style: { flex: 1 } }),
|
|
994
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: validate, disabled: checking }, checking ? '检验中' : '一键检验'),
|
|
995
|
+
checkNote ? badge('success', checkNote) : null),
|
|
996
|
+
entry.description ? h('span', { className: 'rsww-note' }, entry.description) : null,
|
|
997
|
+
loadErr ? h('span', { className: 'rsww-error' }, loadErr) : null,
|
|
998
|
+
!data && !loadErr ? h('span', { className: 'rsww-note' }, '加载中...') : null,
|
|
999
|
+
checkErrs.map((e, i) => h('span', { className: 'rsww-error', key: i }, (e.target ? e.target + ': ' : '') + e.message)),
|
|
1000
|
+
data && !parsed ? h('span', { className: 'rsww-note' }, '模板文本解析失败,仅展示原文与一键检验。') : null,
|
|
1001
|
+
model && model.inputs.length > 0 ? h('div', { className: 'rsww-section' },
|
|
1002
|
+
h('span', { className: 'rsww-label' }, '顶层 inputs'),
|
|
1003
|
+
h('div', { className: 'rsww-kv' },
|
|
1004
|
+
model.inputs.map((kv, i) => h('div', { className: 'rsww-kv__row', key: i },
|
|
1005
|
+
h('span', { className: 'rsww-mono' }, kv.k),
|
|
1006
|
+
kv.v ? h('span', { className: 'rsww-note' }, kv.v) : null)))) : null,
|
|
1007
|
+
model ? h('div', { className: 'rsww-section' },
|
|
1008
|
+
h('span', { className: 'rsww-label' }, '步骤(' + model.steps.length + ')'),
|
|
1009
|
+
model.steps.map((s, i) => h(ViewerStep, { key: i, step: s }))) : null,
|
|
1010
|
+
h('div', { className: 'rsww-section' },
|
|
1011
|
+
h('div', { className: 'rsww-row' },
|
|
1012
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: () => setShowJson(!showJson) }, showJson ? '收起 JSON5' : 'JSON5 全文'),
|
|
1013
|
+
showJson ? h(CopyButton, { text: entry.json || '' }) : null),
|
|
1014
|
+
showJson ? h('div', { className: 'rsww-full', style: { maxHeight: 300 } }, entry.json || '(空)') : null))
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function TemplatesPage() {
|
|
1018
|
+
const [templates, setTemplates] = useState(null)
|
|
1019
|
+
const [released, setReleased] = useState([])
|
|
1020
|
+
const [releasedErr, setReleasedErr] = useState('')
|
|
1021
|
+
const [error, setError] = useState('')
|
|
1022
|
+
const [editing, setEditing] = useState(null)
|
|
1023
|
+
const [viewing, setViewing] = useState(null)
|
|
1024
|
+
const [busyId, setBusyId] = useState('')
|
|
1025
|
+
const [specOpen, setSpecOpen] = useState(false)
|
|
1026
|
+
const [specText, setSpecText] = useState('')
|
|
1027
|
+
const [specLoaded, setSpecLoaded] = useState(false)
|
|
1028
|
+
const reload = useCallback(async () => {
|
|
1029
|
+
const [t, r] = await Promise.all([request('templates'), request('released')])
|
|
1030
|
+
if (!t.ok) { setError(t.error); return }
|
|
1031
|
+
setTemplates(t.data.templates || [])
|
|
1032
|
+
setError('')
|
|
1033
|
+
if (r.ok) { setReleased(r.data.ids || []); setReleasedErr('') } else setReleasedErr('已创建列表拉取失败,创建按钮按未创建态渲染')
|
|
1034
|
+
}, [])
|
|
1035
|
+
useEffect(() => { reload() }, [reload])
|
|
1036
|
+
const toggleSpec = async () => {
|
|
1037
|
+
const next = !specOpen
|
|
1038
|
+
setSpecOpen(next)
|
|
1039
|
+
if (next && !specLoaded) {
|
|
1040
|
+
const o = await request('spec')
|
|
1041
|
+
setSpecText(o.ok ? (o.data.spec || '') : '规范拉取失败:' + o.error)
|
|
1042
|
+
setSpecLoaded(o.ok)
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const write = async (path, body) => {
|
|
1046
|
+
setBusyId(body.id || '?')
|
|
1047
|
+
const o = await post(path, body)
|
|
1048
|
+
setBusyId('')
|
|
1049
|
+
if (!o.ok) { setError(o.error); return }
|
|
1050
|
+
setError('')
|
|
1051
|
+
await reload()
|
|
1052
|
+
}
|
|
1053
|
+
if (templates === null) {
|
|
1054
|
+
return h('div', { className: 'rsww-stack' },
|
|
1055
|
+
h('span', { className: 'rsww-error' }, error || '加载中...'),
|
|
1056
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: reload }, '重试'))
|
|
1057
|
+
}
|
|
1058
|
+
const groups = [['内置模板', templates.filter((t) => t.builtin === true)], ['自定义模板', templates.filter((t) => t.builtin !== true)]]
|
|
1059
|
+
return h('div', { className: 'rsww-stack' },
|
|
1060
|
+
h('div', { className: 'rsww-toolbar' },
|
|
1061
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: () => setEditing({ entry: null, isNew: true }) }, '新建模板'),
|
|
1062
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: toggleSpec }, specOpen ? '收起模板规范' : '模板规范')),
|
|
1063
|
+
error ? h('span', { className: 'rsww-error' }, error) : null,
|
|
1064
|
+
releasedErr ? h('span', { className: 'rsww-note' }, releasedErr) : null,
|
|
1065
|
+
specOpen ? h('div', { className: 'rsww-card' },
|
|
1066
|
+
h('div', { className: 'rsww-row' },
|
|
1067
|
+
h('span', { className: 'rsww-label' }, '模板规范'),
|
|
1068
|
+
specText !== '' && !/^规范拉取失败/.test(specText) ? h(CopyButton, { text: specText }) : null),
|
|
1069
|
+
h('div', { className: 'rsww-full', style: { maxHeight: 320 } }, specText !== '' ? specText : '加载中...')) : null,
|
|
1070
|
+
editing ? h(TemplateEditor, {
|
|
1071
|
+
entry: editing.entry, isNew: editing.isNew, templates,
|
|
1072
|
+
onClose: () => setEditing(null),
|
|
1073
|
+
onSaved: (err2) => { setEditing(null); reload(); if (err2) setError(err2) },
|
|
1074
|
+
}) : null,
|
|
1075
|
+
viewing ? h(TemplateViewer, { entry: viewing, onClose: () => setViewing(null) }) : null,
|
|
1076
|
+
groups.map(([title, list]) => h('div', { className: 'rsww-group', key: title },
|
|
1077
|
+
h('div', { className: 'rsww-group__head' },
|
|
1078
|
+
h('span', { className: 'rsww-group__title' }, title),
|
|
1079
|
+
h('span', { className: 'rsww-group__count' }, list.length + ' 个')),
|
|
1080
|
+
list.map((t) => h(TemplateCard, {
|
|
1081
|
+
key: t.id, t, released, busy: busyId === t.id,
|
|
1082
|
+
onView: () => setViewing(t),
|
|
1083
|
+
onEdit: () => setEditing({ entry: t, isNew: false }),
|
|
1084
|
+
onCopy: () => setEditing({ entry: { id: '', label: (t.label || t.id) + ' 副本', description: t.description || '', enabled: true, json: t.json, copyOf: t.id }, isNew: false }),
|
|
1085
|
+
onRelease: () => write('release', { id: t.id }),
|
|
1086
|
+
onUnrelease: () => write('unrelease', { id: t.id }),
|
|
1087
|
+
onDelete: () => write('template-remove', { id: t.id }),
|
|
1088
|
+
})))))
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// ── gui-config 配置编辑 ─────────────────────────────────────────────────
|
|
1092
|
+
// 副文案与常量镜像 lib/settings-schema.mjs(DEFAULT_CONCURRENCY/KEEP_RUNS 为 host 常量)
|
|
1093
|
+
const SLOT_INFO = [
|
|
1094
|
+
{ key: 'planner', name: 'planner 规划位', desc: '大纲/计划/分诊类步骤显式绑定;留空 = 会话默认模型' },
|
|
1095
|
+
{ key: 'executor', name: 'executor 执行位', desc: '常规步骤缺省绑定;候选失败依次轮换' },
|
|
1096
|
+
{ key: 'reviewer', name: 'reviewer 审阅位', desc: '审批域通用绑定(approve 步固定 reviewer-approve,不降级至此)' },
|
|
1097
|
+
{ key: 'executor-loop', name: 'executor-loop 循环位', desc: 'for_each 实例与审批重做批次的缺省绑定' },
|
|
1098
|
+
{ key: 'reviewer-approve', name: 'reviewer-approve 审批位', desc: 'type:"approve" 步骤固定绑定,模板不可覆盖' },
|
|
1099
|
+
{ key: 'executor-escalate', name: 'executor-escalate 升级位', desc: 'onExhausted 升级步骤的缺省绑定' },
|
|
1100
|
+
]
|
|
1101
|
+
const BUDGET_INFO = [
|
|
1102
|
+
{ key: 'maxStepFail', name: 'maxStepFail', desc: '步骤连续失败上限;达上限步骤 failed、下游 skipped' },
|
|
1103
|
+
{ key: 'approveRounds', name: 'approveRounds', desc: 'approve 步骤默认重审轮次;耗尽走 onExhausted' },
|
|
1104
|
+
{ key: 'escalateLimit', name: 'escalateLimit', desc: 'run 级升级账上限;达上限整流程 blocked' },
|
|
1105
|
+
]
|
|
1106
|
+
const DEFAULT_CONCURRENCY = 4
|
|
1107
|
+
const KEEP_RUNS = 200
|
|
1108
|
+
|
|
1109
|
+
function normSlotValue(v) {
|
|
1110
|
+
if (Array.isArray(v)) return v.filter((x) => typeof x === 'string' && x.trim() !== '')
|
|
1111
|
+
return typeof v === 'string' && v.trim() !== '' ? [v] : undefined
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function slotsNorm(slots) {
|
|
1115
|
+
const out = {}
|
|
1116
|
+
for (const k of SLOT_KEYS) {
|
|
1117
|
+
const v = normSlotValue(slots ? slots[k] : undefined)
|
|
1118
|
+
if (v !== undefined && v.length !== 0) out[k] = v
|
|
1119
|
+
}
|
|
1120
|
+
return out
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function budgetsNorm(budgets) {
|
|
1124
|
+
const out = {}
|
|
1125
|
+
for (const k of BUDGET_KEYS) out[k] = clampRange(budgets ? budgets[k] : '', BUDGET_MIN, BUDGET_MAX)
|
|
1126
|
+
return out
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// 工作位绑定恒为数组形态(单绑定与数组无区别,引擎候选依次轮换)
|
|
1130
|
+
function SlotRow({ info, value, onChange }) {
|
|
1131
|
+
const items = Array.isArray(value) ? value : (typeof value === 'string' && value !== '' ? [value] : [])
|
|
1132
|
+
return h('div', { className: 'rsww-card' },
|
|
1133
|
+
h('div', { className: 'rsww-row' },
|
|
1134
|
+
h('span', { className: 'rsww-text', style: { fontWeight: 600 } }, info.name)),
|
|
1135
|
+
h('span', { className: 'rsww-note' }, info.desc),
|
|
1136
|
+
h(StrRows, { items, onChange, ph: 'provider/model 或模型名,留空=缺省链', min: 1 }))
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function BudgetsForm({ budgets, onChange }) {
|
|
1140
|
+
return h('div', { className: 'rsww-section' },
|
|
1141
|
+
h('span', { className: 'rsww-label' }, '预算(clamp [1,10],失焦钳制)'),
|
|
1142
|
+
BUDGET_INFO.map((b) => h('div', { className: 'rsww-kv__row', key: b.key },
|
|
1143
|
+
h('span', { className: 'rsww-label', style: { width: 110, flex: 'none' } }, b.name),
|
|
1144
|
+
h('input', { className: 'rsww-input', type: 'number', value: budgets[b.key], min: BUDGET_MIN, max: BUDGET_MAX,
|
|
1145
|
+
onChange: (e) => onChange({ ...budgets, [b.key]: e.target.value === '' ? '' : Number(e.target.value) }),
|
|
1146
|
+
onBlur: () => onChange({ ...budgets, [b.key]: clampRange(budgets[b.key], BUDGET_MIN, BUDGET_MAX) }) }),
|
|
1147
|
+
h('span', { className: 'rsww-note', style: { flex: 1, minWidth: 200 } }, b.desc))))
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function ConfigPage() {
|
|
1151
|
+
const [meta, setMeta] = useState(null)
|
|
1152
|
+
const [draft, setDraft] = useState(null)
|
|
1153
|
+
const [error, setError] = useState('')
|
|
1154
|
+
const [saving, setSaving] = useState(false)
|
|
1155
|
+
const [saved, setSaved] = useState(false)
|
|
1156
|
+
const load = useCallback(async () => {
|
|
1157
|
+
const o = await request('config')
|
|
1158
|
+
if (!o.ok) { setError(o.error); return }
|
|
1159
|
+
const snap = { slots: { ...(o.data.config.slots || {}) }, budgets: { ...(o.data.config.budgets || {}) } }
|
|
1160
|
+
setMeta(snap)
|
|
1161
|
+
setDraft({ slots: { ...snap.slots }, budgets: { ...snap.budgets } })
|
|
1162
|
+
setError('')
|
|
1163
|
+
}, [])
|
|
1164
|
+
useEffect(() => { load() }, [load])
|
|
1165
|
+
if (!meta) {
|
|
1166
|
+
return h('div', { className: 'rsww-stack' },
|
|
1167
|
+
h('span', { className: 'rsww-error' }, error || '加载中...'),
|
|
1168
|
+
error ? h('span', { className: 'rsww-note' }, '配置读取失败:宿主半区可能未装载。') : null,
|
|
1169
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: load }, '重试'))
|
|
1170
|
+
}
|
|
1171
|
+
const dirty = JSON.stringify(slotsNorm(draft.slots)) !== JSON.stringify(slotsNorm(meta.slots))
|
|
1172
|
+
|| JSON.stringify(budgetsNorm(draft.budgets)) !== JSON.stringify(budgetsNorm(meta.budgets))
|
|
1173
|
+
const save = async () => {
|
|
1174
|
+
setSaving(true); setError(''); setSaved(false)
|
|
1175
|
+
const o = await post('config-save', { slots: slotsNorm(draft.slots), budgets: budgetsNorm(draft.budgets) })
|
|
1176
|
+
setSaving(false)
|
|
1177
|
+
if (!o.ok) { setError(o.error); return }
|
|
1178
|
+
setSaved(true)
|
|
1179
|
+
await load()
|
|
1180
|
+
}
|
|
1181
|
+
return h('div', { className: 'rsww-stack' },
|
|
1182
|
+
h('div', { className: 'rsww-section' },
|
|
1183
|
+
h('span', { className: 'rsww-label' }, '工作位(留空 = host 缺省解析)'),
|
|
1184
|
+
SLOT_INFO.map((info) => h(SlotRow, {
|
|
1185
|
+
key: info.key, info, value: draft.slots[info.key],
|
|
1186
|
+
onChange: (v) => setDraft((d) => ({ ...d, slots: { ...d.slots, [info.key]: v } })),
|
|
1187
|
+
}))),
|
|
1188
|
+
h(BudgetsForm, { budgets: draft.budgets, onChange: (b) => setDraft((d) => ({ ...d, budgets: b })) }),
|
|
1189
|
+
h('div', { className: 'rsww-section' },
|
|
1190
|
+
h('span', { className: 'rsww-label' }, '常量(不开放配置)'),
|
|
1191
|
+
h('span', { className: 'rsww-note' }, '默认并发 ' + DEFAULT_CONCURRENCY + ' · 运行记录保留 ' + KEEP_RUNS + ' 条')),
|
|
1192
|
+
h('div', { className: 'rsww-toolbar' },
|
|
1193
|
+
dirty ? h(Badge, { tone: 'warn' }, '有未保存修改') : h(Badge, { tone: 'mute' }, '与宿主一致'),
|
|
1194
|
+
h('button', { className: 'rsww-btn', type: 'button', onClick: save, disabled: !dirty || saving }, saving ? '保存中' : '保存'),
|
|
1195
|
+
saved ? h('span', { className: 'rsww-note' }, '已保存并复核') : null,
|
|
1196
|
+
error ? h('span', { className: 'rsww-error' }, error) : null))
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// ── section 壳:标题/说明/子页 pill(模板默认;运行记录唯一视图=会话页签) ──
|
|
1200
|
+
function RswwApp() {
|
|
1201
|
+
const [page, setPage] = useState('templates')
|
|
1202
|
+
const pages = [{ key: 'templates', label: '流程模板' }, { key: 'config', label: '配置' }]
|
|
1203
|
+
return h('div', { className: 'rsww-root' },
|
|
1204
|
+
React.createElement('style', { 'data-plugin': '@mzzsfy/dsh-rs-workflow', dangerouslySetInnerHTML: { __html: CSS } }),
|
|
1205
|
+
React.createElement('div', { className: 'rsww-head' },
|
|
1206
|
+
React.createElement('span', { className: 'rsww-head__title' }, '若水工作流'),
|
|
1207
|
+
React.createElement('span', { className: 'rsww-head__caption' },
|
|
1208
|
+
'强规则编排:模式内消息被接管,批次推进,结构化产出强制校验;流程模板与配置在此集中管理,运行记录在会话「若水编排」页签查看。')),
|
|
1209
|
+
h(PillGroup, { tab: true, ariaLabel: '子页', value: page, options: pages, onChange: setPage }),
|
|
1210
|
+
page === 'templates' ? h(TemplatesPage) : h(ConfigPage))
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return {
|
|
1214
|
+
inject: ['slots', 'sessions'],
|
|
1215
|
+
apply(ctx) {
|
|
1216
|
+
ctx.slots.inject('settings.section', () =>
|
|
1217
|
+
ctx.slots.register(
|
|
1218
|
+
{ name: 'settings.section', id: 'rs-workflow-board', order: 45, label: '若水工作流' },
|
|
1219
|
+
() => React.createElement(RswwApp),
|
|
1220
|
+
))
|
|
1221
|
+
// 会话感知条件注入:当前会话存在 rs 运行记录才挂页签(v3 ensure/judge 模式)
|
|
1222
|
+
ctx.effect(() => {
|
|
1223
|
+
let disposed = false
|
|
1224
|
+
let timer = null
|
|
1225
|
+
let disposeView = null
|
|
1226
|
+
let cache = { sessionId: undefined, isRs: false }
|
|
1227
|
+
let currentSessionId
|
|
1228
|
+
const ensure = (isRs) => {
|
|
1229
|
+
if (disposed) return
|
|
1230
|
+
if (isRs && !disposeView) {
|
|
1231
|
+
disposeView = ctx.slots.inject('conversation.view', () =>
|
|
1232
|
+
ctx.slots.register(
|
|
1233
|
+
{ name: 'conversation.view', id: VIEW_ID, order: 15, label: '若水编排' },
|
|
1234
|
+
(props) => React.createElement(FlowView, { getSessionId: () => currentSessionId, ...props }),
|
|
1235
|
+
))
|
|
1236
|
+
} else if (!isRs && disposeView) {
|
|
1237
|
+
disposeView(); disposeView = null
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const judge = async () => {
|
|
1241
|
+
const sessionId = ctx.sessions?.list?.getSnapshot?.().current
|
|
1242
|
+
currentSessionId = sessionId
|
|
1243
|
+
if (sessionId === undefined) { ensure(false); return }
|
|
1244
|
+
if (cache.sessionId === sessionId && cache.fetched) { ensure(cache.isRs); return }
|
|
1245
|
+
try {
|
|
1246
|
+
const res = await fetch(API + 'runs')
|
|
1247
|
+
if (!res.ok) return
|
|
1248
|
+
const json = await res.json()
|
|
1249
|
+
if (disposed) return
|
|
1250
|
+
const isRs = (json && Array.isArray(json.runs) ? json.runs : []).some((r) => r.sessionId === sessionId)
|
|
1251
|
+
cache = { sessionId, isRs, fetched: true }
|
|
1252
|
+
ensure(isRs)
|
|
1253
|
+
} catch { /* host 未就绪:下轮重试 */ }
|
|
1254
|
+
}
|
|
1255
|
+
const unsubscribe = ctx.sessions.list.subscribe(() => { cache = { sessionId: undefined, isRs: false }; judge() })
|
|
1256
|
+
timer = setInterval(judge, REFRESH_MS)
|
|
1257
|
+
judge()
|
|
1258
|
+
return () => {
|
|
1259
|
+
disposed = true
|
|
1260
|
+
unsubscribe()
|
|
1261
|
+
if (timer) clearInterval(timer)
|
|
1262
|
+
if (disposeView) disposeView()
|
|
1263
|
+
if (disposeChip) disposeChip()
|
|
1264
|
+
}
|
|
1265
|
+
}, 'rs-workflow: session-aware flow view')
|
|
1266
|
+
},
|
|
1267
|
+
}
|
|
1268
|
+
},
|
|
1269
|
+
})
|