dsh-deepmoyu 2.7.2 → 2.8.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/AGENTS.md +9 -0
- package/CHANGELOG.md +28 -0
- package/CONTRIBUTING.md +1 -0
- package/README.md +10 -3
- package/client.js +423 -286
- package/docs/ACTIVITY_TUNING.md +50 -0
- package/docs/CONTINUOUS_BROWSING.md +35 -0
- package/docs/FUN_FEATURE_REFERENCES.md +128 -0
- package/docs/LOCAL_COMPAT_AUDIT_2026-11-12.md +58 -0
- package/docs/NATURAL_AUTONOMY.md +91 -0
- package/docs/PRIVACY.md +7 -0
- package/docs/RELEASE_2_8_0.md +27 -0
- package/docs/RELEASE_AUDIT.md +14 -0
- package/docs/TEST_MATRIX.md +9 -0
- package/docs/WORKFLOW_REFERENCES.md +57 -0
- package/docs/tool-evidence.json +6 -1
- package/index.js +676 -222
- package/lib/background-tools.mjs +1 -1
- package/lib/browse-preferences.mjs +154 -0
- package/lib/browse-progress.mjs +165 -0
- package/lib/comment.mjs +29 -46
- package/lib/config-policy.mjs +2 -1
- package/lib/curiosity.mjs +153 -0
- package/lib/learn-review.mjs +2 -2
- package/lib/learn-store.mjs +7 -0
- package/lib/learn.mjs +234 -69
- package/lib/mcp.mjs +5 -2
- package/lib/memory-center.mjs +21 -19
- package/lib/mode.mjs +23 -9
- package/lib/model-relay-adapter.mjs +1 -1
- package/lib/model-relay.mjs +15 -2
- package/lib/pace.mjs +15 -7
- package/lib/persona.mjs +38 -29
- package/lib/product-status.mjs +38 -2
- package/lib/prompts.mjs +38 -37
- package/lib/pv.py +26 -12
- package/lib/pv_editorial.py +5 -2
- package/lib/session-continuation.mjs +47 -0
- package/package.json +1 -1
- package/presets/deepmoyu.patch.yml +8 -10
- package/prompts/duty.md +30 -38
- package/vendor/bilibili-mcp/mcp_server.py +88 -15
- package/vendor/bilibili-mcp/runtime_guard.py +132 -25
package/client.js
CHANGED
|
@@ -980,6 +980,16 @@ window.__ModuleLoader__.load({
|
|
|
980
980
|
[data-bd-segments]{border-color:#d5e7f5;background:#edf6fd;padding:4px;border-radius:10px;}
|
|
981
981
|
[data-bd-segments] button{color:#466b84;padding:8px 13px;min-height:34px;}
|
|
982
982
|
[data-bd-segments] button[aria-pressed="true"]{background:#fff;color:#126a9a;box-shadow:0 1px 5px #1b639516;}
|
|
983
|
+
[data-bd-browse-settings] [data-bd-browse-topic][aria-pressed="false"]{background:transparent;border-color:transparent;box-shadow:none;}
|
|
984
|
+
[data-bd-browse-settings] [data-bd-browse-topic][aria-pressed="true"]{background:#d7edfa;border-color:#79b8db;color:#114e76;font-weight:700;box-shadow:0 1px 3px #1b63951c;}
|
|
985
|
+
[data-bd-browse-settings] > [data-bd-field]{flex:none;}
|
|
986
|
+
[data-bd-browse-common]{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:12px;align-items:start;}
|
|
987
|
+
[data-bd-browse-common]>[data-bd-field],[data-bd-browse-refine]>div>[data-bd-field]{flex:none;min-width:0;}
|
|
988
|
+
[data-bd-browse-common] textarea{min-height:66px;resize:vertical;}
|
|
989
|
+
[data-bd-pet-preferences],[data-bd-companion-settings],[data-bd-review-controls]{display:flex;flex-direction:column;gap:12px;}
|
|
990
|
+
[data-bd-review-controls]{padding:12px 0 16px;border-bottom:1px solid #d7e8f4;}
|
|
991
|
+
[data-bd-advanced]>summary{display:list-item;cursor:pointer;line-height:1.8;}
|
|
992
|
+
@media(max-width:660px){[data-bd-browse-common]{grid-template-columns:minmax(0,1fr);}}
|
|
983
993
|
[data-bd-pet-modal] [data-bd-sec]{background:#fff;border-color:#d7e8f4;border-radius:13px;box-shadow:0 3px 12px #24649204;padding:18px;gap:12px;}
|
|
984
994
|
[data-bd-pet-modal] [data-bd-h] b{color:#2a4d65;}
|
|
985
995
|
[data-bd-pet-modal] [data-bd-note]{color:#537289;line-height:1.7;}
|
|
@@ -1004,6 +1014,27 @@ window.__ModuleLoader__.load({
|
|
|
1004
1014
|
[data-bd-run-details]>summary{cursor:pointer;color:#3b6681;font-size:12px;}
|
|
1005
1015
|
[data-bd-run-details][open]>summary{margin-bottom:14px;}
|
|
1006
1016
|
[data-bd-run-details]>[data-bd-sec]{margin-bottom:14px;}
|
|
1017
|
+
[data-bd-curiosity-journal]{border:1px solid #d8e4e8;border-radius:13px;background:#fffdf8;padding:14px 17px;}
|
|
1018
|
+
[data-bd-curiosity-journal]>summary{cursor:pointer;color:#446879;font-size:12px;line-height:1.8;}
|
|
1019
|
+
[data-bd-journal-title]{font-weight:700;color:#365e76;margin-right:12px;}
|
|
1020
|
+
[data-bd-journal-content],[data-bd-journal-explore],[data-bd-journal-finds]{display:flex;flex-direction:column;gap:13px;min-width:0;}
|
|
1021
|
+
[data-bd-journal-content]{padding-top:16px;}
|
|
1022
|
+
[data-bd-journal-form]{display:flex;flex-wrap:wrap;align-items:flex-end;gap:10px;}
|
|
1023
|
+
[data-bd-journal-form] [data-bd-field]{margin:0;flex:1 1 300px;}
|
|
1024
|
+
[data-bd-journal-form] input{width:100%;box-sizing:border-box;}
|
|
1025
|
+
[data-bd-curiosity-id],[data-bd-find-id]{border:1px solid #e6dfd2;border-radius:11px;padding:15px;background:#fffefa;min-width:0;overflow-wrap:anywhere;}
|
|
1026
|
+
[data-bd-curiosity-id] h3{font-size:15px;line-height:1.7;font-weight:600;color:#284f67;margin:10px 0;}
|
|
1027
|
+
[data-bd-curiosity-id] p{margin:0 0 12px;}
|
|
1028
|
+
[data-bd-journal-manage]{flex:none;}
|
|
1029
|
+
[data-bd-journal-manage] summary{font-size:11px;cursor:pointer;color:#5d7482;}
|
|
1030
|
+
[data-bd-journal-manage]>div{margin-top:8px;}
|
|
1031
|
+
[data-bd-journal-eyebrow]{font-size:11px;color:#8e7957;}
|
|
1032
|
+
[data-bd-find-id] blockquote{font-size:17px;line-height:1.9;margin:13px 0 16px;color:#365566;white-space:pre-wrap;}
|
|
1033
|
+
[data-bd-find-id]>a{display:inline-block;margin-bottom:15px;}
|
|
1034
|
+
[data-bd-curiosity-journal] a{color:#137fa5;text-underline-offset:3px;}
|
|
1035
|
+
[data-bd-journal-source]{display:inline-block;margin-bottom:6px;}
|
|
1036
|
+
[data-bd-journal-empty]{padding:22px 15px;color:#6c7f88;line-height:1.8;background:#faf8f2;border-radius:9px;font-size:12px;}
|
|
1037
|
+
@media(max-width:660px){[data-bd-curiosity-journal]{padding:13px 14px;}[data-bd-curiosity-journal]>summary>[data-bd-note]{display:block;margin-left:14px;}[data-bd-journal-content] [data-bd-grow]{min-width:0;}[data-bd-journal-content]>[data-bd-row]{justify-content:space-between;}[data-bd-journal-content]>[data-bd-row]>[data-bd-grow]{display:none;}}
|
|
1007
1038
|
[data-bd-week-recovery]>div:only-child{grid-column:1/-1;}
|
|
1008
1039
|
[data-bd-pet-modal] [data-bd-settings-page="video"] [data-bd-pet-grid]>[data-bd-sec]{background:#fff;border-color:#d7e8f4;box-shadow:0 3px 12px #24649204;}
|
|
1009
1040
|
[data-bd-pet-modal] [data-bd-video-stage]:not([data-warn]){background:#e8f5ff;color:#216c94;border-color:#bfdff5;}
|
|
@@ -1353,172 +1384,6 @@ window.__ModuleLoader__.load({
|
|
|
1353
1384
|
})))
|
|
1354
1385
|
}
|
|
1355
1386
|
|
|
1356
|
-
/* 🌙 后台刷一轮…:headless 独立进程那一轮(不占对话)。模型下拉复用 /status 的
|
|
1357
|
-
* models 清单(拿不到就退手填,判据与设置页一致);token 预算以「万」为单位
|
|
1358
|
-
* 显示与输入,提交时换算成整数。开跑走 POST /pet/start {mode:'background'}。 */
|
|
1359
|
-
function PetBgPanel({ st, toast, onDone }) {
|
|
1360
|
-
const cfg = (st && st.config) || {}
|
|
1361
|
-
const rows = Array.isArray(st && st.models) ? st.models : []
|
|
1362
|
-
const [model, setModel] = React.useState(null) // null = 还没从配置初始化过('' 是合法值:默认模型)
|
|
1363
|
-
const [wan, setWan] = React.useState(null) // 同上;'' = 不限
|
|
1364
|
-
const [busy, setBusy] = React.useState(false)
|
|
1365
|
-
const [tail, setTail] = React.useState(null)
|
|
1366
|
-
const lock = React.useRef(false) // 防双发:state 的 busy 要等重渲染才生效,连点两下会漏
|
|
1367
|
-
React.useEffect(() => {
|
|
1368
|
-
if (model === null) setModel(String(cfg.backgroundModelId ?? cfg.modelId ?? ''))
|
|
1369
|
-
if (wan === null) {
|
|
1370
|
-
const t = Number(cfg.backgroundTokenBudget ?? cfg.tokenBudget) || 0
|
|
1371
|
-
setWan(t > 0 ? String(Math.round(t / 10000)) : '')
|
|
1372
|
-
}
|
|
1373
|
-
}, [cfg])
|
|
1374
|
-
/* 日志尾巴:后台那轮「在不在动」只能靠这本账 —— 它不进对话、也不进活动流水 */
|
|
1375
|
-
const tailRev = React.useRef('')
|
|
1376
|
-
React.useEffect(() => {
|
|
1377
|
-
let dead = false
|
|
1378
|
-
const pull = async () => {
|
|
1379
|
-
try {
|
|
1380
|
-
const r = await jget('/pet/log?n=10' + (tailRev.current ? '&rev=' + encodeURIComponent(tailRev.current) : ''))
|
|
1381
|
-
if (!dead && r && r.ok !== false) {
|
|
1382
|
-
if (r.unchanged) return // ⭐ 没新增行就不动(省一次解析+渲染)
|
|
1383
|
-
if (r.rev) tailRev.current = String(r.rev)
|
|
1384
|
-
setTail(Array.isArray(r.entries) ? r.entries : [])
|
|
1385
|
-
}
|
|
1386
|
-
} catch { /* 下次轮询会补 */ }
|
|
1387
|
-
}
|
|
1388
|
-
pull()
|
|
1389
|
-
const t = setInterval(pull, 3000)
|
|
1390
|
-
return () => { dead = true; clearInterval(t) }
|
|
1391
|
-
}, [])
|
|
1392
|
-
const bgBusy = Boolean(st && st.bgBusy)
|
|
1393
|
-
const run = async () => {
|
|
1394
|
-
if (lock.current || busy || bgBusy) return // 绝不双发:上一轮还活着就不再叠一轮
|
|
1395
|
-
lock.current = true
|
|
1396
|
-
setBusy(true)
|
|
1397
|
-
try {
|
|
1398
|
-
const body = { mode: 'background', tokenBudget: Math.max(0, Math.round((parseFloat(wan) || 0) * 10000)) }
|
|
1399
|
-
if (model) body.model = model
|
|
1400
|
-
const r = await jpost('/pet/start', body)
|
|
1401
|
-
if (r && r.ok === false) {
|
|
1402
|
-
toast('后台没跑起来:' + (r.error || '未知原因') + (r.hint ? ' —— ' + r.hint : ''), 'warn')
|
|
1403
|
-
return
|
|
1404
|
-
}
|
|
1405
|
-
if (r && r.started) {
|
|
1406
|
-
toast('后台那轮跑起来了 —— 几分钟,做完日志里会有回执', 'good')
|
|
1407
|
-
if (onDone) onDone()
|
|
1408
|
-
} else {
|
|
1409
|
-
toast('后台没跑起来:' + ((r && r.error) || '可能上一轮还没结束'), 'warn')
|
|
1410
|
-
}
|
|
1411
|
-
} catch (e) { toast('后台没跑起来:' + String(e?.message ?? e), 'warn') }
|
|
1412
|
-
finally { lock.current = false; setBusy(false) }
|
|
1413
|
-
}
|
|
1414
|
-
const cur = model === null ? '' : String(model)
|
|
1415
|
-
const extra = cur && !rows.some((m) => m.id === cur) ? [{ id: cur, label: cur + '(不在当前清单里)' }] : []
|
|
1416
|
-
return h('div', { 'data-bd-pet-card': '', 'data-wide': '' },
|
|
1417
|
-
h('div', { 'data-bd-pet-cardhead': '' }, h('b', {}, '后台刷一轮'), h('span', {}, 'headless 独立进程')),
|
|
1418
|
-
h('div', { 'data-bd-pet-pad': '' },
|
|
1419
|
-
bgBusy ? h('span', { 'data-bd-pet-badge': '', 'data-warn': '' }, '后台正跑着呢 —— 等它做完再开下一轮') : null,
|
|
1420
|
-
h('div', { 'data-bd-row': '' },
|
|
1421
|
-
h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '模型'),
|
|
1422
|
-
rows.length
|
|
1423
|
-
? h('select', {
|
|
1424
|
-
'data-bd-input': '', 'data-bd-num': '', style: { width: '190px' },
|
|
1425
|
-
value: cur, onChange: (e) => setModel(e.target.value),
|
|
1426
|
-
},
|
|
1427
|
-
h('option', { value: '' }, '默认(跟着当前对话)'),
|
|
1428
|
-
extra.length ? h('option', { key: 'extra', value: extra[0].id }, extra[0].label) : null,
|
|
1429
|
-
rows.map((m) => h('option', { key: m.id, value: m.id }, m.label || m.id)))
|
|
1430
|
-
: h('input', {
|
|
1431
|
-
'data-bd-input': '', style: { width: '190px' }, placeholder: 'provider/model(留空 = 默认)',
|
|
1432
|
-
value: cur, onChange: (e) => setModel(e.target.value),
|
|
1433
|
-
})),
|
|
1434
|
-
h('div', { 'data-bd-row': '' },
|
|
1435
|
-
h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, 'token 预算(万 · 0 = 不限)'),
|
|
1436
|
-
h('input', {
|
|
1437
|
-
'data-bd-input': '', 'data-bd-num': '', style: { width: '88px' }, type: 'number', min: 0, step: 1,
|
|
1438
|
-
placeholder: '不限', value: wan === null ? '' : wan,
|
|
1439
|
-
onChange: (e) => setWan(e.target.value),
|
|
1440
|
-
})),
|
|
1441
|
-
h('div', { 'data-bd-note': '' }, '1 万 = 10000 token。后台预算是任务目标;实际运行时长在「设置 → 后台刷」中调整。'),
|
|
1442
|
-
h('div', { 'data-bd-row': '' },
|
|
1443
|
-
h('button', {
|
|
1444
|
-
'data-bd-btn': '', 'data-primary': '', disabled: busy || bgBusy, onClick: run,
|
|
1445
|
-
}, busy ? '开跑…' : '开跑'),
|
|
1446
|
-
busy ? h('i', { 'data-bd-pet-spin': '' }) : null)),
|
|
1447
|
-
tail && tail.length
|
|
1448
|
-
? h('div', {
|
|
1449
|
-
'data-bd-pet-log': '',
|
|
1450
|
-
style: { maxHeight: '122px', borderTop: '1px solid #d8e6ee' },
|
|
1451
|
-
},
|
|
1452
|
-
tail.slice(-4).map((e, i) => h('div', { 'data-bd-pet-logline': '', 'data-t': petEntryTone(e), key: i },
|
|
1453
|
-
h('time', {}, petFmtTime(e && e.at)), h('span', {}, petEntrySummary(e)))))
|
|
1454
|
-
: null)
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
/* 📜 运行日志:GET /pet/log,3 秒自动刷新;type 着色(start 绿 / stop 灰 / error 红 / note 蓝)。
|
|
1458
|
-
* 自动滚到底,但使用者往上翻时暂停自动滚(离底 <28px 才重新粘住),不然永远翻不上去。
|
|
1459
|
-
* 顶部「停止鲸鱼摸鱼」复用现有 POST /feiyu {action:'stop'}(跟设置页「停一下」同一个端点)。 */
|
|
1460
|
-
function PetLogPanel({ toast }) {
|
|
1461
|
-
const [entries, setEntries] = React.useState(null)
|
|
1462
|
-
const [busy, setBusy] = React.useState(false)
|
|
1463
|
-
const [err, setErr] = React.useState('')
|
|
1464
|
-
const listRef = React.useRef(null)
|
|
1465
|
-
const stick = React.useRef(true)
|
|
1466
|
-
const lock = React.useRef(false)
|
|
1467
|
-
const revRef = React.useRef('')
|
|
1468
|
-
React.useEffect(() => {
|
|
1469
|
-
let dead = false
|
|
1470
|
-
const pull = async () => {
|
|
1471
|
-
try {
|
|
1472
|
-
const r = await jget('/pet/log?n=200' + (revRef.current ? '&rev=' + encodeURIComponent(revRef.current) : ''))
|
|
1473
|
-
if (dead) return
|
|
1474
|
-
if (!r || r.ok === false) { setErr((r && r.error) || '读不到'); return }
|
|
1475
|
-
if (r.unchanged) return // ⭐ 日志没新增行:不重解析、不重渲染
|
|
1476
|
-
if (r.rev) revRef.current = String(r.rev)
|
|
1477
|
-
setEntries(Array.isArray(r.entries) ? r.entries : [])
|
|
1478
|
-
setErr('')
|
|
1479
|
-
} catch (e) { if (!dead) setErr(String(e?.message ?? e)) }
|
|
1480
|
-
}
|
|
1481
|
-
pull()
|
|
1482
|
-
const t = setInterval(pull, 3000)
|
|
1483
|
-
return () => { dead = true; clearInterval(t) }
|
|
1484
|
-
}, [])
|
|
1485
|
-
React.useEffect(() => {
|
|
1486
|
-
const el = listRef.current
|
|
1487
|
-
if (el && stick.current) el.scrollTop = el.scrollHeight
|
|
1488
|
-
}, [entries])
|
|
1489
|
-
const onScroll = () => {
|
|
1490
|
-
const el = listRef.current
|
|
1491
|
-
if (!el) return
|
|
1492
|
-
stick.current = el.scrollHeight - el.scrollTop - el.clientHeight < 28
|
|
1493
|
-
}
|
|
1494
|
-
const onStop = async () => {
|
|
1495
|
-
if (lock.current) return
|
|
1496
|
-
lock.current = true
|
|
1497
|
-
setBusy(true)
|
|
1498
|
-
try {
|
|
1499
|
-
const r = await jpost('/feiyu', { action: 'stop' })
|
|
1500
|
-
if (r && r.ok === false) toast('没停成:' + (r.error || r.hint || '未知原因'), 'warn')
|
|
1501
|
-
else toast(r && r.rounds != null ? `上岸啦~这趟一共游了 ${r.rounds} 轮。` : '它本来就没在刷。', 'good')
|
|
1502
|
-
} catch (e) { toast('没停成:' + String(e?.message ?? e), 'warn') }
|
|
1503
|
-
finally { lock.current = false; setBusy(false) }
|
|
1504
|
-
}
|
|
1505
|
-
return h('div', { 'data-bd-pet-card': '', 'data-wide': '' },
|
|
1506
|
-
h('div', { 'data-bd-pet-cardhead': '' },
|
|
1507
|
-
h('b', {}, '运行日志'),
|
|
1508
|
-
h('span', {}, '每 3 秒自动刷新'),
|
|
1509
|
-
h('button', {
|
|
1510
|
-
'data-bd-btn': '', 'data-sm': '', style: { marginLeft: '8px' }, disabled: busy, onClick: onStop,
|
|
1511
|
-
}, busy ? '停…' : '停止鲸鱼摸鱼')),
|
|
1512
|
-
err ? h('div', { 'data-bd-pet-pad': '' }, h('div', { 'data-bd-note': '' }, '读不到日志:' + err)) : null,
|
|
1513
|
-
h('div', { 'data-bd-pet-log': '', ref: listRef, onScroll },
|
|
1514
|
-
!entries
|
|
1515
|
-
? h('div', { 'data-bd-pet-logline': '' }, h('span', {}, '正在读…'))
|
|
1516
|
-
: entries.length === 0
|
|
1517
|
-
? h('div', { 'data-bd-pet-logline': '' }, h('span', {}, '还没动静 —— 开一次会话或跑一轮后台,这儿就会一条条记下来。'))
|
|
1518
|
-
: entries.map((e, i) => h('div', { 'data-bd-pet-logline': '', 'data-t': petEntryTone(e), key: i },
|
|
1519
|
-
h('time', {}, petFmtTime(e && e.at)), h('span', {}, petEntrySummary(e))))))
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
1387
|
/* 🐟 状态卡:字段全部来自 /status(feiyu 激活/轮次/今日挑选/本轮 token、bgBusy、loggedIn) */
|
|
1523
1388
|
function PetStatusCard({ st, fails }) {
|
|
1524
1389
|
const fy = (st && st.feiyu) || {}
|
|
@@ -1582,6 +1447,7 @@ window.__ModuleLoader__.load({
|
|
|
1582
1447
|
gear: ['M9 3h6l1 3 3 1 2 5-2 5-3 1-1 3H9l-1-3-3-1-2-5 2-5 3-1z', 'M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0'],
|
|
1583
1448
|
pulse: ['M3 12h4l3-8 4 16 3-8h4'], hide: ['M3 3l18 18M10.6 10.6a2 2 0 0 0 2.8 2.8M9.4 5.3A10.2 10.2 0 0 1 12 5c6 0 10 7 10 7a17 17 0 0 1-3 3.7M6.2 6.2A22 22 0 0 0 2 12s4 7 10 7a10.3 10.3 0 0 0 5-1.3'],
|
|
1584
1449
|
sliders: ['M4 7h16M4 17h16M8 4v6M16 14v6'], spark: ['M12 3l2.4 6.6L21 12l-6.6 2.4L12 21l-2.4-6.6L3 12l6.6-2.4z'],
|
|
1450
|
+
compass: ['M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0', 'M16 8l-3 5-5 3 3-5z'],
|
|
1585
1451
|
film: ['M4 4h16v16H4zM8 4v16M16 4v16M4 8h4M4 16h4M16 8h4M16 16h4'],
|
|
1586
1452
|
chip: ['M7 7h10v10H7zM9 3v4M15 3v4M9 17v4M15 17v4M3 9h4M3 15h4M17 9h4M17 15h4'],
|
|
1587
1453
|
dots: ['M5 12h.01M12 12h.01M19 12h.01'], key: ['M14 7a5 5 0 1 1-10 0 5 5 0 0 1 10 0M13 10l8 8M17 14l-2 2M20 17l-2 2'],
|
|
@@ -1620,7 +1486,7 @@ window.__ModuleLoader__.load({
|
|
|
1620
1486
|
/* 📌 P3-39:挂载时的初始快照。保存只提交与快照**不同**的键 —— 没动过的键不再
|
|
1621
1487
|
* 整包重发(以前每次都把七个键原样打回 /config,纯噪音写入);全没动就提示
|
|
1622
1488
|
* 「没有改动」。 */
|
|
1623
|
-
const [baseline] = React.useState(initial)
|
|
1489
|
+
const [baseline, setBaseline] = React.useState(initial)
|
|
1624
1490
|
const tell = (text, tone) => { if (typeof say === 'function') say(String(text || ''), tone) }
|
|
1625
1491
|
const update = (key, value) => setDraft(old => ({ ...old, [key]: value }))
|
|
1626
1492
|
const toggle = (key, title, note) => ToggleRow({ label: title, note, on: draft[key], disabled: Boolean(busy), onChange: value => update(key, value) })
|
|
@@ -1640,14 +1506,206 @@ window.__ModuleLoader__.load({
|
|
|
1640
1506
|
h('span', { 'data-bd-note': '' }, draft.quietHoursStart === draft.quietHoursEnd ? '起止时间相同,安静时段不生效。' : '按本地时间执行,支持跨午夜。')) : null,
|
|
1641
1507
|
toggle('petReducedMotion', '减少桌宠动效', '停用浮动、泡泡和事件动画,并遵循系统的减少动态效果设置。'),
|
|
1642
1508
|
h('div', { 'data-bd-row': '' }, h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, '当前:' + (st?.companion?.label || '自然陪伴')),
|
|
1643
|
-
h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy), onClick: () => {
|
|
1509
|
+
h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy), onClick: async () => {
|
|
1644
1510
|
/* P3-39:只提交与初始快照不同的键,没有改动就不发请求 */
|
|
1645
1511
|
const changed = Object.fromEntries(Object.entries(draft).filter(([key, value]) => value !== baseline[key]))
|
|
1646
1512
|
if (!Object.keys(changed).length) { tell('陪伴节奏:没有改动'); return }
|
|
1647
|
-
patch(changed, '陪伴节奏')
|
|
1513
|
+
if (await patch(changed, '陪伴节奏')) setBaseline({ ...draft })
|
|
1648
1514
|
} }, '保存陪伴节奏')))
|
|
1649
1515
|
}
|
|
1650
1516
|
|
|
1517
|
+
function PetBrowseSettings({ st, busy, patch, say }) {
|
|
1518
|
+
const cfg = st?.config || {}
|
|
1519
|
+
const initial = () => ({ browseTopics: Array.isArray(cfg.browseTopics) ? [...cfg.browseTopics] : [],
|
|
1520
|
+
keywords: (Array.isArray(cfg.keywords) ? cfg.keywords : []).join('\n'),
|
|
1521
|
+
browseAvoidKeywords: (Array.isArray(cfg.browseAvoidKeywords) ? cfg.browseAvoidKeywords : []).join('\n'),
|
|
1522
|
+
browseExplorePercent: String(cfg.browseExplorePercent ?? 25), browseMinMinutes: String(cfg.browseMinMinutes ?? 0),
|
|
1523
|
+
browseMaxMinutes: String(cfg.browseMaxMinutes ?? 0), browseRecentDays: String(cfg.browseRecentDays ?? 7), browseSort: cfg.browseSort || 'click',
|
|
1524
|
+
browseMinViews: String(cfg.browseMinViews ?? 50000) })
|
|
1525
|
+
const [draft, setDraft] = React.useState(initial)
|
|
1526
|
+
const [baseline, setBaseline] = React.useState(initial)
|
|
1527
|
+
const update = (key, value) => setDraft(old => ({ ...old, [key]: value }))
|
|
1528
|
+
const topics = ['科技', '知识', '生活', '美食', '游戏', '动画', '音乐', '影视', '动物', '运动', '旅行']
|
|
1529
|
+
const field = (label, control) => h('label', { 'data-bd-field': '', 'data-bd-grow': '' }, h('span', { 'data-bd-label': '' }, label), control)
|
|
1530
|
+
const select = (key, label, options) => field(label, h('select', { 'data-bd-input': '', 'aria-label': label, value: draft[key], disabled: Boolean(busy), onChange: e => update(key, e.target.value) },
|
|
1531
|
+
...options.map(([value, text]) => h('option', { key: value, value }, text))))
|
|
1532
|
+
const minutes = (key, label) => field(label, h('input', { 'data-bd-input': '', 'aria-label': label, type: 'number', min: 0, max: 1440, step: 1,
|
|
1533
|
+
value: draft[key], disabled: Boolean(busy), onChange: e => update(key, e.target.value), style: { width: '100%', boxSizing: 'border-box' } }))
|
|
1534
|
+
const save = async () => {
|
|
1535
|
+
const integer = (key, max, label) => {
|
|
1536
|
+
const value = Number(draft[key])
|
|
1537
|
+
if (String(draft[key]).trim() === '' || !Number.isSafeInteger(value) || value < 0 || value > max) throw new Error(label + '需要填写 0 至 ' + max + ' 之间的整数')
|
|
1538
|
+
return value
|
|
1539
|
+
}
|
|
1540
|
+
try {
|
|
1541
|
+
const value = { ...draft, keywords: [...new Set(draft.keywords.split('\n').map(word => word.trim()).filter(Boolean))],
|
|
1542
|
+
browseAvoidKeywords: [...new Set(draft.browseAvoidKeywords.split('\n').map(word => word.trim()).filter(Boolean))],
|
|
1543
|
+
browseExplorePercent: integer('browseExplorePercent', 100, '探索比例'), browseRecentDays: integer('browseRecentDays', 365, '发布时间'),
|
|
1544
|
+
browseMinMinutes: integer('browseMinMinutes', 1440, '最短时长'), browseMaxMinutes: integer('browseMaxMinutes', 1440, '最长时长'),
|
|
1545
|
+
browseMinViews: integer('browseMinViews', 2_000_000_000, '最低播放量') }
|
|
1546
|
+
if (value.browseMaxMinutes > 0 && value.browseMinMinutes > value.browseMaxMinutes) throw new Error('最短时长不能大于最长时长;最长填 0 表示不限')
|
|
1547
|
+
if (value.browseAvoidKeywords.length > 30 || value.browseAvoidKeywords.some(word => word.length > 80)) throw new Error('避开关键词最多 30 个,每个不超过 80 字')
|
|
1548
|
+
const comparable = { ...draft, keywords: value.keywords.join('\n'), browseAvoidKeywords: value.browseAvoidKeywords.join('\n') }
|
|
1549
|
+
const changed = Object.fromEntries(Object.entries(value).filter(([key]) => JSON.stringify(comparable[key]) !== JSON.stringify(baseline[key])))
|
|
1550
|
+
if (!Object.keys(changed).length) { say('刷视频偏好:没有改动'); return }
|
|
1551
|
+
if (await patch(changed, '刷视频偏好')) setBaseline(comparable)
|
|
1552
|
+
} catch (error) { say(String(error?.message || error), 'warn') }
|
|
1553
|
+
}
|
|
1554
|
+
return h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-browse-settings': '' },
|
|
1555
|
+
h('div', { 'data-bd-h': '' }, h(PetIcon, { name: 'compass' }), h('b', {}, '刷视频偏好')),
|
|
1556
|
+
h('div', { 'data-bd-note': '' }, '默认优先热门,至少 5 万播放;也可自定热度门槛。类别、关键词、日期和时长继续生效。保存后用于下一次启动。'),
|
|
1557
|
+
h('div', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '感兴趣的类别'),
|
|
1558
|
+
h('div', { 'data-bd-segments': '', role: 'group', 'aria-label': '感兴趣的类别', style: { flexWrap: 'wrap' } },
|
|
1559
|
+
...topics.map(topic => h('button', { key: topic, 'data-bd-btn': '', 'data-bd-browse-topic': '', 'data-sm': '', type: 'button', disabled: Boolean(busy), 'aria-pressed': draft.browseTopics.includes(topic),
|
|
1560
|
+
onClick: () => update('browseTopics', draft.browseTopics.includes(topic) ? draft.browseTopics.filter(value => value !== topic) : [...draft.browseTopics, topic]) }, topic))),
|
|
1561
|
+
h('div', { 'data-bd-note': '' }, '可多选,留空随兴趣。')),
|
|
1562
|
+
h('div', { 'data-bd-browse-common': '' },
|
|
1563
|
+
field('喜欢的关键词', h('textarea', { 'data-bd-input': '', 'aria-label': '长期偏好关键词', rows: 2, placeholder: '一行一个,例如 DeepSeek、手工、做饭',
|
|
1564
|
+
value: draft.keywords, disabled: Boolean(busy), onChange: e => update('keywords', e.target.value) })),
|
|
1565
|
+
select('browseRecentDays', '视频发布时间', [['0', '不限,旧片也看'], ['1', '最近 1 天'], ['7', '最近 7 天'], ['30', '最近 30 天'], ['90', '最近 90 天'], ['365', '最近一年'],
|
|
1566
|
+
...(!['0', '1', '7', '30', '90', '365'].includes(draft.browseRecentDays) ? [[draft.browseRecentDays, '最近 ' + draft.browseRecentDays + ' 天']] : [])])),
|
|
1567
|
+
field('最低播放量', h('input', { 'data-bd-input': '', 'aria-label': '最低播放量', type: 'number', min: 0, max: 2_000_000_000, step: 1000,
|
|
1568
|
+
value: draft.browseMinViews, disabled: Boolean(busy), onChange: e => update('browseMinViews', e.target.value) })),
|
|
1569
|
+
h('div', { 'data-bd-row': '', 'aria-label': '播放量快捷选择' }, ...[['0', '不限'], ['10000', '1 万'], ['50000', '5 万'], ['100000', '10 万']].map(([value, text]) => h('button', {
|
|
1570
|
+
key: value, type: 'button', 'data-bd-btn': '', 'data-sm': '', 'aria-pressed': draft.browseMinViews === value, disabled: Boolean(busy), onClick: () => update('browseMinViews', value) }, text))),
|
|
1571
|
+
h('div', { 'data-bd-note': '', 'data-bd-view-threshold': '' }, Number(draft.browseMinViews) > 0 ? '播放门槛草稿:至少 ' + Number(draft.browseMinViews).toLocaleString() + ' 次;保存后用于过滤候选视频。' : '播放门槛草稿:不限播放量。'),
|
|
1572
|
+
h('details', { 'data-bd-advanced': '', 'data-bd-browse-refine': '' },
|
|
1573
|
+
h('summary', {}, '精细选片', h('span', { 'data-bd-note': '' }, ' · '
|
|
1574
|
+
+ (Number(draft.browseMinMinutes) || Number(draft.browseMaxMinutes) ? (Number(draft.browseMinMinutes) || 0) + '–' + (Number(draft.browseMaxMinutes) || '不限') + ' 分钟' : '时长不限')
|
|
1575
|
+
+ ' · 避开 ' + draft.browseAvoidKeywords.split('\n').filter(word => word.trim()).length + ' 个词'
|
|
1576
|
+
+ ' · ' + ({ mixed: '探索 ' + draft.browseExplorePercent + '%', pubdate: '最新优先', click: '播放量优先', totalrank: '平台综合' }[draft.browseSort])
|
|
1577
|
+
+ ' · ' + (Number(draft.browseMinViews) > 0 ? '至少 ' + Number(draft.browseMinViews).toLocaleString() + ' 播放' : '播放不限'))),
|
|
1578
|
+
h('div', {},
|
|
1579
|
+
select('browseSort', '选片顺序', [['mixed', '兴趣与探索兼顾'], ['pubdate', '最新优先'], ['click', '播放量优先'], ['totalrank', '平台综合顺序']]),
|
|
1580
|
+
h('div', { 'data-bd-row': '', style: { alignItems: 'stretch' } }, minutes('browseMinMinutes', '最短时长(分钟)'), minutes('browseMaxMinutes', '最长时长(分钟)')),
|
|
1581
|
+
h('div', { 'data-bd-note': '' }, '0 表示不限。日期、时长缺失时先读取详情核对,避免误跳过。'),
|
|
1582
|
+
field('探索陌生内容的比例', h('div', { 'data-bd-row': '' },
|
|
1583
|
+
h('input', { 'data-bd-input': '', 'aria-label': '探索陌生内容的比例', type: 'range', min: 0, max: 100, step: 1, value: draft.browseExplorePercent,
|
|
1584
|
+
disabled: Boolean(busy), onChange: e => update('browseExplorePercent', e.target.value), style: { flex: '1 1 180px', minWidth: 0 } }),
|
|
1585
|
+
h('output', { 'data-bd-badge': '' }, draft.browseExplorePercent + '%'))),
|
|
1586
|
+
h('div', { 'data-bd-note': '' }, '在「兴趣与探索兼顾」时生效。低一些更贴口味,高一些更容易发现新东西;不会要求凑满数量。'),
|
|
1587
|
+
field('避开关键词', h('textarea', { 'data-bd-input': '', 'aria-label': '避开关键词', rows: 2, maxLength: 2400, placeholder: '一行一个,例如剧透、营销号或 UP 名称',
|
|
1588
|
+
value: draft.browseAvoidKeywords, disabled: Boolean(busy), onChange: e => update('browseAvoidKeywords', e.target.value) })),
|
|
1589
|
+
h('div', { 'data-bd-note': '' }, '最多 30 个避开关键词。类别和喜欢词为软偏好,日期、时长与避开词用于筛选。'))),
|
|
1590
|
+
h('div', { 'data-bd-row': '' }, h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, '运行时间在「运行方式」中安排。'),
|
|
1591
|
+
h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy), onClick: save }, '保存刷视频偏好')))
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function PetCuriosityJournal({ st, busy, onExplore }) {
|
|
1595
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
1596
|
+
const [pane, setPane] = React.useState('explore')
|
|
1597
|
+
const [snapshot, setSnapshot] = React.useState(null)
|
|
1598
|
+
const [question, setQuestion] = React.useState('')
|
|
1599
|
+
const [error, setError] = React.useState('')
|
|
1600
|
+
const [working, setWorking] = React.useState('')
|
|
1601
|
+
const [refresh, setRefresh] = React.useState(0)
|
|
1602
|
+
const [findIndex, setFindIndex] = React.useState(0)
|
|
1603
|
+
const alive = React.useRef(true), visible = React.useRef(false), epoch = React.useRef(0), lock = React.useRef(false)
|
|
1604
|
+
visible.current = expanded
|
|
1605
|
+
React.useEffect(() => { alive.current = true; return () => { alive.current = false; epoch.current += 1 } }, [])
|
|
1606
|
+
const accept = result => {
|
|
1607
|
+
if (!result || result.ok === false || !Array.isArray(result.items) || !Array.isArray(result.finds)) throw new Error(result?.error || '手帐没有返回可读记录')
|
|
1608
|
+
return result
|
|
1609
|
+
}
|
|
1610
|
+
React.useEffect(() => {
|
|
1611
|
+
const generation = ++epoch.current
|
|
1612
|
+
if (!expanded || lock.current) return
|
|
1613
|
+
let dead = false
|
|
1614
|
+
lock.current = true; setWorking('read')
|
|
1615
|
+
;(async () => {
|
|
1616
|
+
try {
|
|
1617
|
+
const result = accept(await jget('/curiosity?state=all'))
|
|
1618
|
+
if (!dead && alive.current && visible.current && generation === epoch.current) { setSnapshot(result); setError('') }
|
|
1619
|
+
} catch (failure) {
|
|
1620
|
+
if (!dead && alive.current && visible.current && generation === epoch.current) setError(String(failure?.message || failure))
|
|
1621
|
+
} finally {
|
|
1622
|
+
lock.current = false
|
|
1623
|
+
if (alive.current) {
|
|
1624
|
+
setWorking('')
|
|
1625
|
+
if (visible.current && generation !== epoch.current) setRefresh(value => value + 1)
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
})()
|
|
1629
|
+
return () => { dead = true; epoch.current += 1 }
|
|
1630
|
+
}, [expanded, refresh])
|
|
1631
|
+
const mutate = async (action, item) => {
|
|
1632
|
+
if (lock.current || busy || !visible.current) return
|
|
1633
|
+
const text = question.trim()
|
|
1634
|
+
if (action === 'open' && !text) { setError('先写一个想弄清的问题。'); return }
|
|
1635
|
+
if (action === 'forget' && typeof window.confirm === 'function' && !window.confirm('从手帐中忘记这条线索及其拾光笔记?B站内容和浏览判重记录保持不变。\n\n' + item.question)) return
|
|
1636
|
+
lock.current = true; setWorking(action === 'open' ? 'open' : item.id)
|
|
1637
|
+
const generation = epoch.current
|
|
1638
|
+
try {
|
|
1639
|
+
const body = action === 'open' ? { action, question: text, query: text } : { action, id: item.id, version: item.version }
|
|
1640
|
+
const result = accept(await jpost('/curiosity', body))
|
|
1641
|
+
if (alive.current && visible.current && generation === epoch.current) {
|
|
1642
|
+
setSnapshot(result); setError(''); if (action === 'open') setQuestion(current => current.trim() === text ? '' : current)
|
|
1643
|
+
}
|
|
1644
|
+
} catch (failure) {
|
|
1645
|
+
if (alive.current && visible.current && generation === epoch.current) {
|
|
1646
|
+
const message = String(failure?.message || failure)
|
|
1647
|
+
setError(['CURIOSITY_STALE', 'CURIOSITY_CHANGED'].includes(failure?.code) || /CURIOSITY_(?:STALE|CHANGED)|记录已变化|版本/.test(message) ? '这条线索已有更新,请刷新后核对再操作。' : message)
|
|
1648
|
+
}
|
|
1649
|
+
} finally {
|
|
1650
|
+
lock.current = false
|
|
1651
|
+
if (alive.current) {
|
|
1652
|
+
setWorking('')
|
|
1653
|
+
if (visible.current && generation !== epoch.current) setRefresh(value => value + 1)
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
const link = value => {
|
|
1658
|
+
try { const url = new URL(String(value)); return url.protocol === 'https:' && ['www.bilibili.com', 'bilibili.com'].includes(url.hostname) && /^\/video\/BV[0-9A-Za-z]{10}/.test(url.pathname) ? url.href : '' } catch { return '' }
|
|
1659
|
+
}
|
|
1660
|
+
const items = Array.isArray(snapshot?.items) ? snapshot.items : [], finds = Array.isArray(snapshot?.finds) ? snapshot.finds : []
|
|
1661
|
+
const counts = snapshot?.counts || {}, active = Boolean(st.feiyu?.active || st.bgBusy)
|
|
1662
|
+
const disabled = Boolean(busy || working), selectedFind = finds.length ? finds[Math.min(findIndex, finds.length - 1)] : null
|
|
1663
|
+
const states = { active: '正在惦记', paused: '先放一放', resolved: '已收起' }
|
|
1664
|
+
return h('details', { 'data-bd-curiosity-journal': '', open: expanded, onToggle: event => setExpanded(event.currentTarget.open) },
|
|
1665
|
+
h('summary', {}, h('span', { 'data-bd-journal-title': '' }, '好奇心手帐'),
|
|
1666
|
+
h('span', { 'data-bd-note': '' }, snapshot ? ` · ${Number(counts.active) || 0} 条在意的线索 · ${finds.length} 段拾光` : '留一个问题,带着它去逛逛')),
|
|
1667
|
+
expanded ? h('div', { 'data-bd-journal-content': '' },
|
|
1668
|
+
h('div', { 'data-bd-row': '' },
|
|
1669
|
+
h('div', { 'data-bd-segments': '', role: 'group', 'aria-label': '手帐内容' },
|
|
1670
|
+
...[['explore', '一起探索'], ['finds', '拾光回看']].map(([key, label]) => h('button', { key, type: 'button', 'data-bd-journal-pane': key, 'aria-pressed': pane === key, onClick: () => setPane(key) }, label))),
|
|
1671
|
+
h('span', { 'data-bd-grow': '' }),
|
|
1672
|
+
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled, onClick: () => setRefresh(value => value + 1) }, '刷新手帐')),
|
|
1673
|
+
error ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, error) : null,
|
|
1674
|
+
working === 'read' && !snapshot ? h('div', { 'data-bd-note': '', role: 'status' }, '正在翻开手帐…') : null,
|
|
1675
|
+
pane === 'explore' ? h('div', { 'data-bd-journal-explore': '' },
|
|
1676
|
+
h('form', { 'data-bd-journal-form': '', onSubmit: event => { event.preventDefault(); mutate('open') } },
|
|
1677
|
+
h('label', { 'data-bd-field': '', 'data-bd-grow': '' }, h('span', { 'data-bd-label': '' }, '想和她一起弄清什么?'),
|
|
1678
|
+
h('input', { 'data-bd-input': '', 'aria-label': '留一个好奇的问题', maxLength: 80, value: question, placeholder: '例如:老电影里的雨声是怎么做出来的', disabled: Boolean(busy), onChange: event => setQuestion(event.target.value) })),
|
|
1679
|
+
h('button', { 'data-bd-btn': '', type: 'submit', disabled: disabled || !question.trim() }, working === 'open' ? '正在记下…' : '留个问题')),
|
|
1680
|
+
h('div', { 'data-bd-note': '' }, '问题先留在本地。点「带着它去逛逛」会新开会话,沿用运行参数和发送权限。管理手帐只影响之后选题;当前探索请在运行控制中暂停。'),
|
|
1681
|
+
active ? h('div', { 'data-bd-note': '' }, '当前任务运行中,结束或暂停后再开启新的探索。') : null,
|
|
1682
|
+
!items.length && snapshot ? h('div', { 'data-bd-journal-empty': '' }, '手帐还是空的。有想追问的事,先留下来。') : null,
|
|
1683
|
+
...items.map(item => h('article', { key: item.id, 'data-bd-curiosity-id': item.id },
|
|
1684
|
+
h('div', { 'data-bd-row': '' }, h('span', { 'data-bd-badge': '', 'data-live': item.status === 'active' ? '' : undefined }, states[item.status] || item.status),
|
|
1685
|
+
h('span', { 'data-bd-note': '' }, item.origin === 'fish' ? '她从看过的内容里想到的' : '你留下的问题'),
|
|
1686
|
+
h('span', { 'data-bd-grow': '' }),
|
|
1687
|
+
h('details', { 'data-bd-journal-manage': '' }, h('summary', {}, '管理线索'),
|
|
1688
|
+
h('div', { 'data-bd-row': '' }, ...[[item.status === 'active' ? 'pause' : 'resume', item.status === 'active' ? '先放一放' : '接着惦记'],
|
|
1689
|
+
...(item.status !== 'resolved' ? [['resolve', '这次先到这']] : []), ['forget', '忘记这条']].map(([action, label]) => h('button', { key: action, 'data-bd-btn': '', 'data-sm': '', disabled, onClick: () => mutate(action, item) }, label))))),
|
|
1690
|
+
h('h3', {}, String(item.question || '')),
|
|
1691
|
+
/^BV[0-9A-Za-z]{10}$/.test(String(item.bvid || '')) ? h('a', { href: 'https://www.bilibili.com/video/' + item.bvid, target: '_blank', rel: 'noopener noreferrer', 'data-bd-journal-source': '' }, '最初看到的那条视频') : null,
|
|
1692
|
+
item.notes?.length ? h('p', { 'data-bd-note': '' }, '最近拾光:' + String(item.notes.at(-1)?.text || '')) : null,
|
|
1693
|
+
h('div', { 'data-bd-row': '' },
|
|
1694
|
+
h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, `${Array.isArray(item.notes) ? item.notes.length : 0} 段有来源的笔记`),
|
|
1695
|
+
h('button', { 'data-bd-btn': '', 'data-primary': '', 'data-bd-curiosity-explore': item.id, disabled: disabled || active || item.status !== 'active' || (st.toolScope?.checked && !st.toolScope.supported),
|
|
1696
|
+
onClick: () => onExplore(item.id) }, '带着它去逛逛')))))
|
|
1697
|
+
: h('div', { 'data-bd-journal-finds': '' },
|
|
1698
|
+
h('div', { 'data-bd-note': '' }, '这些是她读后写下的理解,带着视频出处;时间点供回看,不代表逐句核验。'),
|
|
1699
|
+
selectedFind ? h('article', { 'data-bd-find-id': selectedFind.id },
|
|
1700
|
+
h('span', { 'data-bd-journal-eyebrow': '' }, selectedFind.question || '路上捡到的一点光'),
|
|
1701
|
+
h('blockquote', {}, String(selectedFind.text || '')),
|
|
1702
|
+
link(selectedFind.url) ? h('a', { href: link(selectedFind.url), target: '_blank', rel: 'noopener noreferrer' }, '回到视频' + (Number(selectedFind.seconds) > 0 ? ' · ' + Math.floor(selectedFind.seconds / 60) + ':' + String(Math.floor(selectedFind.seconds % 60)).padStart(2, '0') : '')) : h('span', { 'data-bd-note': '' }, '暂无可回看的视频链接'),
|
|
1703
|
+
h('div', { 'data-bd-row': '' }, h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, `${Math.min(findIndex, finds.length - 1) + 1} / ${finds.length} · 只保存在本地`),
|
|
1704
|
+
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: finds.length < 2, onClick: () => setFindIndex(value => (value + 1) % finds.length) }, '再翻一张')))
|
|
1705
|
+
: h('div', { 'data-bd-journal-empty': '' }, '还没有拾光。她读到真实内容、留下一点发现后,会把出处一起夹在这里。')))
|
|
1706
|
+
: null)
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1651
1709
|
/* ⭐ 设置大弹窗(2026-10-26 桌宠化改造):从桌宠 ⚙️ 进来的居中弹窗。
|
|
1652
1710
|
* 六组主导航与组内子页,运行方式与日志各有内部分段;图库、学习与视频按所在页读取;
|
|
1653
1711
|
* 端点与保存语义与旧设置页一致(/config、/feiyu、/pv、/login/*、/install*、/run)。
|
|
@@ -1723,6 +1781,9 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1723
1781
|
const [learnError, setLearnError] = React.useState('')
|
|
1724
1782
|
const [learnBusy, setLearnBusy] = React.useState({})
|
|
1725
1783
|
const [learnRefresh, setLearnRefresh] = React.useState(0)
|
|
1784
|
+
const [learningWorkspace, setLearningWorkspace] = React.useState('learned')
|
|
1785
|
+
const learningWorkspaceRef = React.useRef(learningWorkspace)
|
|
1786
|
+
learningWorkspaceRef.current = learningWorkspace
|
|
1726
1787
|
const [mirrorNote, setMirrorNote] = React.useState('')
|
|
1727
1788
|
const [soulBusy, setSoulBusy] = React.useState('')
|
|
1728
1789
|
const [memoryQuery, setMemoryQuery] = React.useState('')
|
|
@@ -1767,7 +1828,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1767
1828
|
}, [])
|
|
1768
1829
|
|
|
1769
1830
|
const learnView = {
|
|
1770
|
-
state: learnState,
|
|
1831
|
+
state: learningWorkspace === 'learned' ? 'approved' : learnState,
|
|
1771
1832
|
kind: learnKind,
|
|
1772
1833
|
filter: learnFilter,
|
|
1773
1834
|
limit: 20,
|
|
@@ -1776,12 +1837,13 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1776
1837
|
learnViewRef.current = learnView
|
|
1777
1838
|
|
|
1778
1839
|
React.useEffect(() => {
|
|
1779
|
-
if (view !== 'learning') return
|
|
1840
|
+
if (view !== 'learning' || learningWorkspace === 'memory') return
|
|
1780
1841
|
let dead = false
|
|
1842
|
+
let missing = false
|
|
1781
1843
|
let timer = null
|
|
1782
1844
|
const queryView = { ...learnView }
|
|
1783
1845
|
const request = async () => {
|
|
1784
|
-
if (dead || !alive.current || learnInFlight.current.size || learnPulling.current) return
|
|
1846
|
+
if (dead || missing || !alive.current || learnInFlight.current.size || learnPulling.current) return
|
|
1785
1847
|
const seq = ++learnRequest.current
|
|
1786
1848
|
learnPulling.current = true
|
|
1787
1849
|
const query = '?state=' + encodeURIComponent(queryView.state)
|
|
@@ -1790,7 +1852,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1790
1852
|
+ '&limit=' + queryView.limit + '&offset=' + queryView.offset
|
|
1791
1853
|
try {
|
|
1792
1854
|
const r = await jget('/learn' + query)
|
|
1793
|
-
if (!r || r.ok === false) throw new Error((r && r.error) || '
|
|
1855
|
+
if (!r || r.ok === false || !Array.isArray(r.items)) throw new Error((r && r.error) || '学习记录接口尚未加载,完整重启 DSH 后重试')
|
|
1794
1856
|
const current = learnViewRef.current || {}
|
|
1795
1857
|
const same = current.state === queryView.state && current.kind === queryView.kind
|
|
1796
1858
|
&& current.filter === queryView.filter && Number(current.offset) === Number(queryView.offset)
|
|
@@ -1799,9 +1861,14 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1799
1861
|
setLearnError('')
|
|
1800
1862
|
}
|
|
1801
1863
|
} catch (e) {
|
|
1802
|
-
if (!dead && alive.current && seq === learnRequest.current)
|
|
1864
|
+
if (!dead && alive.current && seq === learnRequest.current) {
|
|
1865
|
+
if (e?.status === 404) { missing = true; if (timer) clearInterval(timer) }
|
|
1866
|
+
setLearnError(e?.status === 404 ? '学习记录接口不存在,请完整重启 DSH 后手动重新读取。' : String(e && e.message || e))
|
|
1867
|
+
}
|
|
1803
1868
|
} finally {
|
|
1804
1869
|
learnPulling.current = false
|
|
1870
|
+
// A view switch can finish while its predecessor is still in flight. Resume the selected view promptly.
|
|
1871
|
+
if (alive.current && learningWorkspaceRef.current !== 'memory' && (dead || seq !== learnRequest.current)) setLearnRefresh(n => n + 1)
|
|
1805
1872
|
}
|
|
1806
1873
|
}
|
|
1807
1874
|
request()
|
|
@@ -1811,10 +1878,10 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1811
1878
|
learnRequest.current += 1
|
|
1812
1879
|
if (timer) clearInterval(timer)
|
|
1813
1880
|
}
|
|
1814
|
-
}, [view, learnState, learnKind, learnFilter, learnOffset, learnRefresh])
|
|
1881
|
+
}, [view, learningWorkspace, learnState, learnKind, learnFilter, learnOffset, learnRefresh])
|
|
1815
1882
|
|
|
1816
1883
|
React.useEffect(() => {
|
|
1817
|
-
if (view !== 'learning') return
|
|
1884
|
+
if (view !== 'learning' || learningWorkspace !== 'memory') return
|
|
1818
1885
|
let dead = false
|
|
1819
1886
|
const seq = ++memoryRequest.current
|
|
1820
1887
|
const timer = setTimeout(async () => {
|
|
@@ -1825,7 +1892,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1825
1892
|
} catch (e) { if (!dead && alive.current && seq === memoryRequest.current) setMemoryError(String(e?.message || e)) }
|
|
1826
1893
|
}, memoryQuery ? 220 : 0)
|
|
1827
1894
|
return () => { dead = true; clearTimeout(timer); memoryRequest.current += 1 }
|
|
1828
|
-
}, [view, memorySource, memoryQuery, memoryOffset, memoryRefresh, st?.rev])
|
|
1895
|
+
}, [view, learningWorkspace, memorySource, memoryQuery, memoryOffset, memoryRefresh, st?.rev])
|
|
1829
1896
|
|
|
1830
1897
|
const icon = (name) => h(PetIcon, { name })
|
|
1831
1898
|
const sectionHead = (_name, title, extra) => h('div', { 'data-bd-h': '' }, h('b', {}, title), extra || null)
|
|
@@ -1997,6 +2064,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1997
2064
|
const tasteTop = Array.isArray(tasteInfo.top) ? tasteInfo.top : []
|
|
1998
2065
|
const tasteLines = Array.isArray(tasteInfo.lines) ? tasteInfo.lines : []
|
|
1999
2066
|
const focusLearning = (state) => {
|
|
2067
|
+
setLearningWorkspace(state === 'approved' ? 'learned' : 'review')
|
|
2000
2068
|
setLearnState(state); setLearnFilter(''); setLearnKind(''); setLearnOffset(0); setLearnQ(null)
|
|
2001
2069
|
learnQueueRef.current?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' })
|
|
2002
2070
|
}
|
|
@@ -2064,6 +2132,16 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2064
2132
|
} }
|
|
2065
2133
|
finally { memoryActionLock.current = false; if (alive.current) setMemoryWorking('') }
|
|
2066
2134
|
}
|
|
2135
|
+
const learningUseNote = (row) => {
|
|
2136
|
+
const practice = row.practice && typeof row.practice === 'object' ? row.practice : {}
|
|
2137
|
+
const reported = Number.isSafeInteger(practice.reported) && practice.reported > 0 ? practice.reported : 0
|
|
2138
|
+
if (!row.when && !reported) return null
|
|
2139
|
+
return h('div', { 'data-bd-learning-application': '' },
|
|
2140
|
+
row.when ? h('div', { 'data-bd-note': '' }, '适用场景:' + String(row.when)) : null,
|
|
2141
|
+
reported ? h('div', { 'data-bd-note': '' }, '实践记录(角色自报):' + reported + ' 次 · '
|
|
2142
|
+
+ ({ tried: '已经尝试', helpful: '自评有帮助', unhelpful: '自评需调整' }[practice.lastOutcome] || '已记录'),
|
|
2143
|
+
practice.lastNote ? h('div', {}, String(practice.lastNote)) : null) : null)
|
|
2144
|
+
}
|
|
2067
2145
|
const memoryLabels = { all: '全部记忆', learning: '学习经验', soul: '心情记录', voice: '语气札记', taste: '口味样本', seen: '浏览历史', history: '版本与审计' }
|
|
2068
2146
|
const memoryItems = Array.isArray(memoryData?.items) ? memoryData.items : []
|
|
2069
2147
|
const memoryExplorerCard = h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-memory-explorer': '' },
|
|
@@ -2085,6 +2163,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2085
2163
|
row.label ? h('strong', { 'data-bd-label': '', 'data-bd-grow': '' }, row.label) : null,
|
|
2086
2164
|
h('time', { 'data-bd-note': '' }, formatTime(row.at) || '时间未记录')),
|
|
2087
2165
|
h('p', {}, row.text),
|
|
2166
|
+
learningUseNote(row),
|
|
2167
|
+
row.example ? h('blockquote', { 'data-bd-example': '' }, h('span', { 'data-bd-note': '' }, '参考例句(理解用法,不照搬)'), h('div', {}, String(row.example))) : null,
|
|
2088
2168
|
row.from ? h('div', { 'data-bd-note': '' }, '来源:' + row.from) : null,
|
|
2089
2169
|
row.why ? h('div', { 'data-bd-note': '' }, '决定理由:' + row.why) : null,
|
|
2090
2170
|
row.state ? h('span', { 'data-bd-note': '' }, '当前状态:' + ({ pending: '待审', approved: '已批准', rejected: '已拒绝' }[row.state] || row.state)) : null,
|
|
@@ -2093,9 +2173,9 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2093
2173
|
row.actions.includes('forget') ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(memoryWorking) || Boolean(busy), onClick: () => changeMemory(row, 'forget') }, memoryWorking === row.source + ':' + row.id ? '正在移除…' : '从当前记忆移除') : null)
|
|
2094
2174
|
: h('span', { 'data-bd-note': '' }, '只读历史记录')))),
|
|
2095
2175
|
h('div', { 'data-bd-row': '' },
|
|
2096
|
-
h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, memoryData ? `匹配 ${Number(memoryData.matched) || 0} 条 · 当前 ${memoryItems.length ? memoryOffset + 1 : 0}–${memoryOffset + memoryItems.length};审计范围为最近 100 条` : '按需读取,离页停止'),
|
|
2176
|
+
h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, memoryData ? `匹配 ${Number(memoryData.matched ?? memoryData.total) || 0} 条 · 当前 ${memoryItems.length ? memoryOffset + 1 : 0}–${memoryOffset + memoryItems.length};审计范围为最近 100 条` : '按需读取,离页停止'),
|
|
2097
2177
|
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: memoryOffset <= 0 || Boolean(memoryWorking), onClick: () => { setMemoryOffset(Math.max(0, memoryOffset - 20)); setMemoryData(null) } }, '前 20 条'),
|
|
2098
|
-
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: !memoryData?.more || Boolean(memoryWorking), onClick: () => { setMemoryOffset(memoryOffset + 20); setMemoryData(null) } }, '后 20 条'),
|
|
2178
|
+
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: !(memoryData?.hasMore ?? memoryData?.more) || Boolean(memoryWorking), onClick: () => { setMemoryOffset(Number.isSafeInteger(memoryData?.nextOffset) ? memoryData.nextOffset : memoryOffset + 20); setMemoryData(null) } }, '后 20 条'),
|
|
2099
2179
|
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(memoryWorking), onClick: () => setMemoryRefresh((n) => n + 1) }, '重新读取记忆')))
|
|
2100
2180
|
const q = learnQ && sameLearnView(learnView) ? learnQ : null
|
|
2101
2181
|
const items = q && Array.isArray(q.items) ? q.items : []
|
|
@@ -2122,10 +2202,11 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2122
2202
|
one.decidedAt ? h('span', { 'data-bd-note': '' }, '决定于 ' + formatTime(one.decidedAt)) : null,
|
|
2123
2203
|
learnBusy[one.id] ? h('span', { 'data-bd-note': '', role: 'status' }, '处理中…') : null),
|
|
2124
2204
|
h('div', { 'data-bd-label': '' }, String(one.text || '')),
|
|
2205
|
+
learningUseNote(one),
|
|
2125
2206
|
one.example ? h('blockquote', { 'data-bd-example': '' },
|
|
2126
|
-
h('span', { 'data-bd-note': '' }, '
|
|
2127
|
-
one.from ? h('div', { 'data-bd-note': '', 'data-bd-source': '' },
|
|
2128
|
-
'来源:' + String(one.from),
|
|
2207
|
+
h('span', { 'data-bd-note': '' }, '参考例句(理解用法,不照搬)'), h('div', {}, String(one.example))) : null,
|
|
2208
|
+
one.from || Array.isArray(one.sources) && one.sources.length ? h('div', { 'data-bd-note': '', 'data-bd-source': '' },
|
|
2209
|
+
'来源:' + String(one.from || one.sources.join(';')),
|
|
2129
2210
|
...links.map((bvid) => h('a', { key: bvid, href: 'https://www.bilibili.com/video/' + bvid, target: '_blank', rel: 'noopener noreferrer' }, '打开 ' + bvid))) : null,
|
|
2130
2211
|
one.why ? h('div', { 'data-bd-note': '' }, '决定理由:' + String(one.why)) : null,
|
|
2131
2212
|
h('div', { 'data-bd-note': '' }, `审计:记录于 ${formatTime(one.at) || '未知'} · 版本 ${Number(one.version) || 1}`),
|
|
@@ -2134,9 +2215,10 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2134
2215
|
canReject ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: pending, onClick: () => decideLearn(one.id, 'reject') }, icon('dots'), one.state === 'approved' ? '改为拒绝' : '拒绝') : null,
|
|
2135
2216
|
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: pending, onClick: () => decideLearn(one.id, 'forget') }, icon('gear'), '移除记录')))
|
|
2136
2217
|
}) : null
|
|
2137
|
-
const learnCard = h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-learning-queue': '', ref: learnQueueRef },
|
|
2138
|
-
h('div', { 'data-bd-row': '' }, sectionHead('list', '
|
|
2218
|
+
const learnCard = () => h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-learning-queue': '', 'data-bd-learned': learningWorkspace === 'learned' ? '' : undefined, ref: learnQueueRef },
|
|
2219
|
+
h('div', { 'data-bd-row': '' }, sectionHead('list', learningWorkspace === 'learned' ? '已学会 · 已批准经验' : '学习审核与历史'),
|
|
2139
2220
|
h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, `待审 ${Number(learnInfo.pending) || 0} · 已批准 ${Number(learnInfo.approved) || 0} · 已拒绝 ${Number(learnInfo.rejected) || 0}`)),
|
|
2221
|
+
learningWorkspace === 'review' ? reviewCard : null,
|
|
2140
2222
|
/* 判重账本概览(2026-10-27 从旧设置页搬进来):「刷过不再刷、回过不再回」记了多少得有地方看 */
|
|
2141
2223
|
(() => {
|
|
2142
2224
|
const sn = st && st.seen
|
|
@@ -2145,32 +2227,35 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2145
2227
|
`判重账本:已浏览 ${Number(sn.videos) || 0} 个视频(今日新增 ${Number(sn.videosToday) || 0} 个)· `
|
|
2146
2228
|
+ `已回复 ${Number(sn.replied) || 0} 条评论`)
|
|
2147
2229
|
})(),
|
|
2148
|
-
h('div', { 'data-bd-note': '' }, '
|
|
2230
|
+
h('div', { 'data-bd-note': '' }, '已批准经验按当前情境选入后续提示词。适用情境和实践记录帮助她下次用上;实践为角色自报,不代表效果已核实。移除当前记录后仍保留审计。'),
|
|
2149
2231
|
h('div', { 'data-bd-row': '' },
|
|
2150
|
-
...
|
|
2232
|
+
...(learningWorkspace === 'review' ? [['pending', '待审'], ['rejected', '已拒绝'], ['all', '全部']] : []).map(([value, label]) => h('button', {
|
|
2151
2233
|
key: value, 'data-bd-btn': '', 'data-sm': '', 'data-primary': learnState === value ? '' : undefined,
|
|
2152
2234
|
onClick: () => { setLearnState(value); setLearnOffset(0); setLearnQ(null) },
|
|
2153
2235
|
}, label)),
|
|
2154
2236
|
h('select', { 'data-bd-input': '', 'aria-label': '学习记录类别', style: { width: '130px', flex: '0 1 130px' }, value: learnKind, onChange: (e) => { setLearnKind(e.target.value); setLearnOffset(0); setLearnQ(null) } },
|
|
2155
2237
|
h('option', { value: '' }, '全部类别'), ...kinds.map((kind) => h('option', { key: kind, value: kind }, labels[kind] || kind)))),
|
|
2156
2238
|
h('input', {
|
|
2157
|
-
'data-bd-input': '', value: learnFilter, maxLength: 160, placeholder: '
|
|
2239
|
+
'data-bd-input': '', value: learnFilter, maxLength: 160, placeholder: '按正文、适用场景、例句、来源或决定理由筛选',
|
|
2158
2240
|
'aria-label': '筛选学习记录', onChange: (e) => { setLearnFilter(e.target.value); setLearnOffset(0); setLearnQ(null) },
|
|
2159
2241
|
}),
|
|
2160
|
-
learnError ? h('div', { 'data-bd-msg': '', 'data-warn': '' }, '学习记录读取失败:' + learnError) : null,
|
|
2161
|
-
!q ? h('div', { 'data-bd-list-empty': '', role: 'status' }, '正在读取学习记录…')
|
|
2162
|
-
: !items.length ? h('div', { 'data-bd-list-empty': '' }, learnFilter ? '
|
|
2242
|
+
learnError ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, '学习记录读取失败:' + learnError) : null,
|
|
2243
|
+
!q ? h('div', { 'data-bd-list-empty': '', role: 'status' }, learnError ? '读取失败,已保存的经验未被删除。请重新读取。' : '正在读取学习记录…')
|
|
2244
|
+
: !items.length ? h('div', { 'data-bd-list-empty': '' }, learnFilter ? '无匹配的学习记录,可清空关键词或更换类别。'
|
|
2245
|
+
: learningWorkspace === 'learned' ? '暂无已批准经验。待审内容通过审核后会出现在这里。' : '该状态暂无学习记录;已批准的内容可在“已学会”查看。')
|
|
2163
2246
|
: h('div', { 'data-bd-learn-list': '', tabIndex: 0, 'aria-label': '学习记录列表' }, ...learnRows),
|
|
2164
|
-
|
|
2165
|
-
h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, `显示 ${items.length ? learnOffset + 1 : 0}–${learnOffset + items.length} / ${q.matched
|
|
2247
|
+
h('div', { 'data-bd-row' : '' },
|
|
2248
|
+
h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, q ? `显示 ${items.length ? learnOffset + 1 : 0}–${learnOffset + items.length} / ${q.matched ?? q.total ?? items.length} 条` : '按需读取,切到记忆检索后停止刷新'),
|
|
2166
2249
|
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: learnOffset <= 0 || Boolean(busy), onClick: () => { setLearnOffset(Math.max(0, learnOffset - 20)); setLearnQ(null) } }, '上一页'),
|
|
2167
|
-
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: !q
|
|
2250
|
+
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: !(q?.hasMore ?? q?.more) || Boolean(busy), onClick: () => { setLearnOffset(Number.isSafeInteger(q?.nextOffset) ? q.nextOffset : learnOffset + 20); setLearnQ(null) } }, '下一页'),
|
|
2251
|
+
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(busy), onClick: () => setLearnRefresh(n => n + 1) }, '重新读取学习'))
|
|
2168
2252
|
)
|
|
2169
2253
|
|
|
2170
2254
|
const reviewStatus = (st && st.learnReview) || {}
|
|
2255
|
+
const hasReviewBatch = Boolean(reviewStatus.id || reviewStatus.batchKey || reviewStatus.lastStartedAt || reviewStatus.lastFinishedAt
|
|
2256
|
+
|| reviewStatus.running || reviewStatus.complete || reviewStatus.lastError || Number(reviewStatus.count) > 0 || Number(reviewStatus.approved) > 0 || Number(reviewStatus.rejected) > 0)
|
|
2171
2257
|
const reviewModels = Array.isArray(st && st.models) ? st.models : []
|
|
2172
|
-
const reviewCard = h('
|
|
2173
|
-
sectionHead('pulse', '后台自动审核'),
|
|
2258
|
+
const reviewCard = h('div', { 'data-bd-review-controls': '' },
|
|
2174
2259
|
h('div', { 'data-bd-row': '', 'data-bd-toggle': '' },
|
|
2175
2260
|
h('div', { 'data-bd-grow': '' },
|
|
2176
2261
|
h('div', { 'data-bd-label': '' }, '自动审核待审内容'),
|
|
@@ -2209,16 +2294,15 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2209
2294
|
try { const r = check(await jpost('/learn/review', { action: 'stop' }), '停止模型审核失败'); if (r.learnReview) setSt((old) => ({ ...old, learnReview: r.learnReview })); safeSay('模型审核已请求停止。', 'good') }
|
|
2210
2295
|
catch (e) { safeSay(String(e && e.message || e), 'warn') } finally { setBusy('') }
|
|
2211
2296
|
} }, '停止审核') : null),
|
|
2212
|
-
h('div', { 'data-bd-row': '' },
|
|
2297
|
+
hasReviewBatch ? h('div', { 'data-bd-row': '', 'data-bd-review-receipt': '' },
|
|
2213
2298
|
h('span', { 'data-bd-badge': '', 'data-live': reviewStatus.running ? '' : undefined }, reviewStatus.running ? '审核中' : reviewStatus.complete ? '本批完成' : reviewStatus.lastError ? '本批未完成' : '未运行'),
|
|
2214
|
-
h('span', { 'data-bd-badge': '' }, '全部待审 ' + Number(reviewStatus.pending ?? learnInfo.pending ?? 0)),
|
|
2215
2299
|
h('span', { 'data-bd-badge': '' }, '本批批准 ' + Number(reviewStatus.approved || 0)),
|
|
2216
2300
|
h('span', { 'data-bd-badge': '' }, '本批拒绝 ' + Number(reviewStatus.rejected || 0)),
|
|
2217
|
-
reviewStatus.remaining != null ? h('span', { 'data-bd-badge': '' }, '本批剩余 ' + Number(reviewStatus.remaining)) : null),
|
|
2218
|
-
reviewStatus.model ? h('div', { 'data-bd-note': '' }, (reviewStatus.running ? '正在使用:' : '最近使用:') + reviewStatus.model) : null,
|
|
2301
|
+
reviewStatus.remaining != null ? h('span', { 'data-bd-badge': '' }, '本批剩余 ' + Number(reviewStatus.remaining)) : null) : null,
|
|
2302
|
+
hasReviewBatch && reviewStatus.model ? h('div', { 'data-bd-note': '' }, (reviewStatus.running ? '正在使用:' : '最近使用:') + reviewStatus.model) : null,
|
|
2219
2303
|
reviewStatus.lastError ? h('div', { 'data-bd-msg': '', 'data-warn': '' }, '审核提示:' + reviewStatus.lastError) : null,
|
|
2220
2304
|
!reviewStatus.running && Number(reviewStatus.nextRetryAt) > 0 ? h('div', { 'data-bd-note': '' }, '自动重试最早:' + formatTime(reviewStatus.nextRetryAt) + (reviewStatus.enabled ? '' : '(自动审核未开启)')) : null,
|
|
2221
|
-
h('div', { 'data-bd-note': '' }, reviewStatus.lastFinishedAt ? '上次结束:' + formatTime(reviewStatus.lastFinishedAt) : '
|
|
2305
|
+
h('div', { 'data-bd-note': '' }, reviewStatus.lastFinishedAt ? '上次结束:' + formatTime(reviewStatus.lastFinishedAt) : '后台自动审核:开启后处理待审队列,也可手动批量审核。'))
|
|
2222
2306
|
|
|
2223
2307
|
/* 🔄 更新通道(2026-10-30 使用者「加一个更新通道,方便及时更新」)
|
|
2224
2308
|
* 只在这里查/升级;普通鱼工具拿不到升级能力(换版本要重启宿主,不能让会话模型自己决定)。
|
|
@@ -2355,8 +2439,12 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2355
2439
|
return h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': view === 'learning' ? 'learning' : 'other' },
|
|
2356
2440
|
h('div', { 'data-bd-pghead': '' }, h('div', {}, h('b', {}, view === 'learning' ? '学习与记忆' : '账号与环境'),
|
|
2357
2441
|
h('p', {}, view === 'learning' ? '管理学习审核、角色记忆、口味统计和历史记录。' : '管理账号登录、运行依赖和本地数据。'))),
|
|
2442
|
+
view === 'learning' ? h('div', { 'data-bd-segments': '', role: 'group', 'aria-label': '学习工作区' },
|
|
2443
|
+
...[['learned', '已学会'], ['review', '待审学习'], ['memory', '记忆检索']].map(([key, label]) => h('button', { key, type: 'button', 'data-bd-learning-workspace': key,
|
|
2444
|
+
'aria-pressed': learningWorkspace === key, onClick: () => { setLearningWorkspace(key); setLearnQ(null); setLearnOffset(0); setLearnError('') } }, label,
|
|
2445
|
+
key !== 'memory' ? h('span', { 'data-bd-note': '' }, ' · ' + Number(learnInfo[key === 'learned' ? 'approved' : 'pending'] || 0)) : null))) : null,
|
|
2358
2446
|
h('div', { 'data-bd-pet-grid': '', 'data-bd-other': '' },
|
|
2359
|
-
...(view === 'learning' ? [memoryOverviewCard, memoryExplorerCard,
|
|
2447
|
+
...(view === 'learning' ? learningWorkspace !== 'memory' ? [learnCard()] : [memoryOverviewCard, memoryExplorerCard, soulCard] : [accountCard, updateCard, asrCard, mirrorCard, groundCard])))
|
|
2360
2448
|
}
|
|
2361
2449
|
|
|
2362
2450
|
|
|
@@ -2675,14 +2763,14 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2675
2763
|
h('button', { 'data-bd-btn': '', 'data-primary': '', type: 'submit', disabled }, working === 'collect' ? '正在收集…' : '加入表情库'))))
|
|
2676
2764
|
}
|
|
2677
2765
|
|
|
2678
|
-
function PetSettingsModal({ onClose, toast, ctx, status, initialTab = 'run' }) {
|
|
2766
|
+
function PetSettingsModal({ onClose, toast, ctx, status, initialTab = 'run', initialRunMode = 'session' }) {
|
|
2679
2767
|
const [tab, setTab] = React.useState(initialTab)
|
|
2680
2768
|
const activeGroup = ({ stickers: 'learning', ledger: 'logs', review: 'logs', other: 'basic' })[tab] || tab
|
|
2681
2769
|
React.useEffect(() => {
|
|
2682
2770
|
const body = modalRef.current?.querySelector('[data-bd-pet-body]')
|
|
2683
2771
|
if (body) body.scrollTop = 0
|
|
2684
2772
|
}, [tab])
|
|
2685
|
-
const [runMode, setRunMode] = React.useState('session')
|
|
2773
|
+
const [runMode, setRunMode] = React.useState(initialRunMode === 'background' ? 'background' : 'session')
|
|
2686
2774
|
const [logMode, setLogMode] = React.useState('events')
|
|
2687
2775
|
const [runDrafts, setRunDrafts] = React.useState({})
|
|
2688
2776
|
const runLock = React.useRef(false)
|
|
@@ -2696,7 +2784,6 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2696
2784
|
const [msg, setMsg] = React.useState('')
|
|
2697
2785
|
const [msgTone, setMsgTone] = React.useState('')
|
|
2698
2786
|
/* 输入一律绑本地草稿:5 秒轮询会把正在打的字冲掉 */
|
|
2699
|
-
const [kws, setKws] = React.useState(null)
|
|
2700
2787
|
const [wsDir, setWsDir] = React.useState(null) // 跟 uiWorkspace 服务的名字区分开
|
|
2701
2788
|
const [rl, setRl] = React.useState(null)
|
|
2702
2789
|
const [perDay, setPerDay] = React.useState(null)
|
|
@@ -2891,7 +2978,6 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2891
2978
|
}
|
|
2892
2979
|
React.useEffect(() => {
|
|
2893
2980
|
if (!cfg) return
|
|
2894
|
-
if (kws === null) setKws((cfg.keywords || []).join('\n'))
|
|
2895
2981
|
if (wsDir === null) setWsDir(cfg.workspace || '')
|
|
2896
2982
|
if (rl === null) setRl(cfg.reportLevel || 'full')
|
|
2897
2983
|
if (perDay === null) setPerDay(String(cfg.perDay ?? 5))
|
|
@@ -2920,6 +3006,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2920
3006
|
setBusy(JSON.stringify(p))
|
|
2921
3007
|
try {
|
|
2922
3008
|
const r = await jpost('/config', p)
|
|
3009
|
+
if (!r || r.ok === false) throw new Error(r?.error || '设置保存失败')
|
|
3010
|
+
if ('persona' in p && typeof r.config?.persona !== 'string') throw new Error('未收到人格保存回执,请重新读取设置后重试。')
|
|
2923
3011
|
if (r?.config) setSt((old) => ({ ...old, config: r.config }))
|
|
2924
3012
|
const clamped = Array.isArray(r?.clamped) ? r.clamped : []
|
|
2925
3013
|
const clampTxt = clamped.map((c) => `${c.key} 想要 ${c.want},最多只能 ${c.got}`).join(';')
|
|
@@ -3387,26 +3475,26 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3387
3475
|
}
|
|
3388
3476
|
} catch (e) { say(String(e?.message ?? e), 'warn') }
|
|
3389
3477
|
}
|
|
3390
|
-
const startRun = async (mode) => {
|
|
3391
|
-
if (runLock.current) return
|
|
3392
|
-
if (mode === 'session' && st.toolScope?.checked && !st.toolScope.supported) { say('当前 DSH 不支持会话工具隔离,无法启动会话运行。', 'warn'); return }
|
|
3478
|
+
const startRun = async (mode, { curiosityId = '' } = {}) => {
|
|
3479
|
+
if (runLock.current) return false
|
|
3480
|
+
if (mode === 'session' && st.toolScope?.checked && !st.toolScope.supported) { say('当前 DSH 不支持会话工具隔离,无法启动会话运行。', 'warn'); return false }
|
|
3393
3481
|
let options
|
|
3394
|
-
try { options = readRun(mode) } catch (e) { say(String(e?.message ?? e), 'warn'); return }
|
|
3482
|
+
try { options = readRun(mode) } catch (e) { say(String(e?.message ?? e), 'warn'); return false }
|
|
3395
3483
|
runLock.current = true
|
|
3396
3484
|
setBusy('start-' + mode)
|
|
3397
3485
|
say('')
|
|
3486
|
+
let sessionId
|
|
3398
3487
|
try {
|
|
3399
|
-
let sessionId
|
|
3400
3488
|
if (mode === 'session') {
|
|
3401
3489
|
const created = await jpost('/pet/session-new', newSessionContext(ctx))
|
|
3402
|
-
if (!created?.ok || !created.sessionId) throw new Error((created?.error || '没能创建会话') + (created?.hint ? ';' + created.hint : ''))
|
|
3490
|
+
if (!created?.ok || !created.sessionId) throw Object.assign(new Error((created?.error || '没能创建会话') + (created?.hint ? ';' + created.hint : '')), { sessionId: created?.sessionId })
|
|
3403
3491
|
sessionId = created.sessionId
|
|
3404
3492
|
}
|
|
3405
3493
|
/* ⭐ 时段不走请求体:host 从 config 里读常设时段,手动开始只负责"现在就跑" */
|
|
3406
3494
|
const body = mode === 'session'
|
|
3407
3495
|
? { mode, model: options.model, tokenBudget: options.tokenBudget, durationMinutes: options.durationMinutes, maxRounds: options.maxRounds }
|
|
3408
3496
|
: { mode, model: options.model, tokenBudget: options.tokenBudget, durationMinutes: options.durationMinutes }
|
|
3409
|
-
const r = await jpost('/pet/start', { ...body, ...(sessionId ? { sessionId } : {}) })
|
|
3497
|
+
const r = await jpost('/pet/start', { ...body, ...(sessionId ? { sessionId } : {}), ...(mode === 'session' && curiosityId ? { curiosityId } : {}) })
|
|
3410
3498
|
if (!r?.ok || r.started === false) throw new Error((r?.error || r?.hint || '没有启动') + (r?.error && r?.hint ? ';' + r.hint : ''))
|
|
3411
3499
|
if (sessionId) {
|
|
3412
3500
|
try {
|
|
@@ -3416,7 +3504,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3416
3504
|
say('会话任务已启动。', 'good')
|
|
3417
3505
|
} else say('后台任务已启动。', 'good')
|
|
3418
3506
|
try { const fresh = await jget('/status'); if (fresh?.ok) setSt(fresh) } catch { /* 下次轮询补上 */ }
|
|
3419
|
-
|
|
3507
|
+
return true
|
|
3508
|
+
} catch (e) { const retainedSessionId = sessionId || e?.sessionId; say(String(e?.message ?? e) + (retainedSessionId ? ';已创建的会话仍保留:' + retainedSessionId : ''), 'warn'); return false }
|
|
3420
3509
|
finally { runLock.current = false; setBusy('') }
|
|
3421
3510
|
}
|
|
3422
3511
|
const controlRun = async (action) => {
|
|
@@ -3442,12 +3531,36 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3442
3531
|
models.map((m) => h('option', { key: m.id, value: m.id }, m.label || m.id)))
|
|
3443
3532
|
: h('input', { ...props, placeholder: 'provider/model;留空使用默认模型' })
|
|
3444
3533
|
}
|
|
3534
|
+
const stopLabel = reason => ({ user: '手动停止', exit: '本段已结束', 'round-limit': '已达本趟轮数上限', 'token-budget': '本趟 token 预算已用完',
|
|
3535
|
+
'total-budget': '累计 token 上限已到', 'time-limit': '已到运行时限', 'max-tokens': '输出达到本次上限', 'model-error': '模型请求失败',
|
|
3536
|
+
interrupted: '任务被中断', 'mode-exit': '已切出鲸鱼摸鱼模式', error: '执行出错', 'spawn-error': '后台启动失败' }[reason] || String(reason || ''))
|
|
3537
|
+
const runState = mode => {
|
|
3538
|
+
const f = st.feiyu || {}, bg = st.backgroundRun || {}, hold = st.bgWindow || {}, continuation = f.continuation || {}
|
|
3539
|
+
if (mode === 'session') {
|
|
3540
|
+
if (continuation.state === 'waiting-retry') return { label: '等待自动续刷', note: (continuation.reason === 'max-tokens' ? '输出达到本次上限,稍后自动接着刷。' : (stopLabel(continuation.reason) || '本轮暂时结束') + ',稍后自动重试续刷。') + (Number(continuation.nextAt) > 0 ? ' 预计 ' + petFmtTime(continuation.nextAt) + '。' : ''), waiting: true }
|
|
3541
|
+
if (continuation.state === 'needs-attention') return { label: '需要处理', note: (stopLabel(continuation.reason || f.stopReason) || '当前无法自动续刷') + ',请核对后点击「继续运行」。', warning: true }
|
|
3542
|
+
if (f.active && f.goal?.activation === 'disarmed') return { label: '需要处理', note: '本轮仍在收尾,但自动续刷已停用;不会继续下一轮。', warning: true }
|
|
3543
|
+
if (f.active) return { label: '运行中', note: f.source === 'manual' ? '正在处理当前手动刷站任务。' : '正在按当前目标继续浏览。' }
|
|
3544
|
+
const paused = f.goal && ['paused', 'blocked'].includes(f.goal.phase)
|
|
3545
|
+
const disarmed = f.goal?.activation === 'disarmed' || f.goal?.phase === 'active'
|
|
3546
|
+
return { label: paused ? '已暂停' : disarmed ? '续刷未启用' : '空闲',
|
|
3547
|
+
note: f.stopReason ? '停止原因:' + stopLabel(f.stopReason) + '。' : disarmed ? '任务记录仍在,当前没有自动续刷。点击「继续运行」后恢复。' : paused ? '当前没有继续浏览,可核对任务后继续。' : '当前没有运行中的会话刷站任务。', warning: Boolean(paused || disarmed) }
|
|
3548
|
+
}
|
|
3549
|
+
if (bg.continuationPending) return { label: '等待后台接力', note: '本段已结束,接力任务已排队' + (Number(bg.nextStartAt) > 0 ? ',预计 ' + petFmtTime(bg.nextStartAt) : '') + '。可停止接力。', waiting: true }
|
|
3550
|
+
if (st.bgBusy) return { label: bg.task === 'learn-review' ? '学习审核中' : '运行中', note: bg.task === 'learn-review' ? '后台正在审核学习记录,当前不是刷视频任务。' : '后台正在按本趟时限继续浏览。' }
|
|
3551
|
+
if (Number(hold.holdUntil) > Date.now()) return { label: hold.holdReason === 'rest' ? '自然休息中' : hold.holdReason === 'manual' ? '本时段已停止' : '自动运行已暂停',
|
|
3552
|
+
note: (hold.holdReason === 'rest' ? '自然休息至 ' : hold.holdReason === 'manual' ? '手动停止,暂停至 ' : '暂停至 ') + petFmtTime(hold.holdUntil)
|
|
3553
|
+
+ (hold.holdReason === 'rest' ? ',之后在有效时段内自动恢复。' : ';点「开始后台刷」可立即恢复。') }
|
|
3554
|
+
return { label: '空闲', note: '当前没有运行中的后台任务。' }
|
|
3555
|
+
}
|
|
3445
3556
|
const runPage = (mode) => {
|
|
3446
3557
|
const session = mode === 'session'
|
|
3447
3558
|
const options = runSettings(mode)
|
|
3448
3559
|
const fy = st.feiyu || {}
|
|
3449
|
-
const
|
|
3560
|
+
const state = runState(mode)
|
|
3561
|
+
const live = session ? Boolean(fy.active || state.waiting) : Boolean(st.bgBusy || st.backgroundRun?.continuationPending)
|
|
3450
3562
|
const tokens = fy.tokens || {}
|
|
3563
|
+
const windowDirty = !session && (String(options.windowStart || '') !== String(cfg.backgroundWindowStart || '') || String(options.windowEnd || '') !== String(cfg.backgroundWindowEnd || ''))
|
|
3451
3564
|
return h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': mode },
|
|
3452
3565
|
h('div', { 'data-bd-pet-grid': '' },
|
|
3453
3566
|
h('section', { 'data-bd-sec': '' }, heading('sliders', '运行参数'),
|
|
@@ -3463,26 +3576,31 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3463
3576
|
h('label', { 'data-bd-field': '', 'data-bd-grow': '' }, h('span', { 'data-bd-label': '' }, '结束(可跨午夜)'),
|
|
3464
3577
|
h('input', { 'data-bd-input': '', type: 'time', value: options.windowEnd || '', onChange: (e) => updateRun(mode, 'windowEnd', e.target.value) }))),
|
|
3465
3578
|
] : []),
|
|
3579
|
+
windowDirty ? h('div', { 'data-bd-msg': '', 'data-warn': '', 'data-bd-window-draft': '', role: 'status' }, '连续时段有未保存草稿。请先「保存默认设置」让新时段生效;直接开始后台刷仍使用已保存的时段。') : null,
|
|
3466
3580
|
h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, session ? '最多运行几分钟(0 = 不限时)' : '没填时段时,一轮刷几分钟'),
|
|
3467
3581
|
h('input', { 'data-bd-input': '', type: 'number', min: session ? 0 : 1, max: session ? 1440 : 180, step: 1,
|
|
3468
3582
|
value: options.durationMinutes, onChange: (e) => updateRun(mode, 'durationMinutes', e.target.value) })),
|
|
3583
|
+
h('details', { 'data-bd-advanced': '', 'data-bd-run-resources': '' }, h('summary', {}, '资源上限', h('span', { 'data-bd-note': '' }, ' · '
|
|
3584
|
+
+ (Number(options.tokenBudget) > 0 ? Number(options.tokenBudget).toLocaleString() + ' token' : 'token 不限')
|
|
3585
|
+
+ (session ? ' · 最多 ' + options.maxRounds + ' 轮' : ' · 软预算'))), h('div', {},
|
|
3469
3586
|
session ? h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '最多刷多少轮'),
|
|
3470
3587
|
h('input', { 'data-bd-input': '', type: 'number', min: 1, max: numLim('maxRounds', 1, 1000)[1], step: 1,
|
|
3471
3588
|
value: options.maxRounds, onChange: (e) => updateRun(mode, 'maxRounds', e.target.value) })) : null,
|
|
3472
3589
|
h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '这趟 token 预算(0 = 不限)'),
|
|
3473
3590
|
h('input', { 'data-bd-input': '', type: 'number', min: 0, max: 5000000, step: 1000,
|
|
3474
|
-
value: options.tokenBudget, onChange: (e) => updateRun(mode, 'tokenBudget', e.target.value) })),
|
|
3591
|
+
value: options.tokenBudget, onChange: (e) => updateRun(mode, 'tokenBudget', e.target.value) })))),
|
|
3475
3592
|
h('div', { 'data-bd-note': '' }, session
|
|
3476
3593
|
? '轮数与 token 预算在每轮结束后检查;到达时限后暂停续轮。会话内的新消息按宿主中断规则处理。'
|
|
3477
3594
|
: (String(options.windowStart || '') && String(options.windowEnd || '')
|
|
3478
|
-
?
|
|
3595
|
+
? `${windowDirty ? '时段草稿(未生效)' : '已保存时段'}:${options.windowStart}–${options.windowEnd}。保存后的时段内会持续接力至结束;重新打开 DSH 后仍按已保存时段调度。手动「停止」后本时段不再自动拉起。token 预算是软目标,不能精确截断。`
|
|
3479
3596
|
: '后台时段为空时,按单次运行时长执行;提前结束会在剩余时长内续接。后台 token 预算为软目标,不能按精确用量自动截断。')),
|
|
3480
3597
|
h('div', { 'data-bd-row': '' },
|
|
3481
3598
|
h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => saveRun(mode) }, '保存默认设置')),
|
|
3482
3599
|
h('div', { 'data-bd-note': '' }, '两种方式分别保存。模型、时长与预算用于下一次启动;后台时段保存后生效。运行中任务不受草稿影响。')),
|
|
3483
|
-
h('section', { 'data-bd-sec': '' }, heading('pulse', '运行状态'),
|
|
3600
|
+
h('section', { 'data-bd-sec': '', 'data-bd-run-state': mode }, heading('pulse', '运行状态'),
|
|
3484
3601
|
h('span', { 'data-bd-badge': '', 'data-live': live ? '' : undefined },
|
|
3485
|
-
|
|
3602
|
+
state.label),
|
|
3603
|
+
h('div', { 'data-bd-note': '', 'data-bd-run-state-note': '', role: state.warning ? 'alert' : 'status' }, state.note),
|
|
3486
3604
|
session ? h('div', { 'data-bd-readout': '' },
|
|
3487
3605
|
h('div', {}, h('b', {}, fy.goal ? (fy.goal.rounds || 0) + ' / ' + (fy.goal.maxRounds || 0) : fy.source === 'manual' ? '手动刷站' : '—'), h('span', {}, '已刷轮数')),
|
|
3488
3606
|
h('div', {}, h('b', {}, tokens.thisRun === null || tokens.thisRun === undefined || tokens.tokensUnknown ? '未计量' : Number(tokens.thisRun).toLocaleString()), h('span', {}, '本趟 token')),
|
|
@@ -3510,10 +3628,16 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3510
3628
|
catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') }
|
|
3511
3629
|
} }, '停止后台任务') : null,
|
|
3512
3630
|
!session && st.lastRunAt ? h('div', { 'data-bd-note': '' }, '上次运行:' + petFmtTime(st.lastRunAt)) : null,
|
|
3631
|
+
(() => {
|
|
3632
|
+
const rows = Array.isArray(st.activity) ? st.activity : []
|
|
3633
|
+
const last = rows.slice().reverse().find(row => ['call', 'done', 'fail'].includes(row.kind) && row.text)
|
|
3634
|
+
return last ? h('div', { 'data-bd-note': '', 'data-bd-last-action': '' }, '最近动作(状态快照):' + String(last.text).slice(0, 240))
|
|
3635
|
+
: h('div', { 'data-bd-note': '', 'data-bd-last-action': '' }, '状态快照暂无工具动作,执行明细请查看运行日志。')
|
|
3636
|
+
})(),
|
|
3513
3637
|
h('div', { 'data-bd-note': '' }, '执行明细保存在「记录与用量」中。'),
|
|
3514
3638
|
/* ⏸️ 免跑牌期间如实说(st.bgWindow.holdUntil 由宿主 /status 提供) */
|
|
3515
3639
|
!session && st.bgWindow && Number(st.bgWindow.holdUntil) > Date.now()
|
|
3516
|
-
? h('div', { 'data-bd-note': '' },
|
|
3640
|
+
? h('div', { 'data-bd-note': '' }, st.bgWindow.holdReason === 'rest' ? '自然休息期间不会接力;休息结束后重新检查时段。' : '暂停期间不自动拉起,手动开始可恢复。')
|
|
3517
3641
|
: null)))
|
|
3518
3642
|
}
|
|
3519
3643
|
|
|
@@ -3546,8 +3670,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3546
3670
|
h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, st.schedule && st.schedule.lastError ? '上次失败:' + st.schedule.lastError : st.schedule && st.schedule.lastStartedAt ? '上次启动:' + petFmtTime(st.schedule.lastStartedAt) : '尚未启动'),
|
|
3547
3671
|
st.schedule && st.schedule.nextRunAt ? h('span', { 'data-bd-note': '' }, '下次检查:' + petFmtTime(st.schedule.nextRunAt)) : null)) : null)
|
|
3548
3672
|
const basicCards = [
|
|
3549
|
-
() => h('div', { 'data-bd-
|
|
3550
|
-
heading('spark', '桌宠'),
|
|
3673
|
+
() => h('div', { 'data-bd-pet-preferences': '' },
|
|
3551
3674
|
ToggleRow({
|
|
3552
3675
|
label: '显示桌宠', on: !(st && st.petShow === false), disabled: Boolean(busy),
|
|
3553
3676
|
note: st && st.petShow === false
|
|
@@ -3560,9 +3683,9 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3560
3683
|
note: '按已配置频率显示闲聊,频率由陪伴节奏控制。',
|
|
3561
3684
|
onChange: (v) => patch({ idleChatter: v }, '闲聊泡泡'),
|
|
3562
3685
|
}), h(PetCompanionSettings, { st, busy, patch, say })),
|
|
3563
|
-
() => h('div', { 'data-bd-sec': '' },
|
|
3564
|
-
heading('spark', '
|
|
3565
|
-
h('div', { 'data-bd-note': '' }, '
|
|
3686
|
+
() => h('div', { 'data-bd-sec': '', 'data-bd-basic-card': 'character' },
|
|
3687
|
+
heading('spark', '角色与陪伴'),
|
|
3688
|
+
h('div', { 'data-bd-note': '' }, '选择她陪你冲浪的方式,仅在鲸鱼摸鱼模式中生效。'),
|
|
3566
3689
|
h('div', { 'data-bd-field': '' },
|
|
3567
3690
|
h('span', { 'data-bd-label': '' }, '陪伴档位'),
|
|
3568
3691
|
h('div', { 'data-bd-segments': '', role: 'group', 'aria-label': '陪伴档位' },
|
|
@@ -3574,24 +3697,16 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3574
3697
|
h('div', { 'data-bd-row': '' },
|
|
3575
3698
|
h('span', { 'data-bd-grow': '' }),
|
|
3576
3699
|
h('button', { 'data-bd-btn': '', 'data-sm': '', 'data-primary': '', disabled: Boolean(busy), onClick: () => patch({ interactionStyle: interactionStyle || 'companion' }, '陪伴档位') }, '保存档位')),
|
|
3577
|
-
h('
|
|
3578
|
-
'
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3700
|
+
h('details', { 'data-bd-advanced': '', 'data-bd-companion-details': '' }, h('summary', {}, '桌宠与提醒', h('span', { 'data-bd-note': '' }, ' · '
|
|
3701
|
+
+ (cfg.focusMode ? '专注中' : cfg.idleChatter === false ? '闲聊关闭' : ({ rare: '低频闲聊', normal: '标准闲聊', lively: '活泼闲聊' }[cfg.chatterFrequency || 'normal']))
|
|
3702
|
+
+ (cfg.quietHoursEnabled ? ' · 安静 ' + (cfg.quietHoursStart || '23:00') + '–' + (cfg.quietHoursEnd || '08:00') : ''))), h('div', {}, basicCards[0]()))),
|
|
3703
|
+
() => h('div', { 'data-bd-sec': '', 'data-bd-basic-card': 'reports' },
|
|
3704
|
+
heading('list', '报告与参考'),
|
|
3705
|
+
h('div', { 'data-bd-note': '' }, '只限制留片与报告收录的精选数量,不限制继续浏览或独立互动,也不是每天要完成的数量。'),
|
|
3582
3706
|
h('div', { 'data-bd-row': '' },
|
|
3583
|
-
h('span', { 'data-bd-
|
|
3584
|
-
persona && persona.length > 400 ? `已写 ${persona.length} 字(每轮都占一份 token)` : ''),
|
|
3585
|
-
h('button', {
|
|
3586
|
-
'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy),
|
|
3587
|
-
onClick: () => patch({ persona: String(persona || '').trim() }, '人格设定'),
|
|
3588
|
-
}, '保存'))),
|
|
3589
|
-
() => h('div', { 'data-bd-sec': '' },
|
|
3590
|
-
heading('list', '限量与报告'),
|
|
3591
|
-
h('div', { 'data-bd-row': '' },
|
|
3592
|
-
h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '一天最多挑几条'),
|
|
3707
|
+
h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '每日精选上限'),
|
|
3593
3708
|
h('input', {
|
|
3594
|
-
'data-bd-input': '', 'data-bd-num': '', type: 'number', 'aria-label': '
|
|
3709
|
+
'data-bd-input': '', 'data-bd-num': '', type: 'number', 'aria-label': '每日精选上限',
|
|
3595
3710
|
min: numLim('perDay', 0, 50)[0], max: numLim('perDay', 0, 50)[1],
|
|
3596
3711
|
value: perDay === null ? '5' : perDay, onChange: (e) => setPerDay(e.target.value),
|
|
3597
3712
|
})),
|
|
@@ -3604,30 +3719,15 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3604
3719
|
h('option', { value: 'full' }, '完整'),
|
|
3605
3720
|
h('option', { value: 'brief' }, '精简'),
|
|
3606
3721
|
h('option', { value: 'off' }, '不写'))),
|
|
3607
|
-
h('
|
|
3608
|
-
h('button', {
|
|
3609
|
-
'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy),
|
|
3610
|
-
onClick: () => patch({ perDay: clampField('perDay', perDay, 0, 50), reportLevel: rl || 'full' }, '限量与报告'),
|
|
3611
|
-
}, '保存'))),
|
|
3612
|
-
() => h('div', { 'data-bd-sec': '' },
|
|
3613
|
-
heading('dots', '长期偏好'),
|
|
3614
|
-
h('div', { 'data-bd-note': '' }, '关键词与参考文件提供长期背景,不限制她刷什么。参考路径不改变新会话所属工作区。'),
|
|
3615
|
-
h('textarea', {
|
|
3616
|
-
'data-bd-input': '', 'aria-label': '长期偏好关键词', rows: 3, placeholder: '一行一个',
|
|
3617
|
-
value: kws === null ? '' : kws, onChange: (e) => setKws(e.target.value),
|
|
3618
|
-
}),
|
|
3619
|
-
h('input', {
|
|
3722
|
+
h('details', { 'data-bd-advanced': '', 'data-bd-reference-details': '' }, h('summary', {}, '参考文件', h('span', { 'data-bd-note': '' }, wsDir ? ' · 已设置' : ' · 未设置')), h('div', {}, h('input', {
|
|
3620
3723
|
'data-bd-input': '', 'aria-label': '参考文件或目录', placeholder: '参考文件或目录(可选)',
|
|
3621
3724
|
value: wsDir === null ? '' : wsDir, onChange: (e) => setWsDir(e.target.value),
|
|
3622
|
-
}),
|
|
3725
|
+
}), h('div', { 'data-bd-note': '' }, '为报告提供背景资料,不改变新会话所属工作区。'))),
|
|
3623
3726
|
h('div', { 'data-bd-row': '' },
|
|
3624
3727
|
h('button', {
|
|
3625
|
-
'data-bd-btn': '', disabled: Boolean(busy),
|
|
3626
|
-
onClick:
|
|
3627
|
-
|
|
3628
|
-
await patch({ keywords: list, workspace: String(wsDir || '').trim() }, '长期偏好')
|
|
3629
|
-
},
|
|
3630
|
-
}, '保存'))),
|
|
3728
|
+
'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy),
|
|
3729
|
+
onClick: () => patch({ perDay: clampField('perDay', perDay, 0, 50), reportLevel: rl || 'full', workspace: String(wsDir || '').trim() }, '报告与参考'),
|
|
3730
|
+
}, '保存报告设置'))),
|
|
3631
3731
|
() => {
|
|
3632
3732
|
const info = st && st.pace && st.pace.explanation
|
|
3633
3733
|
const saved = Number(cfg.paceFactor ?? info?.factor ?? 1)
|
|
@@ -3664,12 +3764,13 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3664
3764
|
!valid ? h('div', { 'data-bd-msg': '', 'data-warn': '' }, `请填 ${lo}–${hi} 之间的数字;空白或越界草稿不会保存。`) : null,
|
|
3665
3765
|
h('div', { 'data-bd-note': '' }, info?.notes.interval || '保存只影响下一次成功发送后新钉下的间隔与随机抖动;已钉下的等待不会重算。'),
|
|
3666
3766
|
info ? h('div', { 'data-bd-sub': '', 'data-bd-pace-preview': '' },
|
|
3667
|
-
h('div', { 'data-bd-h': '' }, changed ? '
|
|
3767
|
+
h('div', { 'data-bd-h': '' }, changed ? '间隔草稿与当前小时额度(不是当前倒计时)' : '当前间隔与小时额度(不是当前倒计时)'),
|
|
3668
3768
|
...Object.entries(info.kinds).map(([key, v]) => h('div', { key, 'data-bd-note': '', 'data-bd-pace-range': key },
|
|
3669
3769
|
h('b', {}, v.label),
|
|
3670
3770
|
`:基础 ${range(v, 1)} · 已保存 ${range(v, saved)}`,
|
|
3671
3771
|
changed ? ` · 草稿 ${range(v, draft)}` : '',
|
|
3672
|
-
` ·
|
|
3772
|
+
` · 当前本地额度 ${v.hourCap} 条/滚动小时`)),
|
|
3773
|
+
h('div', { 'data-bd-note': '' }, '小时额度按后端当前策略显示,不随间隔倍数改变;草稿只预览间隔,实际等待见发送状态。'))
|
|
3673
3774
|
: h('div', { 'data-bd-note': '' }, '节奏说明尚未就绪;不会猜基础范围,完整重启 DSH 后重试。'),
|
|
3674
3775
|
h('div', { 'data-bd-row': '' },
|
|
3675
3776
|
h('button', {
|
|
@@ -3816,10 +3917,32 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3816
3917
|
h('span', { 'data-bd-badge': '', 'data-live': s.done ? '' : undefined }, s.done ? '✓ 已完成' : '待办'),
|
|
3817
3918
|
h('span', { 'data-bd-grow': '', style: { fontWeight: s.done ? 400 : 600 } }, s.label),
|
|
3818
3919
|
s.done ? null : h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => setTab(s.view) }, '配置')))) : null,
|
|
3819
|
-
h('div', { 'data-bd-pet-grid': '', 'data-bd-basic-grid': '' },
|
|
3920
|
+
h('div', { 'data-bd-pet-grid': '', 'data-bd-basic-grid': '' }, h(PetBrowseSettings, { st, busy, patch, say }), basicCards[1](), basicCards[2]()))
|
|
3921
|
+
}
|
|
3922
|
+
const personaCard = () => {
|
|
3923
|
+
const draft = String(persona === null ? cfg.persona || '' : persona)
|
|
3924
|
+
const saved = String(cfg.persona || '').trim()
|
|
3925
|
+
const changed = draft.trim() !== saved
|
|
3926
|
+
return h('section', { 'data-bd-sec': '', 'data-bd-persona-card': '' },
|
|
3927
|
+
h('div', { 'data-bd-row': '' }, heading('spark', '全局人格'),
|
|
3928
|
+
h('span', { 'data-bd-badge': '' }, saved ? '自定义人格' : '内置默认')),
|
|
3929
|
+
h('div', { 'data-bd-note': '' }, '覆盖鲸鱼摸鱼模式的聊天及所有 AI 发言,包括会话与后台刷站、评论与回复、动态和私信。普通会话不受影响;桌宠的本地状态通知使用固定文案。'),
|
|
3930
|
+
h('div', { 'data-bd-note': '' }, '填写后以你的全局人格为准;留空使用内置默认:活泼可爱,像群友一样接梗、善意嘴硬,严肃问题认真回答。评论和动态用自己的话,不照搬原句或写成引用解说。'),
|
|
3931
|
+
h('textarea', { 'data-bd-input': '', rows: 8, maxLength: 4000, 'aria-label': '大肥鱼的全局人格', value: draft,
|
|
3932
|
+
placeholder: '留空使用内置默认。可以完整描述性格、相处方式与表达偏好,不必提供固定台词。',
|
|
3933
|
+
style: { minHeight: '168px', resize: 'vertical' }, onChange: e => setPersona(e.target.value) }),
|
|
3934
|
+
h('div', { 'data-bd-row': '' },
|
|
3935
|
+
h('span', { 'data-bd-note': '', 'data-bd-grow': '', 'data-bd-persona-status': '', role: 'status' },
|
|
3936
|
+
changed ? '有未保存修改 · ' + draft.length + '/4000 字' : saved ? '已保存自定义人格 · 后续表达使用此设定' : '当前使用内置默认人格'),
|
|
3937
|
+
h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy) || !changed,
|
|
3938
|
+
onClick: async () => {
|
|
3939
|
+
const value = draft.trim()
|
|
3940
|
+
if (await patch({ persona: value }, '全局人格')) setPersona(current => current === draft ? value : current)
|
|
3941
|
+
} }, '保存全局人格')))
|
|
3820
3942
|
}
|
|
3821
3943
|
const speechPage = () => h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'speech' },
|
|
3822
|
-
pageHead('互动与投递', '
|
|
3944
|
+
pageHead('互动与投递', '管理全局人格、公开发言、私信投递、自动回复和发送限制。'),
|
|
3945
|
+
personaCard(),
|
|
3823
3946
|
h(PetSendReviewPanel, { st, patch, busy, say }),
|
|
3824
3947
|
h('div', { 'data-bd-pet-grid': '' },
|
|
3825
3948
|
h('section', { 'data-bd-sec': '' }, heading('spark', '公开发言'), ToggleRow({
|
|
@@ -3899,7 +4022,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3899
4022
|
onChange: (v) => patch({ dmReplyFollowedOnly: v }, '仅回复已关注的用户'),
|
|
3900
4023
|
}))) : null,
|
|
3901
4024
|
h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => patch({ dmMode: dmMode || 'off', dmUid: String(dmUid || '').trim() }, '私信投递设置') }, '保存投递设置')),
|
|
3902
|
-
basicCards[
|
|
4025
|
+
basicCards[3](), h('section', { 'data-bd-sec': '' }, heading('pulse', '发送状态'), (() => {
|
|
3903
4026
|
const pc = st && st.pace
|
|
3904
4027
|
if (!pc || pc.ok === false) return h('div', { 'data-bd-note': '' }, '发送节奏读取失败。可重启 DSH 后重新加载。')
|
|
3905
4028
|
const cd = pc.cooldown
|
|
@@ -4002,39 +4125,53 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4002
4125
|
const f = st?.feiyu || {}
|
|
4003
4126
|
const bg = st?.backgroundRun || {}
|
|
4004
4127
|
const sessionOn = Boolean(f.active)
|
|
4005
|
-
const backgroundOn = Boolean(st?.bgBusy)
|
|
4128
|
+
const backgroundOn = Boolean(st?.bgBusy || bg.continuationPending)
|
|
4006
4129
|
const windowOn = Boolean(st?.bgWindow?.start && st?.bgWindow?.end)
|
|
4007
|
-
const
|
|
4008
|
-
const
|
|
4130
|
+
const sessionState = runState('session'), backgroundState = runState('background')
|
|
4131
|
+
const waitingSession = Boolean(sessionState.waiting)
|
|
4132
|
+
const active = sessionOn || backgroundOn || waitingSession
|
|
4133
|
+
const holdOn = Number(st?.bgWindow?.holdUntil) > Date.now()
|
|
4134
|
+
const scheduleState = st.schedule || {}
|
|
4135
|
+
const scheduleNext = c.scheduleEnabled ? scheduleState.lastError ? '定时检查失败:' + scheduleState.lastError
|
|
4136
|
+
: Number(scheduleState.nextRunAt) > 0 || scheduleState.nextRunAt ? '下次定时检查:' + petFmtTime(scheduleState.nextRunAt)
|
|
4137
|
+
: scheduleState.inWindow ? '当前在每日定时窗口内,等待宿主检查;每天启动一次。' : '等待进入每日定时窗口。' : ''
|
|
4138
|
+
const mode = sessionOn || waitingSession ? '当前会话' : backgroundOn ? (bg.task === 'learn-review' ? '后台学习审核' : '后台连续冲浪') : holdOn ? backgroundState.label
|
|
4139
|
+
: windowOn ? '连续时段已安排' : c.scheduleEnabled ? '每日定时已安排' : '未安排'
|
|
4140
|
+
const next = sessionOn || waitingSession ? sessionState.note : backgroundOn ? backgroundState.note : holdOn ? backgroundState.note
|
|
4141
|
+
: windowOn ? (st.bgWindow.active ? '当前在连续时段内,等待后台调度。' : '到已保存的连续时段后自动开始。') : scheduleNext || '等待手动开始'
|
|
4009
4142
|
const schedule = c.scheduleEnabled ? (c.scheduleAllDay ? '全天检查' : `${c.scheduleStart || '18:00'}–${c.scheduleEnd || '09:00'}`) : '未开启'
|
|
4010
|
-
const selectedBackground = backgroundOn || (!sessionOn && runMode === 'background')
|
|
4143
|
+
const selectedBackground = backgroundOn || (!sessionOn && !waitingSession && runMode === 'background')
|
|
4011
4144
|
const budgetValue = sessionOn ? f.tokens?.budget : backgroundOn ? bg.tokenBudget : selectedBackground ? c.backgroundTokenBudget : c.sessionTokenBudget
|
|
4012
4145
|
const budget = Number(budgetValue) > 0 ? Number(budgetValue).toLocaleString() + ' token / 趟' : '本趟不限(受总闸约束)'
|
|
4013
4146
|
return h('section', { 'data-bd-sec': '', 'data-bd-run-plan': '' },
|
|
4014
|
-
h('div', { 'data-bd-row': '' }, h('div', { 'data-bd-h': '', 'data-bd-grow': '' }, icon('pulse'), h('b', {}, '运行计划')), h('span', { 'data-bd-badge': '', 'data-live': active ? '' : undefined },
|
|
4147
|
+
h('div', { 'data-bd-row': '' }, h('div', { 'data-bd-h': '', 'data-bd-grow': '' }, icon('pulse'), h('b', {}, '运行计划')), h('span', { 'data-bd-badge': '', 'data-live': active ? '' : undefined }, sessionOn || st.bgBusy ? '正在执行' : active ? '等待续刷' : '待命')),
|
|
4015
4148
|
h('div', { 'data-bd-run-plan-grid': '' },
|
|
4016
4149
|
h('div', {}, h('span', {}, '现在'), h('strong', {}, mode)),
|
|
4017
|
-
h('div', {}, h('span', {}, '下一步'), h('strong', {},
|
|
4150
|
+
h('div', {}, h('span', {}, '下一步'), h('strong', {}, next)),
|
|
4018
4151
|
h('div', {}, h('span', {}, '模型'), h('strong', {}, sessionOn ? f.modelActive || f.model || '当前会话模型' : backgroundOn ? bg.model || '后台模型' : (selectedBackground ? c.backgroundModelId : c.sessionModelId) || '沿用宿主默认')),
|
|
4019
4152
|
h('div', {}, h('span', {}, '预算'), h('strong', {}, budget)),
|
|
4020
4153
|
h('div', {}, h('span', {}, '定时'), h('strong', {}, schedule)),
|
|
4021
4154
|
h('div', {}, h('span', {}, '完成反馈'), h('strong', {}, selectedBackground ? '桌宠提醒 + 运行日志' : '当前会话 + 运行日志'))),
|
|
4022
|
-
h('div', { 'data-bd-note': ''
|
|
4155
|
+
scheduleNext ? h('div', { 'data-bd-note': '', 'data-bd-schedule-plan': '', role: scheduleState.lastError ? 'alert' : 'status' }, scheduleNext) : null,
|
|
4156
|
+
h('div', { 'data-bd-note': '' }, active ? '切换设置页不会停止任务。等待续刷与接力也可用暂停或停止按钮取消。' : '当前会话、后台连续刷、定时窗口分别保存,互相不会覆盖参数。'))
|
|
4023
4157
|
}
|
|
4024
4158
|
const runView = () => h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'run' },
|
|
4025
4159
|
pageHead('运行方式', '会话运行、后台运行与定时计划。'),
|
|
4026
4160
|
st.toolScope?.checked && !st.toolScope.supported ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, '当前 DSH 不支持会话工具隔离。会话运行不可用,后台运行和本地管理仍可使用。') : null,
|
|
4027
4161
|
h('div', { 'data-bd-run-launches': '' }, ...['session', 'background'].map(mode => {
|
|
4028
4162
|
const isSession = mode === 'session', options = runSettings(mode)
|
|
4029
|
-
const
|
|
4163
|
+
const state = runState(mode)
|
|
4164
|
+
const active = isSession ? Boolean(st.feiyu?.active || state.waiting) : Boolean(st.bgBusy || st.backgroundRun?.continuationPending)
|
|
4030
4165
|
const title = isSession ? '新开会话刷' : '后台刷'
|
|
4031
4166
|
return h('section', { key: mode, 'data-bd-run-launch': mode },
|
|
4032
|
-
h('div', { 'data-bd-row': '' }, icon(isSession ? 'play' : 'moon'), h('h2', { 'data-bd-grow': '' }, title), h('span', { 'data-bd-badge': '', 'data-live': active ? '' : undefined },
|
|
4167
|
+
h('div', { 'data-bd-row': '' }, icon(isSession ? 'play' : 'moon'), h('h2', { 'data-bd-grow': '' }, title), h('span', { 'data-bd-badge': '', 'data-live': active ? '' : undefined }, state.label)),
|
|
4033
4168
|
h('p', {}, isSession ? '在独立会话中运行,保留对话与执行进度。' : '在后台执行,支持连续时段与每日定时计划。'),
|
|
4169
|
+
state.warning || state.waiting || !isSession && Number(st.bgWindow?.holdUntil) > Date.now() ? h('div', { 'data-bd-note': '' }, state.note) : null,
|
|
4034
4170
|
h('div', { 'data-bd-launch-meta': '' }, h('span', { title: options.model || '' }, options.model || (isSession ? '当前会话模型' : '宿主默认模型')), h('span', {}, Number(options.tokenBudget) > 0 ? Number(options.tokenBudget).toLocaleString() + ' token' : '不设单次 token 上限')),
|
|
4035
4171
|
h('button', { 'data-bd-btn': '', 'data-primary': '', 'data-bd-launch-action': mode, disabled: Boolean(busy) || active || (isSession && st.toolScope?.checked && !st.toolScope.supported),
|
|
4036
4172
|
onClick: () => startRun(mode) }, busy === 'start-' + mode ? '正在启动…' : isSession ? '新开会话刷' : '开始后台刷'))
|
|
4037
4173
|
})),
|
|
4174
|
+
h(PetCuriosityJournal, { st, busy, onExplore: curiosityId => { setRunMode('session'); return startRun('session', { curiosityId }) } }),
|
|
4038
4175
|
h('div', { 'data-bd-segments': '', role: 'group', 'aria-label': '运行方式' }, ...[['session','会话参数'], ['background','后台与定时']].map(([mode, label]) => h('button', { key: mode, 'aria-pressed': runMode === mode, onClick: () => setRunMode(mode) }, label))),
|
|
4039
4176
|
runPage(runMode), runMode === 'background' ? scheduleCard() : null,
|
|
4040
4177
|
h('details', { 'data-bd-run-details': '', open: Boolean(st.recovery?.needsAction) }, h('summary', {}, '运行计划与恢复'), runPlan(), weeklyRecoveryCard({ showReview: false })))
|
|
@@ -4060,7 +4197,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4060
4197
|
h('span', { 'data-bd-grow': '' }),
|
|
4061
4198
|
st.feiyu?.active ? h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => controlRun('stop') }, '暂停会话') : null,
|
|
4062
4199
|
st.bgBusy ? h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: async () => { setBusy('stop-bg'); try { const r = await jpost('/pet/stop', {}); if (!r?.ok) throw new Error(r?.error || '停止失败'); say('已请求停止后台任务', 'good') } catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') } } }, '停止后台任务') : null),
|
|
4063
|
-
logMode === 'events' ? settingLogCard() : basicCards[
|
|
4200
|
+
logMode === 'events' ? settingLogCard() : basicCards[4]())
|
|
4064
4201
|
const videoPage = () => h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'video' },
|
|
4065
4202
|
h('div', { 'data-bd-pghead': '' }, icon('film'), h('div', {}, h('b', {}, '视频工作室'),
|
|
4066
4203
|
h('p', {}, '视频制作、制作记录与本地成片。'))), pvCard())
|
|
@@ -4134,7 +4271,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4134
4271
|
}
|
|
4135
4272
|
|
|
4136
4273
|
const StablePetSettingsModal = typeof React.memo === 'function' ? React.memo(PetSettingsModal, (a, b) =>
|
|
4137
|
-
a.ctx === b.ctx && a.onClose === b.onClose && a.toast === b.toast && a.initialTab === b.initialTab
|
|
4274
|
+
a.ctx === b.ctx && a.onClose === b.onClose && a.toast === b.toast && a.initialTab === b.initialTab && a.initialRunMode === b.initialRunMode
|
|
4138
4275
|
&& a.status?.[0] === b.status?.[0] && a.status?.[1] === b.status?.[1] && a.status?.[2] === b.status?.[2]) : PetSettingsModal
|
|
4139
4276
|
|
|
4140
4277
|
/* 🐾 桌宠本体:状态灯 / 拖拽 / 弹菜单 / 新开会话刷(B 流程)都在这里编排。
|
|
@@ -4143,10 +4280,11 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4143
4280
|
function PetRoot({ ctx }) {
|
|
4144
4281
|
const [st, setSt, fails] = useStatus(5000)
|
|
4145
4282
|
const [pos, setPos] = React.useState(null) // null = 还没从 localStorage/默认位初始化
|
|
4146
|
-
const [panel, setPanel] = React.useState(null) // null | menu |
|
|
4283
|
+
const [panel, setPanel] = React.useState(null) // null | menu | status | settings(互斥)
|
|
4147
4284
|
const [dragging, setDragging] = React.useState(false)
|
|
4148
4285
|
const [sessBusy, setSessBusy] = React.useState(false)
|
|
4149
4286
|
const [settingsTab, setSettingsTab] = React.useState('run')
|
|
4287
|
+
const [settingsRunMode, setSettingsRunMode] = React.useState('session')
|
|
4150
4288
|
const [bubbles, setBubbles] = React.useState([])
|
|
4151
4289
|
const [toasts, toast] = useToasts()
|
|
4152
4290
|
const drag = React.useRef(null)
|
|
@@ -4312,18 +4450,19 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4312
4450
|
try {
|
|
4313
4451
|
let r1 = null
|
|
4314
4452
|
try { r1 = await jpost('/pet/session-new', newSessionContext(ctx)) }
|
|
4315
|
-
catch (e) { toast('创建会话失败:' + String(e?.message ?? e), 'warn'); return }
|
|
4453
|
+
catch (e) { toast('创建会话失败:' + String(e?.message ?? e) + (e?.sessionId ? ';已创建的会话仍保留:' + e.sessionId : ''), 'warn'); return }
|
|
4316
4454
|
if (!r1 || r1.ok === false) {
|
|
4317
|
-
toast('创建会话失败:' + ((r1 && r1.error) || '未知原因') + (r1 && r1.hint ? ';' + r1.hint : ''), 'warn')
|
|
4455
|
+
toast('创建会话失败:' + ((r1 && r1.error) || '未知原因') + (r1 && r1.hint ? ';' + r1.hint : '') + (r1?.sessionId ? ';已创建的会话仍保留:' + r1.sessionId : ''), 'warn')
|
|
4318
4456
|
return
|
|
4319
4457
|
}
|
|
4320
4458
|
/* 切过去看它刷:uiWorkspace 是宿主服务,可能没有/名字可能不同 ⇒ 全程防御式;
|
|
4321
4459
|
* 切不过去也不拦开刷 —— 会话照样开了,只是没自动跳过去。 */
|
|
4460
|
+
let opened = false
|
|
4322
4461
|
try {
|
|
4323
4462
|
const ws = (typeof ctx.get === 'function' ? ctx.get('uiWorkspace') : null)
|
|
4324
|
-
if (ws && typeof ws.openSession === 'function') ws.openSession(r1.sessionId)
|
|
4463
|
+
if (ws && typeof ws.openSession === 'function') { await ws.openSession(r1.sessionId); opened = true }
|
|
4325
4464
|
} catch { /* 切换会话失败不影响开刷 */ }
|
|
4326
|
-
toast('已开会话,鱼进去了', 'good')
|
|
4465
|
+
toast(opened ? '已开会话,鱼进去了' : '会话已创建(' + r1.sessionId + '),无法自动打开时可从侧栏进入。', opened ? 'good' : 'info')
|
|
4327
4466
|
let r2 = null
|
|
4328
4467
|
try { r2 = await jpost('/pet/start', { mode: 'session', sessionId: r1.sessionId }) }
|
|
4329
4468
|
catch (e) { toast('开刷没成:' + String(e?.message ?? e), 'warn'); return }
|
|
@@ -4497,10 +4636,10 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4497
4636
|
item('session', 'play', '新开会话刷',
|
|
4498
4637
|
sessBusy ? '正在开会话…' : `${modelTxt} · 预算 ${petFmtWan(cfg.sessionTokenBudget ?? cfg.tokenBudget)}`,
|
|
4499
4638
|
() => { setPanel(null); runNewSession() }, sessBusy, sessBusy),
|
|
4500
|
-
item('background', 'moon', '后台刷', '运行时长与定时参数', () => setPanel('
|
|
4639
|
+
item('background', 'moon', '后台刷', '运行时长与定时参数', () => { setSettingsTab('run'); setSettingsRunMode('background'); setPanel('settings') }),
|
|
4501
4640
|
item('logs', 'list', '运行记录', '', () => { setSettingsTab('logs'); setPanel('settings') }),
|
|
4502
4641
|
item('status', 'pulse', '运行状态', '', () => setPanel('status')),
|
|
4503
|
-
item('settings', 'gear', '工作台', '', () => { setSettingsTab('run'); setPanel('settings') }),
|
|
4642
|
+
item('settings', 'gear', '工作台', '', () => { setSettingsTab('run'); setSettingsRunMode('session'); setPanel('settings') }),
|
|
4504
4643
|
h('hr', { 'data-bd-pet-sep': '' }),
|
|
4505
4644
|
item('hide', 'hide', '隐藏桌宠', '', hidePet),
|
|
4506
4645
|
h('footer', { 'data-bd-menu-token': '' },
|
|
@@ -4542,10 +4681,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4542
4681
|
同时 [data-bd-pet][data-align] / [data-flip] 这些选择器也要求卡片是它的后代。 */
|
|
4543
4682
|
panel === 'menu' ? menuCard : null,
|
|
4544
4683
|
panel === 'menu' && preview ? h(PetMenuPreview, { key: preview.kind, kind: preview.kind, anchor: preview.anchor, st, fails, tokenValue, tokenNote, onEnter: holdPreview, onLeave: leavePreview }) : null,
|
|
4545
|
-
panel === 'bg' ? h(PetBgPanel, { st, toast, onDone: () => setPanel('log') }) : null,
|
|
4546
|
-
panel === 'log' ? h(PetLogPanel, { toast }) : null,
|
|
4547
4684
|
panel === 'status' ? h(PetStatusCard, { st, fails }) : null,
|
|
4548
|
-
panel === 'settings' ? h(StablePetSettingsModal, { onClose: closeSettings, toast, ctx, status: [st, setSt, fails], initialTab: settingsTab }) : null,
|
|
4685
|
+
panel === 'settings' ? h(StablePetSettingsModal, { onClose: closeSettings, toast, ctx, status: [st, setSt, fails], initialTab: settingsTab, initialRunMode: settingsRunMode }) : null,
|
|
4549
4686
|
h('div', { key: 'toast', 'data-bd-pet-toast': '', role: 'status', 'aria-live': 'polite', 'aria-atomic': false },
|
|
4550
4687
|
toasts.map((t) => h('div', { key: t.id, 'data-tone': t.tone || undefined }, t.text))),
|
|
4551
4688
|
]))
|