@ljwei-stak/dsh-model-route-bot 0.64.0 → 0.65.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/.dsh-plugin/client.js +694 -407
- package/README.i18n.yaml +2 -2
- package/README.md +209 -147
- package/README.zh.md +208 -146
- package/apps/integration-client/bot-attention.mjs +2 -2
- package/apps/integration-client/panels.jsx +239 -134
- package/apps/integration-client/product.css +36 -0
- package/apps/integration-client/routing-submissions.mjs +68 -0
- package/apps/integration-client/task-progress.mjs +46 -0
- package/apps/integration-client/tasks-panel.jsx +147 -0
- package/modules/bot-runtime/read-model.mjs +2 -2
- package/package.json +6 -87
- package/packages/model-router-plugin/README.md +39 -15
- package/packages/model-router-plugin/README.zh.md +40 -16
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/bot-routing-candidates.mjs +6 -6
- package/packages/orchestration/src/bot-routing-service.mjs +30 -5
- package/packages/orchestration/src/bot-routing.mjs +6 -4
- package/packages/orchestration/src/product-composition.mjs +18 -24
- package/packages/orchestration/src/task-journal.mjs +23 -0
- package/packages/orchestration/src/task-product.mjs +49 -0
- package/remote-computer-agent/build.mjs +48 -13
- package/CHANGELOG.md +0 -456
- package/docs/INSTALLATION_GUIDE.zh.md +0 -150
- package/docs/MANUAL_TASK_ASSIGNMENT.zh.md +0 -46
- package/docs/ROUTING_ADAPTIVE_LEARNING.zh.md +0 -153
- package/docs/ROUTING_ALGORITHM_OPTIMIZATION.zh.md +0 -91
- package/docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md +0 -684
- package/docs/ROUTING_LIVEBENCH_DATA_SEARCH.zh.md +0 -74
- package/docs/ROUTING_LIVEBENCH_VALIDATION.zh.md +0 -154
- package/docs/ROUTING_MATURITY.zh.md +0 -76
- package/docs/ROUTING_RESEARCH.zh.md +0 -59
- package/docs/ROUTING_SELFTEST_STEPS.zh.md +0 -192
- package/docs/WORKBENCH_USER_GUIDE.zh.md +0 -293
- package/docs/archive/MIGRATION.md +0 -53
- package/docs/archive/README.md +0 -21
- package/docs/assets/candidate-pruning.svg +0 -80
- package/docs/assets/desktop-official-tools-0.9.0.png +0 -0
- package/docs/assets/router-only-0.12.0.png +0 -0
- package/docs/assets/routing-workflow.svg +0 -96
- package/docs/assets/workbench-budget-reservations.png +0 -0
- package/docs/assets/workbench-run-progress.png +0 -0
- package/docs/assets/workbench-run-timeout.png +0 -0
- package/docs/assets/workbench-subscription-usage.png +0 -0
- package/docs/assets/workbench-task-preview.jpg +0 -0
- package/docs/assets/workbench-usage.svg +0 -119
- package/docs/releases/0.20.0-validation.md +0 -26
- package/docs/releases/0.22.0-validation.md +0 -28
- package/docs/releases/0.23.0-validation.md +0 -30
- package/docs/releases/0.23.1-validation.md +0 -23
- package/docs/releases/0.24.0-validation.md +0 -41
- package/docs/releases/0.25.0-validation.md +0 -41
- package/docs/releases/0.25.1-validation.md +0 -36
- package/docs/releases/0.25.2-validation.md +0 -48
- package/docs/releases/0.25.3-validation.md +0 -50
- package/docs/releases/0.25.4-validation.md +0 -41
- package/docs/releases/0.25.5-validation.md +0 -40
- package/docs/releases/0.26.0-preview.0-validation.md +0 -23
- package/docs/releases/0.26.0-preview.1-f-difference.json +0 -6845
- package/docs/releases/0.26.0-preview.1-f-difference.md +0 -85
- package/docs/releases/0.26.0-preview.1-validation.md +0 -103
- package/docs/releases/0.63.0-validation.md +0 -51
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
|
|
2
2
|
import { PRODUCT_PANELS, TASK_STATES, modelName, money } from './api.mjs'
|
|
3
|
-
import {
|
|
3
|
+
import { TaskWorkspace } from './tasks-panel.jsx'
|
|
4
4
|
import { normalizeEndpoint, parsePairingPaste, secretNameFor } from './pairing.mjs'
|
|
5
5
|
import { framePoint } from '../../packages/orchestration/src/desktop-contract.mjs'
|
|
6
6
|
import { BotBindingEditor } from './binding-editor.jsx'
|
|
@@ -8,6 +8,7 @@ import { LibraryPanel } from './library-panel.jsx'
|
|
|
8
8
|
import { DesktopWorkbench } from './desktop-pane.jsx'
|
|
9
9
|
import { RoutinesPanel } from './routines-panel.jsx'
|
|
10
10
|
import { UiIcon } from './ui-icons.jsx'
|
|
11
|
+
import { routingSubmissions, submissionCode } from './routing-submissions.mjs'
|
|
11
12
|
|
|
12
13
|
const NAV_EVENT = 'bot-product:navigation'
|
|
13
14
|
let activePanel = null
|
|
@@ -164,38 +165,33 @@ export function BotBindings({ snapshot, openSession, dispatch, busy }) {
|
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
|
|
167
|
-
/**
|
|
168
|
-
* What the team needs from a person, above the task table. Healthy state renders nothing,
|
|
169
|
-
* and each row offers the action it names instead of a link into a report.
|
|
170
|
-
*/
|
|
171
|
-
function BotAttention({ snapshot, dispatch, busy, openTask }) {
|
|
172
|
-
const rows = deriveBotAttention(snapshot)
|
|
173
|
-
if (rows.length === 0) return null
|
|
174
|
-
const actions = snapshot?.availableActions ?? []
|
|
175
|
-
const run = (row, action, payload) => {
|
|
176
|
-
if (!dispatch || busy || !actions.includes(action)) return
|
|
177
|
-
void dispatch(action, { botId: row.botId, taskId: row.taskId, requestKey: crypto.randomUUID(), ...payload })
|
|
178
|
-
}
|
|
179
|
-
return <section className="bp-card bp-attention" aria-label="需要你处理">
|
|
180
|
-
<div className="bp-card-title"><h2>需要你处理</h2><Tag>{botAttentionSummary(rows)}</Tag></div>
|
|
181
|
-
<ul className="bp-attention-list">{rows.map(row => <li key={row.key} className={`bp-attention-row bp-attention-${row.stage}`}>
|
|
182
|
-
<div><strong>{row.title}</strong><p className="bp-muted">{row.detail}</p></div>
|
|
183
|
-
{row.action === 'reconcile' && actions.includes('task.reconcile') && <button disabled={busy} onClick={() => run(row, 'task.reconcile')}>核对原收据</button>}
|
|
184
|
-
{row.action === 'retry' && actions.includes('task.retry') && <button disabled={busy} onClick={() => run(row, 'task.retry')}>重试</button>}
|
|
185
|
-
{row.action !== 'reconcile' && row.action !== 'retry' && <button disabled={busy} onClick={() => openTask(row.taskId)}>打开任务</button>}
|
|
186
|
-
</li>)}</ul>
|
|
187
|
-
</section>
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
168
|
/**
|
|
192
169
|
* Bot routing: who should take this task, and why.
|
|
193
170
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* person acts on it elsewhere.
|
|
171
|
+
* Preview is read-only. Only an explicit create action can submit the captured, current task.
|
|
172
|
+
* A late response must not revive an edited draft or move a decision to another Host.
|
|
197
173
|
*/
|
|
198
|
-
|
|
174
|
+
const routingPresetLabel = preset => ({ economy: '省钱优先', balanced: '均衡', quality: '效果优先' })[preset] ?? '当前策略'
|
|
175
|
+
const routingRefusalMessage = reason => {
|
|
176
|
+
const code = String(reason ?? '').split(':')[0]
|
|
177
|
+
return ({ INVALID_CANDIDATE: '候选信息不完整', BOT_DISABLED: '已停用', MISSING_BOT_ID: '缺少 Bot 标识', BOT_MODEL_UNBOUND: '尚未绑定模型',
|
|
178
|
+
BOT_MACHINE_OFFLINE: '所需电脑未在线', BOT_BUSY: '正在处理其他任务', BOT_MISSING_TOOL: '缺少任务所需工具', BOT_MISSING_CAPABILITY: '缺少任务所需能力',
|
|
179
|
+
BOT_ROUTING_WORKER_CEILING_INSUFFICIENT: '保存的电脑权限不足' })[code] ?? '不满足本次执行条件'
|
|
180
|
+
}
|
|
181
|
+
const validRoutingView = value => value && typeof value.enabled === 'boolean' && ['economy', 'balanced', 'quality'].includes(value.preset)
|
|
182
|
+
&& Array.isArray(value.candidates) && value.candidates.every(row => row && typeof row.botId === 'string' && row.botId !== '')
|
|
183
|
+
const submissionFailureMessage = failure => {
|
|
184
|
+
const code = failure?.code
|
|
185
|
+
if (code === 'REMOTE_OWNER_REQUIRED') return '请重新登录宿主拥有者账号后核对原提交;请保留核对码,不要重新创建任务。'
|
|
186
|
+
if (['BOT_ROUTING_STATUS_UNAVAILABLE', 'PRODUCT_ACTION_UNAVAILABLE'].includes(code)) return '当前宿主尚不支持原提交核对,请更新宿主或在任务记录中核对;请保留核对码。'
|
|
187
|
+
if (['BOT_ROUTING_SUBMISSION_IDENTITY_MISMATCH', 'INVALID_ROUTING_SUBMISSION_STATUS'].includes(code)) return '返回记录与原提交身份不一致,尚不能确认结果。请在原宿主核对任务记录并保留核对码。'
|
|
188
|
+
if (code === 'PRODUCT_READ_ONLY') return '宿主目前处于只读状态,请恢复宿主后核对原提交;请保留核对码,不要重新创建任务。'
|
|
189
|
+
return '原提交核对尚未完成,请恢复宿主连接后再次核对;请保留核对码,不会重新提交任务。'
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function BotRoutingCard({ snapshot, dispatch, busy, submissionScope = dispatch }) {
|
|
193
|
+
const submissions = useMemo(() => routingSubmissions(submissionScope), [submissionScope])
|
|
194
|
+
const submission = useSyncExternalStore(submissions.subscribe, submissions.getSnapshot, submissions.getSnapshot)
|
|
199
195
|
const [view, setView] = React.useState(null)
|
|
200
196
|
const [error, setError] = React.useState('')
|
|
201
197
|
const [text, setText] = React.useState('')
|
|
@@ -204,86 +200,218 @@ function BotRoutingCard({ snapshot, dispatch, busy }) {
|
|
|
204
200
|
const [root, setRoot] = React.useState('')
|
|
205
201
|
const [effect, setEffect] = React.useState('read')
|
|
206
202
|
const [decision, setDecision] = React.useState(null)
|
|
207
|
-
const [working, setWorking] = React.useState(
|
|
208
|
-
const
|
|
209
|
-
|
|
203
|
+
const [working, setWorking] = React.useState('')
|
|
204
|
+
const [savedCode, setSavedCode] = useState('')
|
|
205
|
+
const [copyStatus, setCopyStatus] = useState('')
|
|
206
|
+
const codeInput = useRef(null)
|
|
207
|
+
const [viewReady, setViewReady] = React.useState(false)
|
|
208
|
+
const control = useRef({ live: false, epoch: 0, draft: 0, active: null, prepared: null, ready: false })
|
|
209
|
+
const form = useRef({ text: '', pin: '', machine: '', root: '', effect: 'read' })
|
|
210
|
+
const busyRef = useRef(busy)
|
|
211
|
+
busyRef.current = busy
|
|
210
212
|
const call = useCallback(async (action, payload) => {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
if (typeof dispatch !== 'function') throw new Error('宿主接口尚未就绪。')
|
|
214
|
+
const value = await dispatch(action, payload, { errorResult: true })
|
|
215
|
+
if (value === undefined || value?.ok === false) throw Object.assign(new Error(value?.error?.message ?? '宿主尚未确认这次请求,请刷新后重试。'), { code: value?.error?.code })
|
|
213
216
|
return value
|
|
217
|
+
}, [dispatch])
|
|
218
|
+
const invalidate = useCallback(() => {
|
|
219
|
+
control.current.draft++; control.current.prepared = null
|
|
220
|
+
setDecision(null)
|
|
214
221
|
}, [])
|
|
215
|
-
|
|
222
|
+
const editDraft = changes => {
|
|
223
|
+
const next = { ...form.current, ...changes }
|
|
224
|
+
form.current = next
|
|
225
|
+
setText(next.text); setPin(next.pin); setMachine(next.machine); setRoot(next.root); setEffect(next.effect)
|
|
226
|
+
const previous = submissions.getSnapshot()
|
|
227
|
+
if (previous?.state === 'confirmed') submissions.clear(previous)
|
|
228
|
+
invalidate()
|
|
229
|
+
}
|
|
230
|
+
const begin = useCallback(kind => {
|
|
231
|
+
const state = control.current
|
|
232
|
+
if (!state.live || state.active || busyRef.current) return null
|
|
233
|
+
const token = { epoch: state.epoch }
|
|
234
|
+
state.active = token; setWorking(kind); setError('')
|
|
235
|
+
return token
|
|
236
|
+
}, [])
|
|
237
|
+
const current = token => control.current.live && control.current.epoch === token.epoch && control.current.active === token
|
|
238
|
+
const finish = token => { if (current(token)) { control.current.active = null; setWorking('') } }
|
|
239
|
+
const staleView = () => { control.current.ready = false; setViewReady(false) }
|
|
216
240
|
const load = React.useCallback(async () => {
|
|
217
|
-
|
|
241
|
+
const token = begin('inspect')
|
|
242
|
+
if (!token) return
|
|
243
|
+
invalidate(); staleView()
|
|
218
244
|
try {
|
|
219
245
|
const next = await call('botRoutingInspect', {})
|
|
220
|
-
|
|
221
|
-
|
|
246
|
+
if (!current(token)) return
|
|
247
|
+
if (!validRoutingView(next)) throw new Error('宿主未返回完整候选信息,请刷新后重试。')
|
|
248
|
+
setView(next); control.current.ready = true; setViewReady(true)
|
|
222
249
|
} catch (failure) {
|
|
223
|
-
setError(String(failure?.message ?? failure))
|
|
224
|
-
}
|
|
225
|
-
}, [call])
|
|
226
|
-
useEffect(() => {
|
|
250
|
+
if (current(token)) setError(String(failure?.message ?? failure))
|
|
251
|
+
} finally { finish(token) }
|
|
252
|
+
}, [call, begin, invalidate])
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
const state = control.current
|
|
255
|
+
state.live = true; state.epoch++; state.active = null; state.ready = false
|
|
256
|
+
setView(null); setViewReady(false); setWorking(''); setSavedCode(''); setCopyStatus(''); setError(''); invalidate()
|
|
257
|
+
void load()
|
|
258
|
+
return () => { state.live = false; state.epoch++; state.draft++; state.active = null; state.prepared = null; state.ready = false }
|
|
259
|
+
}, [load, invalidate])
|
|
260
|
+
useEffect(() => { setCopyStatus('') }, [submissions, submission?.requestKey])
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (!busy && !view && control.current.live && !control.current.active) void load()
|
|
263
|
+
}, [busy, view, load])
|
|
227
264
|
const saveSettings = async settings => {
|
|
228
|
-
|
|
265
|
+
if (!control.current.ready) return
|
|
266
|
+
const token = begin('settings')
|
|
267
|
+
if (!token) return
|
|
268
|
+
invalidate(); staleView()
|
|
229
269
|
try {
|
|
230
270
|
const value = await call('botRouting.save', { expectedRevision: view.settingsRevision ?? 0, settings })
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
271
|
+
if (!current(token)) return
|
|
272
|
+
if (!validRoutingView(value)) throw new Error('设置保存结果尚未确认,请刷新候选核对。')
|
|
273
|
+
setView(value); control.current.ready = true; setViewReady(true)
|
|
274
|
+
} catch (failure) { if (current(token)) setError(String(failure?.message ?? failure)) }
|
|
275
|
+
finally { finish(token) }
|
|
234
276
|
}
|
|
235
277
|
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
278
|
+
const routeRequest = () => {
|
|
279
|
+
const input = form.current
|
|
280
|
+
return { text: input.text.trim(), requiresMachine: input.machine !== '',
|
|
281
|
+
requesterBotId: view.requesterBotId ?? snapshot.mainBotId ?? snapshot.bots?.find(bot => bot.main)?.id ?? '',
|
|
282
|
+
...(input.pin ? { pinnedBotId: input.pin } : {}),
|
|
283
|
+
...(input.machine ? { machineId: input.machine, rootId: input.root || snapshot.machines?.items?.find(item => item.id === input.machine)?.roots?.[0] || '' } : {}),
|
|
284
|
+
needs: { write: input.effect === 'write', desktop: input.effect === 'desktop', browser: input.effect === 'browser' } }
|
|
285
|
+
}
|
|
241
286
|
const submitRouted = async () => {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
287
|
+
const state = control.current, prepared = state.prepared
|
|
288
|
+
if (!state.ready || submissions.getSnapshot() || !prepared || prepared.draft !== state.draft || prepared.epoch !== state.epoch) return
|
|
289
|
+
const token = begin('dispatch')
|
|
290
|
+
if (!token) return
|
|
291
|
+
let reference
|
|
245
292
|
try {
|
|
246
|
-
|
|
293
|
+
prepared.requestKey ??= crypto.randomUUID()
|
|
294
|
+
const request = { ...prepared.request, requestKey: prepared.requestKey }
|
|
295
|
+
if (!request.requesterBotId) throw new Error('尚未取得提交任务的主 Bot,请刷新候选后重试。')
|
|
296
|
+
reference = { requestKey: request.requestKey, requesterBotId: request.requesterBotId }
|
|
297
|
+
if (!submissions.start(reference)) return
|
|
247
298
|
const next = await call('botRouting.dispatch', request)
|
|
248
|
-
|
|
249
|
-
|
|
299
|
+
const planId = next?.planId ?? next?.taskId
|
|
300
|
+
if (next?.accepted !== true || typeof planId !== 'string' || !planId.trim()) throw new Error('未取得有效的任务计划编号。')
|
|
301
|
+
// The receipt belongs to the captured Host even if its card was unmounted meanwhile.
|
|
302
|
+
submissions.update(reference, { state: 'confirmed', planId, planState: next.planState })
|
|
303
|
+
if (current(token)) state.prepared = null
|
|
250
304
|
} catch (failure) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
305
|
+
if (reference) submissions.update(reference, { state: 'unknown' })
|
|
306
|
+
if (current(token)) {
|
|
307
|
+
invalidate(); staleView()
|
|
308
|
+
setError(reference ? '提交结果尚未确认,请刷新任务列表或核对原提交,避免重复创建。' : String(failure?.message ?? failure))
|
|
309
|
+
}
|
|
310
|
+
} finally { finish(token) }
|
|
311
|
+
}
|
|
312
|
+
const checkSubmission = async () => {
|
|
313
|
+
const reference = submissions.getSnapshot()
|
|
314
|
+
if (!reference || !snapshot.availableActions?.includes('botRouting.status')) return
|
|
315
|
+
const token = begin('status')
|
|
316
|
+
if (!token) return
|
|
317
|
+
try {
|
|
318
|
+
const next = await call('botRouting.status', { requestKey: reference.requestKey, requesterBotId: reference.requesterBotId })
|
|
319
|
+
if (typeof next?.found !== 'boolean' || next.originBotId !== reference.requesterBotId
|
|
320
|
+
|| next.found && (next.accepted !== true || typeof next.planId !== 'string' || !next.planId.trim())) throw Object.assign(new Error('宿主未返回有效的原提交记录。'), { code: 'INVALID_ROUTING_SUBMISSION_STATUS' })
|
|
321
|
+
submissions.update(reference, next.found ? { state: 'confirmed', planId: next.planId, planState: next.planState } : { state: 'not-found' })
|
|
322
|
+
} catch (failure) {
|
|
323
|
+
if (current(token)) setError(submissionFailureMessage(failure))
|
|
324
|
+
} finally { finish(token) }
|
|
325
|
+
}
|
|
326
|
+
const restoreSubmission = event => {
|
|
327
|
+
event.preventDefault()
|
|
328
|
+
if (busyRef.current || control.current.active || !control.current.live || submissions.getSnapshot()) return
|
|
329
|
+
try {
|
|
330
|
+
if (!submissions.restore(savedCode)) return
|
|
331
|
+
setSavedCode(''); invalidate()
|
|
332
|
+
void checkSubmission()
|
|
333
|
+
} catch { setError('核对码格式无效或版本不支持,请粘贴完整的提交核对码。') }
|
|
334
|
+
}
|
|
335
|
+
const copySubmission = async () => {
|
|
336
|
+
const reference = submissions.getSnapshot(), epoch = control.current.epoch
|
|
337
|
+
if (!reference) return
|
|
338
|
+
const code = submissionCode(reference)
|
|
339
|
+
const stillCurrent = () => control.current.live && control.current.epoch === epoch
|
|
340
|
+
&& submissions.getSnapshot()?.requestKey === reference.requestKey && submissions.getSnapshot()?.requesterBotId === reference.requesterBotId
|
|
341
|
+
codeInput.current?.focus(); codeInput.current?.select()
|
|
342
|
+
try {
|
|
343
|
+
if (typeof globalThis.navigator?.clipboard?.writeText !== 'function') throw new Error('CLIPBOARD_UNAVAILABLE')
|
|
344
|
+
await globalThis.navigator.clipboard.writeText(code)
|
|
345
|
+
if (stillCurrent()) setCopyStatus('已复制核对码,请保存后用于原宿主查询。')
|
|
346
|
+
} catch {
|
|
347
|
+
if (stillCurrent()) setCopyStatus('请选中核对码后手动复制,并保存用于原宿主查询。')
|
|
254
348
|
}
|
|
255
349
|
}
|
|
256
350
|
const decide = async () => {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
351
|
+
const state = control.current
|
|
352
|
+
if (!state.ready || submissions.getSnapshot() || !view.enabled || !form.current.text.trim()) return
|
|
353
|
+
const token = begin('preview')
|
|
354
|
+
if (!token) return
|
|
355
|
+
invalidate()
|
|
356
|
+
const draft = state.draft, request = routeRequest()
|
|
260
357
|
try {
|
|
261
|
-
const request = routeRequest()
|
|
262
358
|
const next = await call('botRoutingDecide', request)
|
|
263
|
-
if (
|
|
359
|
+
if (!current(token) || state.draft !== draft) return
|
|
360
|
+
if (!next || !Object.hasOwn(next, 'chosen') || !Array.isArray(next.ranked) || !Array.isArray(next.rejected)
|
|
361
|
+
|| next.chosen !== null && typeof next.chosen?.botId !== 'string') throw new Error('宿主未返回完整预览,请刷新后重试。')
|
|
362
|
+
state.prepared = next.chosen ? { request, draft, epoch: token.epoch, requestKey: null } : null
|
|
264
363
|
setDecision(next)
|
|
265
364
|
} catch (failure) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
} finally {
|
|
269
|
-
setWorking(false)
|
|
270
|
-
}
|
|
365
|
+
if (current(token) && state.draft === draft) { invalidate(); staleView(); setError(String(failure?.message ?? failure)) }
|
|
366
|
+
} finally { finish(token) }
|
|
271
367
|
}
|
|
272
368
|
|
|
369
|
+
const submissionNotice = submission && <section className="bp-blocked" role="region" aria-label="原提交核对">
|
|
370
|
+
{submission.state === 'confirmed' ? <p role="status">已提交:{submission.planId}。{TASK_STATES[submission.planState] ?? '请在任务记录中跟踪执行结果。'}</p>
|
|
371
|
+
: submission.state === 'not-found' ? <p role="status">当前尚未查到原提交,原请求仍可能完成,请稍后再次核对。</p>
|
|
372
|
+
: <p role="status">{submission.state === 'submitting' && working === 'dispatch' ? '正在提交任务…' : '提交结果尚未确认,请核对原提交。'}</p>}
|
|
373
|
+
<p>核对只读取原任务记录,不会再次提交或授予权限。</p>
|
|
374
|
+
<details className="bp-technical"><summary>保存提交核对码</summary>
|
|
375
|
+
<p>重载应用前可保存核对码,之后在原宿主粘贴查询。核对码只包含提交编号,不包含任务正文或登录凭据。</p>
|
|
376
|
+
<label>当前提交核对码<input ref={codeInput} aria-label="当前提交核对码" className="bp-submission-code" readOnly value={submissionCode(submission)} onFocus={event => event.target.select()} /></label>
|
|
377
|
+
<div className="bp-actions"><button type="button" onClick={() => { void copySubmission() }}>复制核对码</button>{copyStatus && <small role="status">{copyStatus}</small>}</div>
|
|
378
|
+
</details>
|
|
379
|
+
<div className="bp-actions">
|
|
380
|
+
{snapshot.availableActions?.includes('botRouting.status') ? <button type="button" disabled={busy || Boolean(working)} onClick={() => { void checkSubmission() }}>核对原提交</button>
|
|
381
|
+
: <small>当前宿主尚未提供原提交核对,请先在任务记录中核对。</small>}
|
|
382
|
+
{submission.state === 'confirmed' && <button type="button" disabled={busy || Boolean(working)} onClick={() => { submissions.clear(submission); invalidate() }}>新建另一项任务</button>}
|
|
383
|
+
</div>
|
|
384
|
+
{submission.state === 'not-found' && <details className="bp-technical"><summary>已自行核对记录</summary>
|
|
385
|
+
<p>原请求仍可能完成。清除提醒不会停止原任务,再次创建可能重复;只有自行核对后才清除。</p>
|
|
386
|
+
<button type="button" disabled={busy || Boolean(working)} onClick={() => { submissions.dismissUnconfirmed(submission); invalidate() }}>清除本窗口提醒</button>
|
|
387
|
+
</details>}
|
|
388
|
+
</section>
|
|
389
|
+
const savedSubmissionForm = !submission && snapshot.availableActions?.includes('botRouting.status') && <details className="bp-technical">
|
|
390
|
+
<summary>核对已保存的提交</summary><p>请连接原宿主并粘贴之前保存的核对码。此操作只查询原记录,不会创建任务。</p>
|
|
391
|
+
<form className="bp-submission-restore" onSubmit={restoreSubmission}>
|
|
392
|
+
<label>已保存的提交核对码<input aria-label="已保存的提交核对码" className="bp-submission-code" value={savedCode} maxLength={1024} autoComplete="off" spellCheck={false} disabled={busy || Boolean(working)} onChange={event => setSavedCode(event.target.value)} placeholder="dsh-route-v1:…" /></label>
|
|
393
|
+
<button type="submit" disabled={busy || Boolean(working) || !savedCode.trim()}>核对已保存的提交</button>
|
|
394
|
+
</form>
|
|
395
|
+
</details>
|
|
273
396
|
if (!view) {
|
|
274
397
|
return <article className="bp-card"><div className="bp-card-title"><h2>Bot 路由</h2></div>
|
|
275
|
-
<p className="bp-muted">{error || '正在读取候选…'}</p><button type="button" disabled={busy || working} onClick={() => { void load() }}>读取候选</button
|
|
398
|
+
<p className="bp-muted">{error || '正在读取候选…'}</p><button type="button" disabled={busy || working} onClick={() => { void load() }}>读取候选</button>{submissionNotice}{savedSubmissionForm}</article>
|
|
276
399
|
}
|
|
277
400
|
const machines = (snapshot.machines?.items ?? []).filter(item => item.status !== 'revoked')
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
401
|
+
const inputDisabled = busy || !viewReady || working === 'dispatch' || working === 'settings'
|
|
402
|
+
const nameOf = botId => view.candidates.find(row => row.botId === botId)?.name ?? botId
|
|
403
|
+
return <article className="bp-card"><div className="bp-card-title"><h2>Bot 路由</h2><Tag>{view.enabled ? `已开启 · ${routingPresetLabel(view.preset)}` : '未开启'}</Tag>
|
|
404
|
+
<button type="button" disabled={busy || Boolean(working)} onClick={() => { void load() }}>刷新候选</button></div>
|
|
405
|
+
<div className="bp-actions"><label className="bp-check"><input type="checkbox" checked={view.enabled} disabled={busy || Boolean(working) || !viewReady} onChange={event => { void saveSettings({ enabled: event.target.checked, preset: view.preset, autoGrant: view.autoGrant }) }} />开启 Bot 路由</label>
|
|
406
|
+
<label>策略<select value={view.preset} disabled={busy || Boolean(working) || !viewReady} onChange={event => { void saveSettings({ enabled: view.enabled, preset: event.target.value, autoGrant: view.autoGrant }) }}>
|
|
282
407
|
<option value="economy">省钱优先</option><option value="balanced">均衡</option><option value="quality">效果优先</option></select></label>
|
|
283
|
-
<label className="bp-check"><input type="checkbox" checked={view.autoGrant?.machineTools === true} disabled={busy || working} onChange={event => { void saveSettings({ enabled: view.enabled, preset: view.preset, autoGrant: { ...view.autoGrant, machineTools: event.target.checked } }) }} />自动授予任务所需电脑权限</label></div>
|
|
408
|
+
<label className="bp-check"><input type="checkbox" checked={view.autoGrant?.machineTools === true} disabled={busy || Boolean(working) || !viewReady} onChange={event => { void saveSettings({ enabled: view.enabled, preset: view.preset, autoGrant: { ...view.autoGrant, machineTools: event.target.checked } }) }} />自动授予任务所需电脑权限</label></div>
|
|
284
409
|
{!view.enabled && <p className="bp-muted">开启后,主 Bot 可根据各 Bot 已配置的模型、工具与电脑权限分配任务。手动指定也会检查是否具备执行条件。</p>}
|
|
285
410
|
{error && <p className="bp-error" role="alert">{error}</p>}
|
|
286
|
-
{
|
|
411
|
+
{submissionNotice}
|
|
412
|
+
{savedSubmissionForm}
|
|
413
|
+
{!viewReady && !working && <p className="bp-muted">候选尚未更新,请刷新候选后再预览。</p>}
|
|
414
|
+
{view.enabled ? <div className="bp-table-wrap"><table className="bp-routing-candidates">
|
|
287
415
|
<thead><tr><th>Bot</th><th>模型</th><th>机器</th><th>价格/百万</th><th>质量</th><th>状态</th></tr></thead>
|
|
288
416
|
<tbody>{view.candidates.map(row => <tr key={row.botId}>
|
|
289
417
|
<td>{row.name}{row.main ? <Tag>Main</Tag> : null}<small>{row.botId}</small></td>
|
|
@@ -294,78 +422,51 @@ function BotRoutingCard({ snapshot, dispatch, busy }) {
|
|
|
294
422
|
<td>{row.busy ? '正忙' : '空闲'}</td>
|
|
295
423
|
</tr>)}</tbody></table></div> : null}
|
|
296
424
|
{view.enabled ? <div className="bp-form">
|
|
297
|
-
<label>任务描述<textarea rows={2} value={text} onChange={event => {
|
|
298
|
-
<label>路由方式<select value={pin} onChange={event => {
|
|
425
|
+
<label>任务描述<textarea rows={2} value={text} disabled={inputDisabled} onChange={event => editDraft({ text: event.target.value })} placeholder="例如:整理上周的发布记录" /></label>
|
|
426
|
+
<label>路由方式<select value={pin} disabled={inputDisabled} onChange={event => editDraft({ pin: event.target.value })}>
|
|
299
427
|
<option value="">自动路由(按预设打分)</option>
|
|
300
428
|
{view.candidates.map(row => <option key={row.botId} value={row.botId}>指定:{row.name}</option>)}
|
|
301
429
|
</select></label>
|
|
302
|
-
<label>需要远程电脑<select value={machine} onChange={event => {
|
|
430
|
+
<label>需要远程电脑<select value={machine} disabled={inputDisabled} onChange={event => editDraft({ machine: event.target.value, root: '', effect: 'read' })}>
|
|
303
431
|
<option value="">不需要</option>
|
|
304
432
|
{machines.map(item => <option key={item.id} value={item.id}>{item.name ?? item.id}</option>)}
|
|
305
433
|
</select></label>
|
|
306
|
-
{machine && <label>共享目录<select value={root || machines.find(item => item.id === machine)?.roots?.[0] || ''} onChange={event => {
|
|
434
|
+
{machine && <label>共享目录<select value={root || machines.find(item => item.id === machine)?.roots?.[0] || ''} disabled={inputDisabled} onChange={event => editDraft({ root: event.target.value })}>
|
|
307
435
|
{(machines.find(item => item.id === machine)?.roots ?? []).map(id => <option key={id} value={id}>{id}</option>)}
|
|
308
436
|
</select></label>}
|
|
309
|
-
<label>任务需要的操作<select value={effect} onChange={event => {
|
|
437
|
+
<label>任务需要的操作<select value={effect} disabled={inputDisabled} onChange={event => editDraft({ effect: event.target.value })}>
|
|
310
438
|
<option value="read">只读 / 分析</option><option value="write">写入文件</option>
|
|
311
439
|
{machine && <><option value="desktop">操作 Windows 桌面</option><option value="browser">操作远程浏览器</option></>}
|
|
312
440
|
</select></label>
|
|
313
|
-
<button type="button" disabled={busy || working || !text.trim()} onClick={() => { void decide() }}>{working ? '计算中…' : '预览路由'}</button>
|
|
314
|
-
{error && <p className="bp-error" role="alert">{error}</p>}
|
|
441
|
+
<button type="button" disabled={busy || Boolean(working) || Boolean(submission) || !viewReady || !text.trim()} onClick={() => { void decide() }}>{working === 'preview' ? '计算中…' : '预览路由'}</button>
|
|
315
442
|
</div> : null}
|
|
316
|
-
{decision ? <div className="bp-blocked">
|
|
443
|
+
{decision ? <div className="bp-blocked" role="region" aria-label="路由预览结果">
|
|
317
444
|
<strong>选中:{decision.chosen ? `${decision.chosen.name ?? decision.chosen.botId}${decision.pinned ? '(手动指定)' : ''}` : '没有可用的 Bot'}</strong>
|
|
318
|
-
{decision.
|
|
319
|
-
{decision.
|
|
445
|
+
<p>{decision.pinned ? '手动指定的 Bot 已通过本次执行条件检查,未与其他 Bot 比较评分。' : `按${routingPresetLabel(decision.preset)}策略比较,综合评分越低越靠前。`}</p>
|
|
446
|
+
{decision.binding ? <><p>所需电脑权限:{decision.binding.mode === 'read-only' ? '只读' : '读写'} · {decision.binding.tools.length} 项工具。</p>
|
|
447
|
+
<p>{decision.grant?.autoGrant === false ? '自动授权已关闭,创建任务时会核对已有授权;权限不足时需先配置。' : '创建任务时才会按已保存的权限上限核对和授权。'}</p></> : null}
|
|
448
|
+
{decision.rejected?.length ? <p>未满足条件:{decision.rejected.map(row => `${nameOf(row.botId)}(${routingRefusalMessage(row.reason)})`).join(';')}</p> : null}
|
|
449
|
+
{!decision.pinned && decision.ranked?.length > 0 && <details className="bp-technical"><summary>查看比较依据</summary>
|
|
450
|
+
<p>评分只用于本次候选排序,不代表成功率。未知价格按保守值计算,未知质量按中间值参与比较。</p>
|
|
451
|
+
<div className="bp-table-wrap"><table className="bp-routing-comparison" aria-label="候选比较"><thead><tr><th>Bot</th><th>综合评分</th><th>价格/百万</th><th>参考质量</th></tr></thead>
|
|
452
|
+
<tbody>{decision.ranked.map(row => <tr key={row.botId}><td>{nameOf(row.botId)}</td><td>{Number.isFinite(row.score) ? row.score.toFixed(6) : '未知'}</td>
|
|
453
|
+
<td>{Number.isFinite(row.pricePerMTokUsd) ? `$${row.pricePerMTokUsd}` : '未知'}</td><td>{Number.isFinite(row.quality) ? row.quality : '未知'}</td></tr>)}</tbody></table></div>
|
|
454
|
+
</details>}
|
|
455
|
+
{(decision.binding || decision.rejected?.length > 0) && <details className="bp-technical"><summary>执行条件详情</summary>
|
|
456
|
+
{decision.binding && <p>所需工具:{decision.binding.tools.join('、')}</p>}
|
|
457
|
+
{decision.rejected?.map(row => <p key={row.botId}>{row.botId}:{row.reason}</p>)}
|
|
458
|
+
</details>}
|
|
320
459
|
{decision.chosen ? <div className="bp-actions">
|
|
321
|
-
<button type="button" disabled={busy} onClick={() => { void submitRouted() }}>按此路由创建任务</button>
|
|
322
|
-
{dispatched ? <small role="status">已提交:{dispatched.plan?.taskId ?? dispatched.submitted?.taskId ?? '已受理'}</small> : null}
|
|
460
|
+
<button type="button" disabled={busy || Boolean(working) || !viewReady || Boolean(submission)} onClick={() => { void submitRouted() }}>按此路由创建任务</button>
|
|
323
461
|
</div> : null}
|
|
324
462
|
</div> : null}
|
|
325
463
|
</article>
|
|
326
464
|
}
|
|
327
465
|
|
|
328
|
-
export function Tasks({ snapshot, dispatch, busy }) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const rows = section.items
|
|
333
|
-
const task = rows.find(row => (row.taskId ?? row.id) === selected)
|
|
334
|
-
const actions = snapshot.availableActions ?? []
|
|
335
|
-
return <><BotAttention snapshot={snapshot} dispatch={dispatch} busy={busy} openTask={setSelected} />{section.plans?.map((plan, index)=><article className="bp-card" key={plan.planId}>
|
|
336
|
-
<div className="bp-card-title"><div><h2>协作计划 {index + 1}</h2><p className="bp-muted">{plan.packages.length} 个工作包 · {plan.packages.filter(item => item.state === 'succeeded').length} 个已完成</p></div><div className="bp-actions"><Tag>{TASK_STATES[plan.state]??plan.state}</Tag>
|
|
337
|
-
{actions.includes('task.plan.cancel')&&!['succeeded','cancelled'].includes(plan.state)&&<button disabled={busy} onClick={()=>dispatch('task.plan.cancel',{botId:plan.ownerBotId,planId:plan.planId})}>停止整个计划</button>}</div></div>
|
|
338
|
-
{plan.needsReconciliation&&<p role="status">有工作包停止尚未确认,请核对原收据;计划尚未完成停止。</p>}
|
|
339
|
-
<ul className="bp-plan-list">{plan.packages.map((item, step) => {
|
|
340
|
-
const linkedTask = rows.find(row => (row.taskId ?? row.id) === item.taskId)
|
|
341
|
-
return <li key={item.id}><span className="bp-plan-step">{item.state === 'succeeded' ? <UiIcon name="check" size={13} /> : step + 1}</span><div className="bp-plan-copy"><strong>{linkedTask?.title ?? linkedTask?.input?.task ?? plan.plan?.packages?.find(pkg => pkg.id === item.id)?.objective ?? `工作包 ${step + 1}`}</strong><small>{snapshot.bots?.find(bot=>bot.id===item.targetBotId)?.name??item.targetBotId}
|
|
342
|
-
{item.dependsOn.length>0&&` · 前置:${item.dependsOn.join('、')}`}{item.result?.code==='DEPENDENCY_FAILED'&&' · 前置包失败,尚未执行'}</small></div><Tag>{TASK_STATES[item.state]??item.state}</Tag></li>
|
|
343
|
-
})}</ul><details className="bp-technical bp-plan-id"><summary>计划编号</summary><p className="bp-code">{plan.planId}</p></details>
|
|
344
|
-
</article>)}{rows.length === 0 ? <p className="bp-empty">尚无持久任务。在 Main 私聊中提出目标后,可在这里查看工作包、执行尝试与收据。</p> : <div className="bp-table-wrap"><table>
|
|
345
|
-
<thead><tr><th>任务</th><th>Bot</th><th>状态</th><th>执行尝试</th><th>操作</th></tr></thead><tbody>{rows.map((row, index) => {
|
|
346
|
-
const id = row.taskId ?? row.id
|
|
347
|
-
const canCancel = !['cancelled', 'succeeded', 'failed', 'skipped'].includes(row.state)
|
|
348
|
-
const remote = row.result?.remote
|
|
349
|
-
// A terminal Task state alone does not prove that the original remote
|
|
350
|
-
// operation stopped. The host still makes the authoritative decision.
|
|
351
|
-
const stopKnown = !remote || remote.stopConfirmed === true
|
|
352
|
-
return <tr key={id}><td><button className="bp-text-button" onClick={() => setSelected(id)}>{row.title ?? row.input?.task ?? `任务 ${index + 1}`}</button></td>
|
|
353
|
-
<td>{snapshot.bots?.find(bot => bot.id === row.botId)?.name ?? row.botId ?? '未取得归属'}</td><td><Tag>{TASK_STATES[row.state] ?? row.state}</Tag>{row.blockedReason && <small>{row.blockedReason}</small>}
|
|
354
|
-
{remote&&(!stopKnown?<small role="status">写入或停止尚未确认,请核对原操作;不会自动重发,也暂不能重试。</small>:remote.notStarted===true&&remote.possiblyApplied!==true?<small>原操作未启动。</small>:remote.status==='succeeded'?<small>写入已确认。{row.state==='cancelled'?'取消未撤销文件变更。':''}</small>:remote.possiblyApplied===true&&<small>操作已停止,文件可能已改变;重试不会撤销已有变更,请先核对产物。</small>)}
|
|
355
|
-
</td>
|
|
356
|
-
<td>{Number.isSafeInteger(row.attempts)?row.attempts:row.attempts?.length ?? row.attemptCount ?? '未知'}</td><td className="bp-actions">
|
|
357
|
-
{actions.includes('task.cancel') && canCancel && <button disabled={busy} onClick={() => dispatch('task.cancel', { botId:row.botId,taskId: id })}>停止</button>}
|
|
358
|
-
{actions.includes('task.retry') && ['failed', 'cancelled', 'waiting-budget'].includes(row.state) && <button disabled={busy || !stopKnown} onClick={() => { if (stopKnown) dispatch('task.retry', { botId:row.botId,taskId: id, requestKey: crypto.randomUUID() }) }}>重试</button>}
|
|
359
|
-
{actions.includes('task.reconcile') && row.state === 'needs-reconciliation' && <button disabled={busy} onClick={() => dispatch('task.reconcile', { botId:row.botId,taskId: id, requestKey: crypto.randomUUID() })}>核对原收据</button>}
|
|
360
|
-
</td></tr>
|
|
361
|
-
})}</tbody></table></div>}
|
|
362
|
-
{task && <article className="bp-card"><div className="bp-card-title"><h2>任务详情</h2><button onClick={() => setSelected(null)}>关闭</button></div>
|
|
363
|
-
<dl className="bp-facts"><Value label="Task">{task.taskId ?? task.id}</Value><Value label="Package">{task.packageId}</Value><Value label="Attempt">{task.attemptId}</Value><Value label="Operation">{task.operationId}</Value>
|
|
364
|
-
<Value label="期限">{task.deadlineAt ? new Date(task.deadlineAt).toLocaleString('zh-CN') : '未取得'}</Value><Value label="MR 运行">{task.legacyRunIds?.join('、')}</Value></dl>
|
|
365
|
-
<h3>执行与收据依据</h3><pre>{JSON.stringify({ attempts: task.attempts, executionRef: task.executionRef, remoteReceipt:task.result?.remote,reconciliation: task.reconciliation, packages: task.packages }, null, 2)}</pre>
|
|
366
|
-
</article>}
|
|
367
|
-
{snapshot.availableActions?.includes('botRoutingInspect') && <BotRoutingCard snapshot={snapshot} dispatch={dispatch} busy={busy} />}
|
|
368
|
-
</>
|
|
466
|
+
export function Tasks({ snapshot, dispatch, busy, submissionScope = dispatch, inspect, openSession }) {
|
|
467
|
+
if (snapshot.tasks?.status !== 'available') return <Blocked value={snapshot.tasks} />
|
|
468
|
+
return <TaskWorkspace snapshot={snapshot} dispatch={dispatch} busy={busy} scope={submissionScope} inspect={inspect} openSession={openSession}
|
|
469
|
+
routingCard={snapshot.availableActions?.includes('botRoutingInspect') ? <BotRoutingCard snapshot={snapshot} dispatch={dispatch} busy={busy} submissionScope={submissionScope} /> : null} />
|
|
369
470
|
}
|
|
370
471
|
|
|
371
472
|
|
|
@@ -805,7 +906,7 @@ export function ProductPage({ section, productApi, openSession, openPanel }) {
|
|
|
805
906
|
void loop()
|
|
806
907
|
return () => { stopped = true; state.live = false; state.epoch++; state.request++; controller.abort(); clearTimeout(timer) }
|
|
807
908
|
}, [refresh])
|
|
808
|
-
const dispatch = useCallback(async (action, payload) => {
|
|
909
|
+
const dispatch = useCallback(async (action, payload, { errorResult = false } = {}) => {
|
|
809
910
|
if (dispatchState.current.snapshotStale || dispatchState.current.readOnly || snapshotApi.current !== productApi || !lifecycle.current.live) return
|
|
810
911
|
const state = lifecycle.current, epoch = state.epoch
|
|
811
912
|
const current = () => state.live && state.epoch === epoch
|
|
@@ -816,10 +917,14 @@ export function ProductPage({ section, productApi, openSession, openPanel }) {
|
|
|
816
917
|
await refresh()
|
|
817
918
|
if (current() && result?.warning) setError('宿主已撤销授权,但远程停止尚未确认;请恢复连接并检查 Agent。')
|
|
818
919
|
return result
|
|
819
|
-
} catch (cause) {
|
|
920
|
+
} catch (cause) {
|
|
921
|
+
if (current()) { state.request++; setSnapshotStale(true); setError(errorResult ? '宿主请求未完成,请查看操作区域的提示并刷新记录。' : cause.message) }
|
|
922
|
+
if (errorResult) return { ok: false, error: { code: cause?.code ?? 'PRODUCT_REQUEST_FAILED', message: String(cause?.message ?? cause) } }
|
|
923
|
+
} finally {
|
|
820
924
|
if (current()) { pendingActions.current = Math.max(0, pendingActions.current - 1); setBusy(pendingActions.current > 0) }
|
|
821
925
|
}
|
|
822
926
|
}, [productApi, refresh])
|
|
927
|
+
const inspectTask = useCallback((payload, signal) => productApi.dispatch('task.inspect', payload, signal), [productApi])
|
|
823
928
|
const disabled = busy || snapshotStale || snapshotApi.current !== productApi || Boolean(snapshot?.readOnly)
|
|
824
929
|
const title = { bindings: 'Bot 绑定与权限', tasks: '任务', machines: '远程电脑', library: '资料库', routine: '例行任务', usage: '费用与用量' }[section]
|
|
825
930
|
const meta = {
|
|
@@ -839,7 +944,7 @@ export function ProductPage({ section, productApi, openSession, openPanel }) {
|
|
|
839
944
|
{snapshot?.readOnly && <Blocked value={{ reason: snapshot.readOnly, nextAction: '关闭持有写锁的另一宿主后重新加载。' }} />}
|
|
840
945
|
{!snapshot && !error && <div className="bp-loading" role="status"><span className="bp-loading-line" aria-hidden="true" /><span className="bp-loading-line" aria-hidden="true" /><p>正在读取宿主状态…</p></div>}
|
|
841
946
|
{snapshot && <>{section === 'bindings' && <><BotBindings snapshot={snapshot} openSession={openSession} dispatch={dispatch} busy={disabled} />{snapshot.availableActions?.includes('roles.list') && <RoleCatalog dispatch={dispatch} busy={disabled} />}</>}
|
|
842
|
-
{section === 'tasks' && <Tasks snapshot={snapshot} dispatch={dispatch} busy={disabled} />}
|
|
947
|
+
{section === 'tasks' && <Tasks snapshot={snapshot} dispatch={dispatch} busy={disabled} submissionScope={productApi} inspect={!snapshotStale && snapshotApi.current === productApi && !snapshot.readOnly ? inspectTask : undefined} openSession={openSession} />}
|
|
843
948
|
{section === 'machines' && <Machines snapshot={snapshot} dispatch={dispatch} busy={disabled} />}
|
|
844
949
|
{section === 'routine' && <RoutinesPanel snapshot={snapshot} dispatch={dispatch} busy={disabled} onOpenMachines={() => openPanel(PRODUCT_PANELS.machines)} />}
|
|
845
950
|
{section === 'library' && <Library snapshot={snapshot} dispatch={dispatch} busy={disabled} />}
|
|
@@ -97,6 +97,35 @@ html body[data-bot-product-ui][data-ds-dark-theme] {
|
|
|
97
97
|
.bp-person-copy { min-width: 0; }
|
|
98
98
|
.bp-person-copy strong { font-size: 13px; font-weight: 550; }
|
|
99
99
|
.bp-plan-list { list-style: none; display: grid; gap: 0; margin: 12px 0 0; padding: 0; }
|
|
100
|
+
.bp-task-counts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin: 18px 0; }
|
|
101
|
+
.bp-task-overview .bp-card-title > .bp-tag { flex-shrink: 0; white-space: nowrap; }
|
|
102
|
+
.bp-task-counts button { display: grid; justify-items: start; gap: 2px; padding: 14px 16px; text-align: left; background: var(--bp-subtle); border-color: transparent; }
|
|
103
|
+
.bp-task-counts strong { font-size: 25px; font-weight: 600; line-height: 1.3; }
|
|
104
|
+
.bp-task-counts span { color: var(--bp-muted); font-size: 12px; }
|
|
105
|
+
.bp-task-counts button[aria-pressed=true], .bp-task-bots button[aria-pressed=true] { border-color: var(--bp-focus); background: var(--bp-surface); }
|
|
106
|
+
.bp-task-bots { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
107
|
+
.bp-task-bots button { text-align: left; max-width: 100%; overflow-wrap: anywhere; }
|
|
108
|
+
.bp-task-bots small { margin: 1px 0 0; font-size: 10px; }
|
|
109
|
+
.bp-task-search { display: flex; align-items: flex-end; gap: 12px; margin: 10px 0 18px; }
|
|
110
|
+
.bp-task-search label { display: grid; gap: 6px; width: min(100%, 520px); color: var(--bp-muted); font-size: 12px; }
|
|
111
|
+
.bp-task-search input { width: 100%; color: var(--bp-ink); }
|
|
112
|
+
.bp-task-detail { scroll-margin-top: 16px; }
|
|
113
|
+
.bp-task-detail .bp-card-title p { overflow-wrap: anywhere; }
|
|
114
|
+
.bp-task-detail .bp-actions { flex-wrap: wrap; }
|
|
115
|
+
.bp-task-timeline { list-style: none; margin: 20px 0; padding: 0; }
|
|
116
|
+
.bp-task-timeline li { position: relative; display: flex; gap: 15px; padding: 0 0 20px; }
|
|
117
|
+
.bp-task-timeline li::before { content: ''; position: absolute; top: 12px; bottom: 0; left: 4px; width: 1px; background: var(--bp-border); }
|
|
118
|
+
.bp-task-timeline li:last-child::before { display: none; }
|
|
119
|
+
.bp-timeline-dot { width: 9px; height: 9px; margin-top: 7px; border: 2px solid var(--bp-focus); background: var(--bp-surface); border-radius: 50%; flex-shrink: 0; }
|
|
120
|
+
.bp-task-timeline strong { font-size: 13px; font-weight: 550; }
|
|
121
|
+
.bp-task-plans > button { margin: 0 0 16px; }
|
|
122
|
+
@container (max-width: 600px) {
|
|
123
|
+
.bp-task-counts { gap: 6px; }
|
|
124
|
+
.bp-task-counts button { padding: 12px 9px; }
|
|
125
|
+
.bp-task-counts strong { font-size: 21px; }
|
|
126
|
+
.bp-task-search { flex-wrap: wrap; }
|
|
127
|
+
.bp-attention-row > .bp-actions { flex-wrap: wrap; }
|
|
128
|
+
}
|
|
100
129
|
.bp-plan-list li { display: flex; align-items: flex-start; gap: 12px; padding: 13px 0; border-top: 1px solid var(--bp-border); }
|
|
101
130
|
.bp-plan-step { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; flex: none; margin-top: 1px; border: 1px solid var(--bp-border); border-radius: 7px; color: var(--bp-muted); background: var(--bp-subtle); font-size: 11px; }
|
|
102
131
|
.bp-plan-copy { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
@@ -137,6 +166,10 @@ html body[data-bot-product-ui][data-ds-dark-theme] {
|
|
|
137
166
|
.bp-page tbody tr:last-child td { border-bottom: 0; }
|
|
138
167
|
.bp-page tbody tr:hover { background: var(--bp-subtle); }
|
|
139
168
|
.bp-page td:first-child { max-width: 360px; overflow-wrap: anywhere; }
|
|
169
|
+
.bp-page .bp-routing-candidates { min-width: 680px; }
|
|
170
|
+
.bp-page .bp-routing-comparison { min-width: 480px; }
|
|
171
|
+
.bp-page :is(.bp-routing-candidates, .bp-routing-comparison) :is(th,td):first-child { min-width: 128px; }
|
|
172
|
+
.bp-page .bp-routing-comparison td:nth-child(2) { white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
140
173
|
.bp-page .bp-text-button { background: transparent; border: 0; padding: 0; min-height: 24px; text-align: left; justify-content: flex-start; font-weight: 500; }
|
|
141
174
|
.bp-page pre { overflow: auto; max-height: 400px; margin: 14px 0; padding: 15px; border: 1px solid var(--bp-border); border-radius: 9px; background: var(--bp-subtle); font: 11px/1.7 ui-monospace,SFMono-Regular,Consolas,monospace; }
|
|
142
175
|
.bp-code { overflow-wrap: anywhere; font: 11px/1.8 ui-monospace,SFMono-Regular,Consolas,monospace; color: var(--bp-muted); }
|
|
@@ -262,6 +295,9 @@ body[data-bot-product-ui] .mr-workspace { background: var(--bp-bg); color: var(-
|
|
|
262
295
|
@media (prefers-reduced-motion: reduce) {
|
|
263
296
|
.bp-page *, .bp-nav *, .bp-desktop-viewer * { animation: none !important; transition: none !important; }
|
|
264
297
|
}
|
|
298
|
+
.bp-submission-code { width: 100%; min-width: 0; box-sizing: border-box; font-family: var(--bp-mono, monospace); }
|
|
299
|
+
.bp-submission-restore { display: grid; gap: 10px; margin-top: 10px; }
|
|
300
|
+
.bp-submission-restore > button { justify-self: start; }
|
|
265
301
|
/* Sidebar width belongs to the Bot surface, independently of the Harness rail. */
|
|
266
302
|
.bp-nav { container-name: workspace-navigation; container-type: inline-size; }
|
|
267
303
|
.bp-nav-compact { padding: 14px 11px 10px; gap: 2px; }
|