dsh-deepmoyu 2.7.1 → 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 +43 -0
- package/CONTRIBUTING.md +1 -0
- package/README.md +10 -3
- package/SECURITY.md +4 -0
- package/client.js +509 -296
- 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 +16 -2
- package/docs/RELEASING.md +15 -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 +941 -268
- package/lib/asr.py +22 -0
- 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/favops.py +38 -6
- package/lib/learn-review.mjs +13 -3
- package/lib/learn-store.mjs +7 -0
- package/lib/learn.mjs +234 -69
- package/lib/login.mjs +20 -3
- package/lib/mcp.mjs +5 -2
- package/lib/memory-center.mjs +21 -19
- package/lib/mirror.mjs +15 -1
- package/lib/mode.mjs +23 -9
- package/lib/model-relay-adapter.mjs +1 -1
- package/lib/model-relay.mjs +21 -4
- package/lib/pace.mjs +15 -7
- package/lib/persona.mjs +41 -30
- package/lib/pet-session.mjs +9 -3
- package/lib/product-status.mjs +38 -2
- package/lib/prompts.mjs +38 -37
- package/lib/pv-session.mjs +16 -1
- package/lib/pv.mjs +18 -3
- package/lib/pv.py +168 -30
- package/lib/pv_cover.py +11 -1
- package/lib/pv_editorial.py +28 -4
- package/lib/pv_plan.py +20 -2
- package/lib/pv_verify.py +14 -0
- package/lib/session-continuation.mjs +47 -0
- package/lib/state.mjs +6 -2
- package/lib/sticker-automation.mjs +16 -2
- package/lib/sticker.mjs +4 -1
- package/lib/uvfetch.mjs +38 -12
- 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 +204 -47
- package/vendor/bilibili-mcp/runtime_guard.py +164 -31
package/client.js
CHANGED
|
@@ -69,12 +69,21 @@ window.__ModuleLoader__.load({
|
|
|
69
69
|
error.sessionId = detail?.sessionId
|
|
70
70
|
throw error
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
/* ⏱ P3-38:所有 API 请求带 15s 超时 —— 后端卡死时请求不能永远挂着(挂住的 PATCH
|
|
73
|
+
* 会占住单飞挡板、挂住的轮询会一直 inFlight)。AbortSignal.timeout 在旧宿主上
|
|
74
|
+
* 可能不存在,缺了就退回无超时,行为跟以前一样;超时抛的是 DOMException
|
|
75
|
+
* (name=TimeoutError,没有 .status),各调用方的 catch 原样兜住,释放逻辑不受影响。 */
|
|
76
|
+
const FETCH_TIMEOUT_MS = 15000
|
|
77
|
+
const fetchSignal = () => {
|
|
78
|
+
try { return AbortSignal.timeout(FETCH_TIMEOUT_MS) } catch { return undefined }
|
|
79
|
+
}
|
|
80
|
+
const jget = async (p) => asJson(await fetch(API + p, { credentials: 'same-origin', signal: fetchSignal() }))
|
|
73
81
|
const jpost = async (p, body) => asJson(await fetch(API + p, {
|
|
74
82
|
method: 'POST',
|
|
75
83
|
credentials: 'same-origin',
|
|
76
84
|
headers: { 'content-type': 'application/json' },
|
|
77
85
|
body: JSON.stringify(body || {}),
|
|
86
|
+
signal: fetchSignal(),
|
|
78
87
|
}))
|
|
79
88
|
|
|
80
89
|
const CSS = `
|
|
@@ -971,6 +980,16 @@ window.__ModuleLoader__.load({
|
|
|
971
980
|
[data-bd-segments]{border-color:#d5e7f5;background:#edf6fd;padding:4px;border-radius:10px;}
|
|
972
981
|
[data-bd-segments] button{color:#466b84;padding:8px 13px;min-height:34px;}
|
|
973
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);}}
|
|
974
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;}
|
|
975
994
|
[data-bd-pet-modal] [data-bd-h] b{color:#2a4d65;}
|
|
976
995
|
[data-bd-pet-modal] [data-bd-note]{color:#537289;line-height:1.7;}
|
|
@@ -995,6 +1014,27 @@ window.__ModuleLoader__.load({
|
|
|
995
1014
|
[data-bd-run-details]>summary{cursor:pointer;color:#3b6681;font-size:12px;}
|
|
996
1015
|
[data-bd-run-details][open]>summary{margin-bottom:14px;}
|
|
997
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;}}
|
|
998
1038
|
[data-bd-week-recovery]>div:only-child{grid-column:1/-1;}
|
|
999
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;}
|
|
1000
1040
|
[data-bd-pet-modal] [data-bd-video-stage]:not([data-warn]){background:#e8f5ff;color:#216c94;border-color:#bfdff5;}
|
|
@@ -1108,6 +1148,41 @@ window.__ModuleLoader__.load({
|
|
|
1108
1148
|
return [st, externalSet.current, fails]
|
|
1109
1149
|
}
|
|
1110
1150
|
|
|
1151
|
+
/* 🔐 P2-15 单飞 PATCH 包装:并发 PATCH 响应乱序会把本地 config 回写成旧值(审计 P2-15)——
|
|
1152
|
+
* 例如「制作模型」select 连改两下、两个 PATCH 同时在飞,先发的那笔**后**回来,
|
|
1153
|
+
* patch 成功路径里的 setSt({config: r.config}) 会把界面回写成上一笔的旧值。
|
|
1154
|
+
* 这里保证同一时刻只有一个 /config 在飞:
|
|
1155
|
+
* · 在飞期间新到的改动只记「待发」(pending 按键合并、同键取最新值),界面控件
|
|
1156
|
+
* 照常更新本地显示、调用方拿到 true 不回弹;
|
|
1157
|
+
* · 当前请求结束后若有积压,再补发一次(补发永远带最新值;补发期间又来新值会
|
|
1158
|
+
* 走上面的排队分支,不会出现并发)。
|
|
1159
|
+
* 失败提示仍由 patch 内部的 say() 负责,单飞层不做二次弹错。 */
|
|
1160
|
+
function useLatestPatch(patch) {
|
|
1161
|
+
const patchRef = React.useRef(patch)
|
|
1162
|
+
patchRef.current = patch
|
|
1163
|
+
const inFlight = React.useRef(false)
|
|
1164
|
+
const pending = React.useRef(null) // { p, what }:在飞期间积压的改动(按键合并)
|
|
1165
|
+
const send = async (p, what) => {
|
|
1166
|
+
if (inFlight.current) {
|
|
1167
|
+
const old = pending.current
|
|
1168
|
+
pending.current = old
|
|
1169
|
+
? { p: { ...old.p, ...p }, what: [old.what, what].filter(Boolean).join('、') }
|
|
1170
|
+
: { p: { ...p }, what }
|
|
1171
|
+
return true
|
|
1172
|
+
}
|
|
1173
|
+
inFlight.current = true
|
|
1174
|
+
try {
|
|
1175
|
+
return await patchRef.current(p, what)
|
|
1176
|
+
} finally {
|
|
1177
|
+
inFlight.current = false
|
|
1178
|
+
const next = pending.current
|
|
1179
|
+
pending.current = null
|
|
1180
|
+
if (next) send(next.p, next.what).catch(() => {})
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return send
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1111
1186
|
/* 🗑 旧的全页设置 Panel 已退役(2026-10-27):它的配置项早已搬进 PetSettingsModal,
|
|
1112
1187
|
活动流水 / 出手节奏读数 / 判重账本行也一并搬了进来。回归断言改由 render-modal.cjs 盯弹窗本体。 */
|
|
1113
1188
|
|
|
@@ -1309,172 +1384,6 @@ window.__ModuleLoader__.load({
|
|
|
1309
1384
|
})))
|
|
1310
1385
|
}
|
|
1311
1386
|
|
|
1312
|
-
/* 🌙 后台刷一轮…:headless 独立进程那一轮(不占对话)。模型下拉复用 /status 的
|
|
1313
|
-
* models 清单(拿不到就退手填,判据与设置页一致);token 预算以「万」为单位
|
|
1314
|
-
* 显示与输入,提交时换算成整数。开跑走 POST /pet/start {mode:'background'}。 */
|
|
1315
|
-
function PetBgPanel({ st, toast, onDone }) {
|
|
1316
|
-
const cfg = (st && st.config) || {}
|
|
1317
|
-
const rows = Array.isArray(st && st.models) ? st.models : []
|
|
1318
|
-
const [model, setModel] = React.useState(null) // null = 还没从配置初始化过('' 是合法值:默认模型)
|
|
1319
|
-
const [wan, setWan] = React.useState(null) // 同上;'' = 不限
|
|
1320
|
-
const [busy, setBusy] = React.useState(false)
|
|
1321
|
-
const [tail, setTail] = React.useState(null)
|
|
1322
|
-
const lock = React.useRef(false) // 防双发:state 的 busy 要等重渲染才生效,连点两下会漏
|
|
1323
|
-
React.useEffect(() => {
|
|
1324
|
-
if (model === null) setModel(String(cfg.backgroundModelId ?? cfg.modelId ?? ''))
|
|
1325
|
-
if (wan === null) {
|
|
1326
|
-
const t = Number(cfg.backgroundTokenBudget ?? cfg.tokenBudget) || 0
|
|
1327
|
-
setWan(t > 0 ? String(Math.round(t / 10000)) : '')
|
|
1328
|
-
}
|
|
1329
|
-
}, [cfg])
|
|
1330
|
-
/* 日志尾巴:后台那轮「在不在动」只能靠这本账 —— 它不进对话、也不进活动流水 */
|
|
1331
|
-
const tailRev = React.useRef('')
|
|
1332
|
-
React.useEffect(() => {
|
|
1333
|
-
let dead = false
|
|
1334
|
-
const pull = async () => {
|
|
1335
|
-
try {
|
|
1336
|
-
const r = await jget('/pet/log?n=10' + (tailRev.current ? '&rev=' + encodeURIComponent(tailRev.current) : ''))
|
|
1337
|
-
if (!dead && r && r.ok !== false) {
|
|
1338
|
-
if (r.unchanged) return // ⭐ 没新增行就不动(省一次解析+渲染)
|
|
1339
|
-
if (r.rev) tailRev.current = String(r.rev)
|
|
1340
|
-
setTail(Array.isArray(r.entries) ? r.entries : [])
|
|
1341
|
-
}
|
|
1342
|
-
} catch { /* 下次轮询会补 */ }
|
|
1343
|
-
}
|
|
1344
|
-
pull()
|
|
1345
|
-
const t = setInterval(pull, 3000)
|
|
1346
|
-
return () => { dead = true; clearInterval(t) }
|
|
1347
|
-
}, [])
|
|
1348
|
-
const bgBusy = Boolean(st && st.bgBusy)
|
|
1349
|
-
const run = async () => {
|
|
1350
|
-
if (lock.current || busy || bgBusy) return // 绝不双发:上一轮还活着就不再叠一轮
|
|
1351
|
-
lock.current = true
|
|
1352
|
-
setBusy(true)
|
|
1353
|
-
try {
|
|
1354
|
-
const body = { mode: 'background', tokenBudget: Math.max(0, Math.round((parseFloat(wan) || 0) * 10000)) }
|
|
1355
|
-
if (model) body.model = model
|
|
1356
|
-
const r = await jpost('/pet/start', body)
|
|
1357
|
-
if (r && r.ok === false) {
|
|
1358
|
-
toast('后台没跑起来:' + (r.error || '未知原因') + (r.hint ? ' —— ' + r.hint : ''), 'warn')
|
|
1359
|
-
return
|
|
1360
|
-
}
|
|
1361
|
-
if (r && r.started) {
|
|
1362
|
-
toast('后台那轮跑起来了 —— 几分钟,做完日志里会有回执', 'good')
|
|
1363
|
-
if (onDone) onDone()
|
|
1364
|
-
} else {
|
|
1365
|
-
toast('后台没跑起来:' + ((r && r.error) || '可能上一轮还没结束'), 'warn')
|
|
1366
|
-
}
|
|
1367
|
-
} catch (e) { toast('后台没跑起来:' + String(e?.message ?? e), 'warn') }
|
|
1368
|
-
finally { lock.current = false; setBusy(false) }
|
|
1369
|
-
}
|
|
1370
|
-
const cur = model === null ? '' : String(model)
|
|
1371
|
-
const extra = cur && !rows.some((m) => m.id === cur) ? [{ id: cur, label: cur + '(不在当前清单里)' }] : []
|
|
1372
|
-
return h('div', { 'data-bd-pet-card': '', 'data-wide': '' },
|
|
1373
|
-
h('div', { 'data-bd-pet-cardhead': '' }, h('b', {}, '后台刷一轮'), h('span', {}, 'headless 独立进程')),
|
|
1374
|
-
h('div', { 'data-bd-pet-pad': '' },
|
|
1375
|
-
bgBusy ? h('span', { 'data-bd-pet-badge': '', 'data-warn': '' }, '后台正跑着呢 —— 等它做完再开下一轮') : null,
|
|
1376
|
-
h('div', { 'data-bd-row': '' },
|
|
1377
|
-
h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '模型'),
|
|
1378
|
-
rows.length
|
|
1379
|
-
? h('select', {
|
|
1380
|
-
'data-bd-input': '', 'data-bd-num': '', style: { width: '190px' },
|
|
1381
|
-
value: cur, onChange: (e) => setModel(e.target.value),
|
|
1382
|
-
},
|
|
1383
|
-
h('option', { value: '' }, '默认(跟着当前对话)'),
|
|
1384
|
-
extra.length ? h('option', { key: 'extra', value: extra[0].id }, extra[0].label) : null,
|
|
1385
|
-
rows.map((m) => h('option', { key: m.id, value: m.id }, m.label || m.id)))
|
|
1386
|
-
: h('input', {
|
|
1387
|
-
'data-bd-input': '', style: { width: '190px' }, placeholder: 'provider/model(留空 = 默认)',
|
|
1388
|
-
value: cur, onChange: (e) => setModel(e.target.value),
|
|
1389
|
-
})),
|
|
1390
|
-
h('div', { 'data-bd-row': '' },
|
|
1391
|
-
h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, 'token 预算(万 · 0 = 不限)'),
|
|
1392
|
-
h('input', {
|
|
1393
|
-
'data-bd-input': '', 'data-bd-num': '', style: { width: '88px' }, type: 'number', min: 0, step: 1,
|
|
1394
|
-
placeholder: '不限', value: wan === null ? '' : wan,
|
|
1395
|
-
onChange: (e) => setWan(e.target.value),
|
|
1396
|
-
})),
|
|
1397
|
-
h('div', { 'data-bd-note': '' }, '1 万 = 10000 token。后台预算是任务目标;实际运行时长在「设置 → 后台刷」中调整。'),
|
|
1398
|
-
h('div', { 'data-bd-row': '' },
|
|
1399
|
-
h('button', {
|
|
1400
|
-
'data-bd-btn': '', 'data-primary': '', disabled: busy || bgBusy, onClick: run,
|
|
1401
|
-
}, busy ? '开跑…' : '开跑'),
|
|
1402
|
-
busy ? h('i', { 'data-bd-pet-spin': '' }) : null)),
|
|
1403
|
-
tail && tail.length
|
|
1404
|
-
? h('div', {
|
|
1405
|
-
'data-bd-pet-log': '',
|
|
1406
|
-
style: { maxHeight: '122px', borderTop: '1px solid #d8e6ee' },
|
|
1407
|
-
},
|
|
1408
|
-
tail.slice(-4).map((e, i) => h('div', { 'data-bd-pet-logline': '', 'data-t': petEntryTone(e), key: i },
|
|
1409
|
-
h('time', {}, petFmtTime(e && e.at)), h('span', {}, petEntrySummary(e)))))
|
|
1410
|
-
: null)
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
/* 📜 运行日志:GET /pet/log,3 秒自动刷新;type 着色(start 绿 / stop 灰 / error 红 / note 蓝)。
|
|
1414
|
-
* 自动滚到底,但使用者往上翻时暂停自动滚(离底 <28px 才重新粘住),不然永远翻不上去。
|
|
1415
|
-
* 顶部「停止鲸鱼摸鱼」复用现有 POST /feiyu {action:'stop'}(跟设置页「停一下」同一个端点)。 */
|
|
1416
|
-
function PetLogPanel({ toast }) {
|
|
1417
|
-
const [entries, setEntries] = React.useState(null)
|
|
1418
|
-
const [busy, setBusy] = React.useState(false)
|
|
1419
|
-
const [err, setErr] = React.useState('')
|
|
1420
|
-
const listRef = React.useRef(null)
|
|
1421
|
-
const stick = React.useRef(true)
|
|
1422
|
-
const lock = React.useRef(false)
|
|
1423
|
-
const revRef = React.useRef('')
|
|
1424
|
-
React.useEffect(() => {
|
|
1425
|
-
let dead = false
|
|
1426
|
-
const pull = async () => {
|
|
1427
|
-
try {
|
|
1428
|
-
const r = await jget('/pet/log?n=200' + (revRef.current ? '&rev=' + encodeURIComponent(revRef.current) : ''))
|
|
1429
|
-
if (dead) return
|
|
1430
|
-
if (!r || r.ok === false) { setErr((r && r.error) || '读不到'); return }
|
|
1431
|
-
if (r.unchanged) return // ⭐ 日志没新增行:不重解析、不重渲染
|
|
1432
|
-
if (r.rev) revRef.current = String(r.rev)
|
|
1433
|
-
setEntries(Array.isArray(r.entries) ? r.entries : [])
|
|
1434
|
-
setErr('')
|
|
1435
|
-
} catch (e) { if (!dead) setErr(String(e?.message ?? e)) }
|
|
1436
|
-
}
|
|
1437
|
-
pull()
|
|
1438
|
-
const t = setInterval(pull, 3000)
|
|
1439
|
-
return () => { dead = true; clearInterval(t) }
|
|
1440
|
-
}, [])
|
|
1441
|
-
React.useEffect(() => {
|
|
1442
|
-
const el = listRef.current
|
|
1443
|
-
if (el && stick.current) el.scrollTop = el.scrollHeight
|
|
1444
|
-
}, [entries])
|
|
1445
|
-
const onScroll = () => {
|
|
1446
|
-
const el = listRef.current
|
|
1447
|
-
if (!el) return
|
|
1448
|
-
stick.current = el.scrollHeight - el.scrollTop - el.clientHeight < 28
|
|
1449
|
-
}
|
|
1450
|
-
const onStop = async () => {
|
|
1451
|
-
if (lock.current) return
|
|
1452
|
-
lock.current = true
|
|
1453
|
-
setBusy(true)
|
|
1454
|
-
try {
|
|
1455
|
-
const r = await jpost('/feiyu', { action: 'stop' })
|
|
1456
|
-
if (r && r.ok === false) toast('没停成:' + (r.error || r.hint || '未知原因'), 'warn')
|
|
1457
|
-
else toast(r && r.rounds != null ? `上岸啦~这趟一共游了 ${r.rounds} 轮。` : '它本来就没在刷。', 'good')
|
|
1458
|
-
} catch (e) { toast('没停成:' + String(e?.message ?? e), 'warn') }
|
|
1459
|
-
finally { lock.current = false; setBusy(false) }
|
|
1460
|
-
}
|
|
1461
|
-
return h('div', { 'data-bd-pet-card': '', 'data-wide': '' },
|
|
1462
|
-
h('div', { 'data-bd-pet-cardhead': '' },
|
|
1463
|
-
h('b', {}, '运行日志'),
|
|
1464
|
-
h('span', {}, '每 3 秒自动刷新'),
|
|
1465
|
-
h('button', {
|
|
1466
|
-
'data-bd-btn': '', 'data-sm': '', style: { marginLeft: '8px' }, disabled: busy, onClick: onStop,
|
|
1467
|
-
}, busy ? '停…' : '停止鲸鱼摸鱼')),
|
|
1468
|
-
err ? h('div', { 'data-bd-pet-pad': '' }, h('div', { 'data-bd-note': '' }, '读不到日志:' + err)) : null,
|
|
1469
|
-
h('div', { 'data-bd-pet-log': '', ref: listRef, onScroll },
|
|
1470
|
-
!entries
|
|
1471
|
-
? h('div', { 'data-bd-pet-logline': '' }, h('span', {}, '正在读…'))
|
|
1472
|
-
: entries.length === 0
|
|
1473
|
-
? h('div', { 'data-bd-pet-logline': '' }, h('span', {}, '还没动静 —— 开一次会话或跑一轮后台,这儿就会一条条记下来。'))
|
|
1474
|
-
: entries.map((e, i) => h('div', { 'data-bd-pet-logline': '', 'data-t': petEntryTone(e), key: i },
|
|
1475
|
-
h('time', {}, petFmtTime(e && e.at)), h('span', {}, petEntrySummary(e))))))
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
1387
|
/* 🐟 状态卡:字段全部来自 /status(feiyu 激活/轮次/今日挑选/本轮 token、bgBusy、loggedIn) */
|
|
1479
1388
|
function PetStatusCard({ st, fails }) {
|
|
1480
1389
|
const fy = (st && st.feiyu) || {}
|
|
@@ -1538,6 +1447,7 @@ window.__ModuleLoader__.load({
|
|
|
1538
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'],
|
|
1539
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'],
|
|
1540
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'],
|
|
1541
1451
|
film: ['M4 4h16v16H4zM8 4v16M16 4v16M4 8h4M4 16h4M16 8h4M16 16h4'],
|
|
1542
1452
|
chip: ['M7 7h10v10H7zM9 3v4M15 3v4M9 17v4M15 17v4M3 9h4M3 15h4M17 9h4M17 15h4'],
|
|
1543
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'],
|
|
@@ -1567,12 +1477,17 @@ window.__ModuleLoader__.load({
|
|
|
1567
1477
|
})
|
|
1568
1478
|
}
|
|
1569
1479
|
|
|
1570
|
-
function PetCompanionSettings({ st, busy, patch }) {
|
|
1480
|
+
function PetCompanionSettings({ st, busy, patch, say }) {
|
|
1571
1481
|
const cfg = st?.config || {}
|
|
1572
1482
|
const initial = () => ({ chatterFrequency: cfg.chatterFrequency || 'normal', notificationLevel: cfg.notificationLevel || 'normal',
|
|
1573
1483
|
focusMode: cfg.focusMode === true, quietHoursEnabled: cfg.quietHoursEnabled === true,
|
|
1574
1484
|
quietHoursStart: cfg.quietHoursStart || '23:00', quietHoursEnd: cfg.quietHoursEnd || '08:00', petReducedMotion: cfg.petReducedMotion === true })
|
|
1575
1485
|
const [draft, setDraft] = React.useState(initial)
|
|
1486
|
+
/* 📌 P3-39:挂载时的初始快照。保存只提交与快照**不同**的键 —— 没动过的键不再
|
|
1487
|
+
* 整包重发(以前每次都把七个键原样打回 /config,纯噪音写入);全没动就提示
|
|
1488
|
+
* 「没有改动」。 */
|
|
1489
|
+
const [baseline, setBaseline] = React.useState(initial)
|
|
1490
|
+
const tell = (text, tone) => { if (typeof say === 'function') say(String(text || ''), tone) }
|
|
1576
1491
|
const update = (key, value) => setDraft(old => ({ ...old, [key]: value }))
|
|
1577
1492
|
const toggle = (key, title, note) => ToggleRow({ label: title, note, on: draft[key], disabled: Boolean(busy), onChange: value => update(key, value) })
|
|
1578
1493
|
return h('div', { 'data-bd-companion-settings': '' },
|
|
@@ -1591,7 +1506,204 @@ window.__ModuleLoader__.load({
|
|
|
1591
1506
|
h('span', { 'data-bd-note': '' }, draft.quietHoursStart === draft.quietHoursEnd ? '起止时间相同,安静时段不生效。' : '按本地时间执行,支持跨午夜。')) : null,
|
|
1592
1507
|
toggle('petReducedMotion', '减少桌宠动效', '停用浮动、泡泡和事件动画,并遵循系统的减少动态效果设置。'),
|
|
1593
1508
|
h('div', { 'data-bd-row': '' }, h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, '当前:' + (st?.companion?.label || '自然陪伴')),
|
|
1594
|
-
h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy), onClick: () =>
|
|
1509
|
+
h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy), onClick: async () => {
|
|
1510
|
+
/* P3-39:只提交与初始快照不同的键,没有改动就不发请求 */
|
|
1511
|
+
const changed = Object.fromEntries(Object.entries(draft).filter(([key, value]) => value !== baseline[key]))
|
|
1512
|
+
if (!Object.keys(changed).length) { tell('陪伴节奏:没有改动'); return }
|
|
1513
|
+
if (await patch(changed, '陪伴节奏')) setBaseline({ ...draft })
|
|
1514
|
+
} }, '保存陪伴节奏')))
|
|
1515
|
+
}
|
|
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)
|
|
1595
1707
|
}
|
|
1596
1708
|
|
|
1597
1709
|
/* ⭐ 设置大弹窗(2026-10-26 桌宠化改造):从桌宠 ⚙️ 进来的居中弹窗。
|
|
@@ -1669,6 +1781,9 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1669
1781
|
const [learnError, setLearnError] = React.useState('')
|
|
1670
1782
|
const [learnBusy, setLearnBusy] = React.useState({})
|
|
1671
1783
|
const [learnRefresh, setLearnRefresh] = React.useState(0)
|
|
1784
|
+
const [learningWorkspace, setLearningWorkspace] = React.useState('learned')
|
|
1785
|
+
const learningWorkspaceRef = React.useRef(learningWorkspace)
|
|
1786
|
+
learningWorkspaceRef.current = learningWorkspace
|
|
1672
1787
|
const [mirrorNote, setMirrorNote] = React.useState('')
|
|
1673
1788
|
const [soulBusy, setSoulBusy] = React.useState('')
|
|
1674
1789
|
const [memoryQuery, setMemoryQuery] = React.useState('')
|
|
@@ -1713,7 +1828,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1713
1828
|
}, [])
|
|
1714
1829
|
|
|
1715
1830
|
const learnView = {
|
|
1716
|
-
state: learnState,
|
|
1831
|
+
state: learningWorkspace === 'learned' ? 'approved' : learnState,
|
|
1717
1832
|
kind: learnKind,
|
|
1718
1833
|
filter: learnFilter,
|
|
1719
1834
|
limit: 20,
|
|
@@ -1722,12 +1837,13 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1722
1837
|
learnViewRef.current = learnView
|
|
1723
1838
|
|
|
1724
1839
|
React.useEffect(() => {
|
|
1725
|
-
if (view !== 'learning') return
|
|
1840
|
+
if (view !== 'learning' || learningWorkspace === 'memory') return
|
|
1726
1841
|
let dead = false
|
|
1842
|
+
let missing = false
|
|
1727
1843
|
let timer = null
|
|
1728
1844
|
const queryView = { ...learnView }
|
|
1729
1845
|
const request = async () => {
|
|
1730
|
-
if (dead || !alive.current || learnInFlight.current.size || learnPulling.current) return
|
|
1846
|
+
if (dead || missing || !alive.current || learnInFlight.current.size || learnPulling.current) return
|
|
1731
1847
|
const seq = ++learnRequest.current
|
|
1732
1848
|
learnPulling.current = true
|
|
1733
1849
|
const query = '?state=' + encodeURIComponent(queryView.state)
|
|
@@ -1736,7 +1852,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1736
1852
|
+ '&limit=' + queryView.limit + '&offset=' + queryView.offset
|
|
1737
1853
|
try {
|
|
1738
1854
|
const r = await jget('/learn' + query)
|
|
1739
|
-
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 后重试')
|
|
1740
1856
|
const current = learnViewRef.current || {}
|
|
1741
1857
|
const same = current.state === queryView.state && current.kind === queryView.kind
|
|
1742
1858
|
&& current.filter === queryView.filter && Number(current.offset) === Number(queryView.offset)
|
|
@@ -1745,9 +1861,14 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1745
1861
|
setLearnError('')
|
|
1746
1862
|
}
|
|
1747
1863
|
} catch (e) {
|
|
1748
|
-
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
|
+
}
|
|
1749
1868
|
} finally {
|
|
1750
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)
|
|
1751
1872
|
}
|
|
1752
1873
|
}
|
|
1753
1874
|
request()
|
|
@@ -1757,10 +1878,10 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1757
1878
|
learnRequest.current += 1
|
|
1758
1879
|
if (timer) clearInterval(timer)
|
|
1759
1880
|
}
|
|
1760
|
-
}, [view, learnState, learnKind, learnFilter, learnOffset, learnRefresh])
|
|
1881
|
+
}, [view, learningWorkspace, learnState, learnKind, learnFilter, learnOffset, learnRefresh])
|
|
1761
1882
|
|
|
1762
1883
|
React.useEffect(() => {
|
|
1763
|
-
if (view !== 'learning') return
|
|
1884
|
+
if (view !== 'learning' || learningWorkspace !== 'memory') return
|
|
1764
1885
|
let dead = false
|
|
1765
1886
|
const seq = ++memoryRequest.current
|
|
1766
1887
|
const timer = setTimeout(async () => {
|
|
@@ -1771,7 +1892,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1771
1892
|
} catch (e) { if (!dead && alive.current && seq === memoryRequest.current) setMemoryError(String(e?.message || e)) }
|
|
1772
1893
|
}, memoryQuery ? 220 : 0)
|
|
1773
1894
|
return () => { dead = true; clearTimeout(timer); memoryRequest.current += 1 }
|
|
1774
|
-
}, [view, memorySource, memoryQuery, memoryOffset, memoryRefresh, st?.rev])
|
|
1895
|
+
}, [view, learningWorkspace, memorySource, memoryQuery, memoryOffset, memoryRefresh, st?.rev])
|
|
1775
1896
|
|
|
1776
1897
|
const icon = (name) => h(PetIcon, { name })
|
|
1777
1898
|
const sectionHead = (_name, title, extra) => h('div', { 'data-bd-h': '' }, h('b', {}, title), extra || null)
|
|
@@ -1943,6 +2064,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
1943
2064
|
const tasteTop = Array.isArray(tasteInfo.top) ? tasteInfo.top : []
|
|
1944
2065
|
const tasteLines = Array.isArray(tasteInfo.lines) ? tasteInfo.lines : []
|
|
1945
2066
|
const focusLearning = (state) => {
|
|
2067
|
+
setLearningWorkspace(state === 'approved' ? 'learned' : 'review')
|
|
1946
2068
|
setLearnState(state); setLearnFilter(''); setLearnKind(''); setLearnOffset(0); setLearnQ(null)
|
|
1947
2069
|
learnQueueRef.current?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' })
|
|
1948
2070
|
}
|
|
@@ -2010,6 +2132,16 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2010
2132
|
} }
|
|
2011
2133
|
finally { memoryActionLock.current = false; if (alive.current) setMemoryWorking('') }
|
|
2012
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
|
+
}
|
|
2013
2145
|
const memoryLabels = { all: '全部记忆', learning: '学习经验', soul: '心情记录', voice: '语气札记', taste: '口味样本', seen: '浏览历史', history: '版本与审计' }
|
|
2014
2146
|
const memoryItems = Array.isArray(memoryData?.items) ? memoryData.items : []
|
|
2015
2147
|
const memoryExplorerCard = h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-memory-explorer': '' },
|
|
@@ -2031,6 +2163,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2031
2163
|
row.label ? h('strong', { 'data-bd-label': '', 'data-bd-grow': '' }, row.label) : null,
|
|
2032
2164
|
h('time', { 'data-bd-note': '' }, formatTime(row.at) || '时间未记录')),
|
|
2033
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,
|
|
2034
2168
|
row.from ? h('div', { 'data-bd-note': '' }, '来源:' + row.from) : null,
|
|
2035
2169
|
row.why ? h('div', { 'data-bd-note': '' }, '决定理由:' + row.why) : null,
|
|
2036
2170
|
row.state ? h('span', { 'data-bd-note': '' }, '当前状态:' + ({ pending: '待审', approved: '已批准', rejected: '已拒绝' }[row.state] || row.state)) : null,
|
|
@@ -2039,9 +2173,9 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2039
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)
|
|
2040
2174
|
: h('span', { 'data-bd-note': '' }, '只读历史记录')))),
|
|
2041
2175
|
h('div', { 'data-bd-row': '' },
|
|
2042
|
-
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 条` : '按需读取,离页停止'),
|
|
2043
2177
|
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: memoryOffset <= 0 || Boolean(memoryWorking), onClick: () => { setMemoryOffset(Math.max(0, memoryOffset - 20)); setMemoryData(null) } }, '前 20 条'),
|
|
2044
|
-
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 条'),
|
|
2045
2179
|
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(memoryWorking), onClick: () => setMemoryRefresh((n) => n + 1) }, '重新读取记忆')))
|
|
2046
2180
|
const q = learnQ && sameLearnView(learnView) ? learnQ : null
|
|
2047
2181
|
const items = q && Array.isArray(q.items) ? q.items : []
|
|
@@ -2068,10 +2202,11 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2068
2202
|
one.decidedAt ? h('span', { 'data-bd-note': '' }, '决定于 ' + formatTime(one.decidedAt)) : null,
|
|
2069
2203
|
learnBusy[one.id] ? h('span', { 'data-bd-note': '', role: 'status' }, '处理中…') : null),
|
|
2070
2204
|
h('div', { 'data-bd-label': '' }, String(one.text || '')),
|
|
2205
|
+
learningUseNote(one),
|
|
2071
2206
|
one.example ? h('blockquote', { 'data-bd-example': '' },
|
|
2072
|
-
h('span', { 'data-bd-note': '' }, '
|
|
2073
|
-
one.from ? h('div', { 'data-bd-note': '', 'data-bd-source': '' },
|
|
2074
|
-
'来源:' + 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(';')),
|
|
2075
2210
|
...links.map((bvid) => h('a', { key: bvid, href: 'https://www.bilibili.com/video/' + bvid, target: '_blank', rel: 'noopener noreferrer' }, '打开 ' + bvid))) : null,
|
|
2076
2211
|
one.why ? h('div', { 'data-bd-note': '' }, '决定理由:' + String(one.why)) : null,
|
|
2077
2212
|
h('div', { 'data-bd-note': '' }, `审计:记录于 ${formatTime(one.at) || '未知'} · 版本 ${Number(one.version) || 1}`),
|
|
@@ -2080,9 +2215,10 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2080
2215
|
canReject ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: pending, onClick: () => decideLearn(one.id, 'reject') }, icon('dots'), one.state === 'approved' ? '改为拒绝' : '拒绝') : null,
|
|
2081
2216
|
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: pending, onClick: () => decideLearn(one.id, 'forget') }, icon('gear'), '移除记录')))
|
|
2082
2217
|
}) : null
|
|
2083
|
-
const learnCard = h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-learning-queue': '', ref: learnQueueRef },
|
|
2084
|
-
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' ? '已学会 · 已批准经验' : '学习审核与历史'),
|
|
2085
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,
|
|
2086
2222
|
/* 判重账本概览(2026-10-27 从旧设置页搬进来):「刷过不再刷、回过不再回」记了多少得有地方看 */
|
|
2087
2223
|
(() => {
|
|
2088
2224
|
const sn = st && st.seen
|
|
@@ -2091,32 +2227,35 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2091
2227
|
`判重账本:已浏览 ${Number(sn.videos) || 0} 个视频(今日新增 ${Number(sn.videosToday) || 0} 个)· `
|
|
2092
2228
|
+ `已回复 ${Number(sn.replied) || 0} 条评论`)
|
|
2093
2229
|
})(),
|
|
2094
|
-
h('div', { 'data-bd-note': '' }, '
|
|
2230
|
+
h('div', { 'data-bd-note': '' }, '已批准经验按当前情境选入后续提示词。适用情境和实践记录帮助她下次用上;实践为角色自报,不代表效果已核实。移除当前记录后仍保留审计。'),
|
|
2095
2231
|
h('div', { 'data-bd-row': '' },
|
|
2096
|
-
...
|
|
2232
|
+
...(learningWorkspace === 'review' ? [['pending', '待审'], ['rejected', '已拒绝'], ['all', '全部']] : []).map(([value, label]) => h('button', {
|
|
2097
2233
|
key: value, 'data-bd-btn': '', 'data-sm': '', 'data-primary': learnState === value ? '' : undefined,
|
|
2098
2234
|
onClick: () => { setLearnState(value); setLearnOffset(0); setLearnQ(null) },
|
|
2099
2235
|
}, label)),
|
|
2100
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) } },
|
|
2101
2237
|
h('option', { value: '' }, '全部类别'), ...kinds.map((kind) => h('option', { key: kind, value: kind }, labels[kind] || kind)))),
|
|
2102
2238
|
h('input', {
|
|
2103
|
-
'data-bd-input': '', value: learnFilter, maxLength: 160, placeholder: '
|
|
2239
|
+
'data-bd-input': '', value: learnFilter, maxLength: 160, placeholder: '按正文、适用场景、例句、来源或决定理由筛选',
|
|
2104
2240
|
'aria-label': '筛选学习记录', onChange: (e) => { setLearnFilter(e.target.value); setLearnOffset(0); setLearnQ(null) },
|
|
2105
2241
|
}),
|
|
2106
|
-
learnError ? h('div', { 'data-bd-msg': '', 'data-warn': '' }, '学习记录读取失败:' + learnError) : null,
|
|
2107
|
-
!q ? h('div', { 'data-bd-list-empty': '', role: 'status' }, '正在读取学习记录…')
|
|
2108
|
-
: !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' ? '暂无已批准经验。待审内容通过审核后会出现在这里。' : '该状态暂无学习记录;已批准的内容可在“已学会”查看。')
|
|
2109
2246
|
: h('div', { 'data-bd-learn-list': '', tabIndex: 0, 'aria-label': '学习记录列表' }, ...learnRows),
|
|
2110
|
-
|
|
2111
|
-
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} 条` : '按需读取,切到记忆检索后停止刷新'),
|
|
2112
2249
|
h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: learnOffset <= 0 || Boolean(busy), onClick: () => { setLearnOffset(Math.max(0, learnOffset - 20)); setLearnQ(null) } }, '上一页'),
|
|
2113
|
-
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) }, '重新读取学习'))
|
|
2114
2252
|
)
|
|
2115
2253
|
|
|
2116
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)
|
|
2117
2257
|
const reviewModels = Array.isArray(st && st.models) ? st.models : []
|
|
2118
|
-
const reviewCard = h('
|
|
2119
|
-
sectionHead('pulse', '后台自动审核'),
|
|
2258
|
+
const reviewCard = h('div', { 'data-bd-review-controls': '' },
|
|
2120
2259
|
h('div', { 'data-bd-row': '', 'data-bd-toggle': '' },
|
|
2121
2260
|
h('div', { 'data-bd-grow': '' },
|
|
2122
2261
|
h('div', { 'data-bd-label': '' }, '自动审核待审内容'),
|
|
@@ -2155,16 +2294,15 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2155
2294
|
try { const r = check(await jpost('/learn/review', { action: 'stop' }), '停止模型审核失败'); if (r.learnReview) setSt((old) => ({ ...old, learnReview: r.learnReview })); safeSay('模型审核已请求停止。', 'good') }
|
|
2156
2295
|
catch (e) { safeSay(String(e && e.message || e), 'warn') } finally { setBusy('') }
|
|
2157
2296
|
} }, '停止审核') : null),
|
|
2158
|
-
h('div', { 'data-bd-row': '' },
|
|
2297
|
+
hasReviewBatch ? h('div', { 'data-bd-row': '', 'data-bd-review-receipt': '' },
|
|
2159
2298
|
h('span', { 'data-bd-badge': '', 'data-live': reviewStatus.running ? '' : undefined }, reviewStatus.running ? '审核中' : reviewStatus.complete ? '本批完成' : reviewStatus.lastError ? '本批未完成' : '未运行'),
|
|
2160
|
-
h('span', { 'data-bd-badge': '' }, '全部待审 ' + Number(reviewStatus.pending ?? learnInfo.pending ?? 0)),
|
|
2161
2299
|
h('span', { 'data-bd-badge': '' }, '本批批准 ' + Number(reviewStatus.approved || 0)),
|
|
2162
2300
|
h('span', { 'data-bd-badge': '' }, '本批拒绝 ' + Number(reviewStatus.rejected || 0)),
|
|
2163
|
-
reviewStatus.remaining != null ? h('span', { 'data-bd-badge': '' }, '本批剩余 ' + Number(reviewStatus.remaining)) : null),
|
|
2164
|
-
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,
|
|
2165
2303
|
reviewStatus.lastError ? h('div', { 'data-bd-msg': '', 'data-warn': '' }, '审核提示:' + reviewStatus.lastError) : null,
|
|
2166
2304
|
!reviewStatus.running && Number(reviewStatus.nextRetryAt) > 0 ? h('div', { 'data-bd-note': '' }, '自动重试最早:' + formatTime(reviewStatus.nextRetryAt) + (reviewStatus.enabled ? '' : '(自动审核未开启)')) : null,
|
|
2167
|
-
h('div', { 'data-bd-note': '' }, reviewStatus.lastFinishedAt ? '上次结束:' + formatTime(reviewStatus.lastFinishedAt) : '
|
|
2305
|
+
h('div', { 'data-bd-note': '' }, reviewStatus.lastFinishedAt ? '上次结束:' + formatTime(reviewStatus.lastFinishedAt) : '后台自动审核:开启后处理待审队列,也可手动批量审核。'))
|
|
2168
2306
|
|
|
2169
2307
|
/* 🔄 更新通道(2026-10-30 使用者「加一个更新通道,方便及时更新」)
|
|
2170
2308
|
* 只在这里查/升级;普通鱼工具拿不到升级能力(换版本要重启宿主,不能让会话模型自己决定)。
|
|
@@ -2183,7 +2321,9 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2183
2321
|
setUpdateError(r.error || '')
|
|
2184
2322
|
} catch (e) { if (updateAlive.current) setUpdateError(String(e && e.message || e)) }
|
|
2185
2323
|
}
|
|
2186
|
-
|
|
2324
|
+
/* ⭐ P3-36:updateCard 只在「账号与环境」页渲染(learning 页不挂这张卡),
|
|
2325
|
+
预取也只在该页跑 —— learning 页挂载时白跑一次 /update 徒增一次接口往返。 */
|
|
2326
|
+
React.useEffect(() => { if (view !== 'learning') pullUpdate() }, [view])
|
|
2187
2327
|
const updateCard = (() => {
|
|
2188
2328
|
const u = updateState || {}
|
|
2189
2329
|
const install = u.install || {}
|
|
@@ -2299,8 +2439,12 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2299
2439
|
return h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': view === 'learning' ? 'learning' : 'other' },
|
|
2300
2440
|
h('div', { 'data-bd-pghead': '' }, h('div', {}, h('b', {}, view === 'learning' ? '学习与记忆' : '账号与环境'),
|
|
2301
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,
|
|
2302
2446
|
h('div', { 'data-bd-pet-grid': '', 'data-bd-other': '' },
|
|
2303
|
-
...(view === 'learning' ? [memoryOverviewCard, memoryExplorerCard,
|
|
2447
|
+
...(view === 'learning' ? learningWorkspace !== 'memory' ? [learnCard()] : [memoryOverviewCard, memoryExplorerCard, soulCard] : [accountCard, updateCard, asrCard, mirrorCard, groundCard])))
|
|
2304
2448
|
}
|
|
2305
2449
|
|
|
2306
2450
|
|
|
@@ -2619,14 +2763,14 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2619
2763
|
h('button', { 'data-bd-btn': '', 'data-primary': '', type: 'submit', disabled }, working === 'collect' ? '正在收集…' : '加入表情库'))))
|
|
2620
2764
|
}
|
|
2621
2765
|
|
|
2622
|
-
function PetSettingsModal({ onClose, toast, ctx, status, initialTab = 'run' }) {
|
|
2766
|
+
function PetSettingsModal({ onClose, toast, ctx, status, initialTab = 'run', initialRunMode = 'session' }) {
|
|
2623
2767
|
const [tab, setTab] = React.useState(initialTab)
|
|
2624
2768
|
const activeGroup = ({ stickers: 'learning', ledger: 'logs', review: 'logs', other: 'basic' })[tab] || tab
|
|
2625
2769
|
React.useEffect(() => {
|
|
2626
2770
|
const body = modalRef.current?.querySelector('[data-bd-pet-body]')
|
|
2627
2771
|
if (body) body.scrollTop = 0
|
|
2628
2772
|
}, [tab])
|
|
2629
|
-
const [runMode, setRunMode] = React.useState('session')
|
|
2773
|
+
const [runMode, setRunMode] = React.useState(initialRunMode === 'background' ? 'background' : 'session')
|
|
2630
2774
|
const [logMode, setLogMode] = React.useState('events')
|
|
2631
2775
|
const [runDrafts, setRunDrafts] = React.useState({})
|
|
2632
2776
|
const runLock = React.useRef(false)
|
|
@@ -2640,7 +2784,6 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2640
2784
|
const [msg, setMsg] = React.useState('')
|
|
2641
2785
|
const [msgTone, setMsgTone] = React.useState('')
|
|
2642
2786
|
/* 输入一律绑本地草稿:5 秒轮询会把正在打的字冲掉 */
|
|
2643
|
-
const [kws, setKws] = React.useState(null)
|
|
2644
2787
|
const [wsDir, setWsDir] = React.useState(null) // 跟 uiWorkspace 服务的名字区分开
|
|
2645
2788
|
const [rl, setRl] = React.useState(null)
|
|
2646
2789
|
const [perDay, setPerDay] = React.useState(null)
|
|
@@ -2692,6 +2835,9 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2692
2835
|
单独 2.5 秒增量轮询,跟 5 秒的 /status 分开,不然那条流会一顿一顿的。 */
|
|
2693
2836
|
const [acts, setActs] = React.useState([])
|
|
2694
2837
|
const [actLive, setActLive] = React.useState(null)
|
|
2838
|
+
/* ⚠️ P3-37:/activity 404(宿主还没加载这版接口)不是抖动 —— 停轮询 + 出提示条,
|
|
2839
|
+
不然每 2.5s 打一次必 404 的接口,网络面板和日志里全是噪音。 */
|
|
2840
|
+
const [actRouteMissing, setActRouteMissing] = React.useState(false)
|
|
2695
2841
|
const pvRunning = Boolean(pv && pv.running)
|
|
2696
2842
|
React.useEffect(() => {
|
|
2697
2843
|
if (tab !== 'video' || pvRouteMissing.current) return
|
|
@@ -2732,6 +2878,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2732
2878
|
React.useEffect(() => {
|
|
2733
2879
|
if (tab !== 'logs') return
|
|
2734
2880
|
let dead = false
|
|
2881
|
+
let timer = null
|
|
2735
2882
|
let sinceUid = activityCursor.current
|
|
2736
2883
|
const pull = async () => {
|
|
2737
2884
|
if (dead || activityRequest.current) return
|
|
@@ -2760,12 +2907,20 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2760
2907
|
return next.length > 200 ? next.slice(next.length - 200) : next
|
|
2761
2908
|
})
|
|
2762
2909
|
}
|
|
2763
|
-
} catch {
|
|
2910
|
+
} catch (e) {
|
|
2911
|
+
/* ⚠️ P3-37:404 不是抖动 —— 接口压根不存在(宿主较旧 / 没热加载),
|
|
2912
|
+
停掉这条轮询并提示用户重启;超时等其他失败仍按「抖一下」处理。 */
|
|
2913
|
+
if (e?.status === 404) {
|
|
2914
|
+
if (!dead) setActRouteMissing(true)
|
|
2915
|
+
if (timer) { clearInterval(timer); timer = null }
|
|
2916
|
+
return
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2764
2919
|
finally { if (activityRequest.current === token) activityRequest.current = null }
|
|
2765
2920
|
}
|
|
2766
2921
|
pull()
|
|
2767
|
-
|
|
2768
|
-
return () => { dead = true; activityEpoch.current += 1; activityRequest.current = null; clearInterval(
|
|
2922
|
+
timer = setInterval(pull, 2500)
|
|
2923
|
+
return () => { dead = true; activityEpoch.current += 1; activityRequest.current = null; if (timer) clearInterval(timer) }
|
|
2769
2924
|
}, [tab])
|
|
2770
2925
|
|
|
2771
2926
|
const onClearAct = async () => {
|
|
@@ -2823,7 +2978,6 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2823
2978
|
}
|
|
2824
2979
|
React.useEffect(() => {
|
|
2825
2980
|
if (!cfg) return
|
|
2826
|
-
if (kws === null) setKws((cfg.keywords || []).join('\n'))
|
|
2827
2981
|
if (wsDir === null) setWsDir(cfg.workspace || '')
|
|
2828
2982
|
if (rl === null) setRl(cfg.reportLevel || 'full')
|
|
2829
2983
|
if (perDay === null) setPerDay(String(cfg.perDay ?? 5))
|
|
@@ -2852,6 +3006,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2852
3006
|
setBusy(JSON.stringify(p))
|
|
2853
3007
|
try {
|
|
2854
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('未收到人格保存回执,请重新读取设置后重试。')
|
|
2855
3011
|
if (r?.config) setSt((old) => ({ ...old, config: r.config }))
|
|
2856
3012
|
const clamped = Array.isArray(r?.clamped) ? r.clamped : []
|
|
2857
3013
|
const clampTxt = clamped.map((c) => `${c.key} 想要 ${c.want},最多只能 ${c.got}`).join(';')
|
|
@@ -2873,6 +3029,9 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2873
3029
|
} catch (e) { say(String(e?.message ?? e), 'warn'); return false }
|
|
2874
3030
|
finally { setBusy('') }
|
|
2875
3031
|
}
|
|
3032
|
+
/* 🔐 P2-15:patch 的单飞包装(见 useLatestPatch 注释)。时间/下拉这类没有 disabled
|
|
3033
|
+
* 挡板的控件连改两下时会并发 PATCH,走它串行化。 */
|
|
3034
|
+
const patchLatest = useLatestPatch(patch)
|
|
2876
3035
|
const sw = (k) => Boolean(cfg && cfg[k])
|
|
2877
3036
|
|
|
2878
3037
|
const onLoginStart = async () => {
|
|
@@ -3111,7 +3270,9 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3111
3270
|
const saveModel = async (value) => {
|
|
3112
3271
|
const previous = currentModel
|
|
3113
3272
|
setVideoModelId(value)
|
|
3114
|
-
|
|
3273
|
+
/* 🔐 P2-15:走单飞 —— 在飞期间再改,新值只记 pending(本地 select 已更新、
|
|
3274
|
+
* 不回弹),当前请求结束后补发;直接发送失败才回弹旧值。 */
|
|
3275
|
+
if (!(await patchLatest({ videoModelId: value }, '制作模型'))) setVideoModelId(previous)
|
|
3115
3276
|
}
|
|
3116
3277
|
const selectedSession = selected && (selected.sessionId || sessions.find((item) => item.productionId === selected.id)?.sessionId)
|
|
3117
3278
|
return h('div', { 'data-bd-pet-grid': '', 'data-bd-video-studio': '' },
|
|
@@ -3204,12 +3365,13 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3204
3365
|
const value = e.target.value
|
|
3205
3366
|
if (!/^\d{2}:\d{2}$/.test(value)) return
|
|
3206
3367
|
setPv((old) => ({ ...(old || {}), at: value }))
|
|
3207
|
-
|
|
3368
|
+
/* 🔐 P2-15:单飞防并发 PATCH 乱序回写(见 useLatestPatch) */
|
|
3369
|
+
patchLatest({ pvAt: value }, '自动做视频的时间')
|
|
3208
3370
|
} }))),
|
|
3209
3371
|
h('section', { 'data-bd-sec': '', 'data-bd-span': '2' },
|
|
3210
3372
|
heading('film', '配音与本地环境'),
|
|
3211
3373
|
h('label', { 'data-bd-row': '' }, h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '配音音色'),
|
|
3212
|
-
h('select', { 'data-bd-input': '', style: { width: '230px' }, value: pv?.voice || 'zh-CN-XiaoyiNeural', onChange: (e) =>
|
|
3374
|
+
h('select', { 'data-bd-input': '', style: { width: '230px' }, value: pv?.voice || 'zh-CN-XiaoyiNeural', onChange: (e) => patchLatest({ pvVoice: e.target.value }, '配音音色') },
|
|
3213
3375
|
h('option', { value: 'zh-CN-XiaoyiNeural' }, '晓伊(默认 · 偏软)'), h('option', { value: 'zh-CN-XiaoxiaoNeural' }, '晓晓(更清亮)'), h('option', { value: 'zh-CN-XiaochenNeural' }, '晓辰(偏成熟)'))),
|
|
3214
3376
|
...[
|
|
3215
3377
|
['GPT-SoVITS 目录', 'gsvRepo', 'pvGsvRepo', 'D:\\...\\GPT-SoVITS'],
|
|
@@ -3313,26 +3475,26 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3313
3475
|
}
|
|
3314
3476
|
} catch (e) { say(String(e?.message ?? e), 'warn') }
|
|
3315
3477
|
}
|
|
3316
|
-
const startRun = async (mode) => {
|
|
3317
|
-
if (runLock.current) return
|
|
3318
|
-
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 }
|
|
3319
3481
|
let options
|
|
3320
|
-
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 }
|
|
3321
3483
|
runLock.current = true
|
|
3322
3484
|
setBusy('start-' + mode)
|
|
3323
3485
|
say('')
|
|
3486
|
+
let sessionId
|
|
3324
3487
|
try {
|
|
3325
|
-
let sessionId
|
|
3326
3488
|
if (mode === 'session') {
|
|
3327
3489
|
const created = await jpost('/pet/session-new', newSessionContext(ctx))
|
|
3328
|
-
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 })
|
|
3329
3491
|
sessionId = created.sessionId
|
|
3330
3492
|
}
|
|
3331
3493
|
/* ⭐ 时段不走请求体:host 从 config 里读常设时段,手动开始只负责"现在就跑" */
|
|
3332
3494
|
const body = mode === 'session'
|
|
3333
3495
|
? { mode, model: options.model, tokenBudget: options.tokenBudget, durationMinutes: options.durationMinutes, maxRounds: options.maxRounds }
|
|
3334
3496
|
: { mode, model: options.model, tokenBudget: options.tokenBudget, durationMinutes: options.durationMinutes }
|
|
3335
|
-
const r = await jpost('/pet/start', { ...body, ...(sessionId ? { sessionId } : {}) })
|
|
3497
|
+
const r = await jpost('/pet/start', { ...body, ...(sessionId ? { sessionId } : {}), ...(mode === 'session' && curiosityId ? { curiosityId } : {}) })
|
|
3336
3498
|
if (!r?.ok || r.started === false) throw new Error((r?.error || r?.hint || '没有启动') + (r?.error && r?.hint ? ';' + r.hint : ''))
|
|
3337
3499
|
if (sessionId) {
|
|
3338
3500
|
try {
|
|
@@ -3342,7 +3504,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3342
3504
|
say('会话任务已启动。', 'good')
|
|
3343
3505
|
} else say('后台任务已启动。', 'good')
|
|
3344
3506
|
try { const fresh = await jget('/status'); if (fresh?.ok) setSt(fresh) } catch { /* 下次轮询补上 */ }
|
|
3345
|
-
|
|
3507
|
+
return true
|
|
3508
|
+
} catch (e) { const retainedSessionId = sessionId || e?.sessionId; say(String(e?.message ?? e) + (retainedSessionId ? ';已创建的会话仍保留:' + retainedSessionId : ''), 'warn'); return false }
|
|
3346
3509
|
finally { runLock.current = false; setBusy('') }
|
|
3347
3510
|
}
|
|
3348
3511
|
const controlRun = async (action) => {
|
|
@@ -3368,12 +3531,36 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3368
3531
|
models.map((m) => h('option', { key: m.id, value: m.id }, m.label || m.id)))
|
|
3369
3532
|
: h('input', { ...props, placeholder: 'provider/model;留空使用默认模型' })
|
|
3370
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
|
+
}
|
|
3371
3556
|
const runPage = (mode) => {
|
|
3372
3557
|
const session = mode === 'session'
|
|
3373
3558
|
const options = runSettings(mode)
|
|
3374
3559
|
const fy = st.feiyu || {}
|
|
3375
|
-
const
|
|
3560
|
+
const state = runState(mode)
|
|
3561
|
+
const live = session ? Boolean(fy.active || state.waiting) : Boolean(st.bgBusy || st.backgroundRun?.continuationPending)
|
|
3376
3562
|
const tokens = fy.tokens || {}
|
|
3563
|
+
const windowDirty = !session && (String(options.windowStart || '') !== String(cfg.backgroundWindowStart || '') || String(options.windowEnd || '') !== String(cfg.backgroundWindowEnd || ''))
|
|
3377
3564
|
return h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': mode },
|
|
3378
3565
|
h('div', { 'data-bd-pet-grid': '' },
|
|
3379
3566
|
h('section', { 'data-bd-sec': '' }, heading('sliders', '运行参数'),
|
|
@@ -3389,26 +3576,31 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3389
3576
|
h('label', { 'data-bd-field': '', 'data-bd-grow': '' }, h('span', { 'data-bd-label': '' }, '结束(可跨午夜)'),
|
|
3390
3577
|
h('input', { 'data-bd-input': '', type: 'time', value: options.windowEnd || '', onChange: (e) => updateRun(mode, 'windowEnd', e.target.value) }))),
|
|
3391
3578
|
] : []),
|
|
3579
|
+
windowDirty ? h('div', { 'data-bd-msg': '', 'data-warn': '', 'data-bd-window-draft': '', role: 'status' }, '连续时段有未保存草稿。请先「保存默认设置」让新时段生效;直接开始后台刷仍使用已保存的时段。') : null,
|
|
3392
3580
|
h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, session ? '最多运行几分钟(0 = 不限时)' : '没填时段时,一轮刷几分钟'),
|
|
3393
3581
|
h('input', { 'data-bd-input': '', type: 'number', min: session ? 0 : 1, max: session ? 1440 : 180, step: 1,
|
|
3394
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', {},
|
|
3395
3586
|
session ? h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '最多刷多少轮'),
|
|
3396
3587
|
h('input', { 'data-bd-input': '', type: 'number', min: 1, max: numLim('maxRounds', 1, 1000)[1], step: 1,
|
|
3397
3588
|
value: options.maxRounds, onChange: (e) => updateRun(mode, 'maxRounds', e.target.value) })) : null,
|
|
3398
3589
|
h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '这趟 token 预算(0 = 不限)'),
|
|
3399
3590
|
h('input', { 'data-bd-input': '', type: 'number', min: 0, max: 5000000, step: 1000,
|
|
3400
|
-
value: options.tokenBudget, onChange: (e) => updateRun(mode, 'tokenBudget', e.target.value) })),
|
|
3591
|
+
value: options.tokenBudget, onChange: (e) => updateRun(mode, 'tokenBudget', e.target.value) })))),
|
|
3401
3592
|
h('div', { 'data-bd-note': '' }, session
|
|
3402
3593
|
? '轮数与 token 预算在每轮结束后检查;到达时限后暂停续轮。会话内的新消息按宿主中断规则处理。'
|
|
3403
3594
|
: (String(options.windowStart || '') && String(options.windowEnd || '')
|
|
3404
|
-
?
|
|
3595
|
+
? `${windowDirty ? '时段草稿(未生效)' : '已保存时段'}:${options.windowStart}–${options.windowEnd}。保存后的时段内会持续接力至结束;重新打开 DSH 后仍按已保存时段调度。手动「停止」后本时段不再自动拉起。token 预算是软目标,不能精确截断。`
|
|
3405
3596
|
: '后台时段为空时,按单次运行时长执行;提前结束会在剩余时长内续接。后台 token 预算为软目标,不能按精确用量自动截断。')),
|
|
3406
3597
|
h('div', { 'data-bd-row': '' },
|
|
3407
3598
|
h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => saveRun(mode) }, '保存默认设置')),
|
|
3408
3599
|
h('div', { 'data-bd-note': '' }, '两种方式分别保存。模型、时长与预算用于下一次启动;后台时段保存后生效。运行中任务不受草稿影响。')),
|
|
3409
|
-
h('section', { 'data-bd-sec': '' }, heading('pulse', '运行状态'),
|
|
3600
|
+
h('section', { 'data-bd-sec': '', 'data-bd-run-state': mode }, heading('pulse', '运行状态'),
|
|
3410
3601
|
h('span', { 'data-bd-badge': '', 'data-live': live ? '' : undefined },
|
|
3411
|
-
|
|
3602
|
+
state.label),
|
|
3603
|
+
h('div', { 'data-bd-note': '', 'data-bd-run-state-note': '', role: state.warning ? 'alert' : 'status' }, state.note),
|
|
3412
3604
|
session ? h('div', { 'data-bd-readout': '' },
|
|
3413
3605
|
h('div', {}, h('b', {}, fy.goal ? (fy.goal.rounds || 0) + ' / ' + (fy.goal.maxRounds || 0) : fy.source === 'manual' ? '手动刷站' : '—'), h('span', {}, '已刷轮数')),
|
|
3414
3606
|
h('div', {}, h('b', {}, tokens.thisRun === null || tokens.thisRun === undefined || tokens.tokensUnknown ? '未计量' : Number(tokens.thisRun).toLocaleString()), h('span', {}, '本趟 token')),
|
|
@@ -3436,10 +3628,16 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3436
3628
|
catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') }
|
|
3437
3629
|
} }, '停止后台任务') : null,
|
|
3438
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
|
+
})(),
|
|
3439
3637
|
h('div', { 'data-bd-note': '' }, '执行明细保存在「记录与用量」中。'),
|
|
3440
3638
|
/* ⏸️ 免跑牌期间如实说(st.bgWindow.holdUntil 由宿主 /status 提供) */
|
|
3441
3639
|
!session && st.bgWindow && Number(st.bgWindow.holdUntil) > Date.now()
|
|
3442
|
-
? h('div', { 'data-bd-note': '' },
|
|
3640
|
+
? h('div', { 'data-bd-note': '' }, st.bgWindow.holdReason === 'rest' ? '自然休息期间不会接力;休息结束后重新检查时段。' : '暂停期间不自动拉起,手动开始可恢复。')
|
|
3443
3641
|
: null)))
|
|
3444
3642
|
}
|
|
3445
3643
|
|
|
@@ -3464,16 +3662,15 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3464
3662
|
})),
|
|
3465
3663
|
!scheduleAllDay ? h('div', { 'data-bd-row' : '' },
|
|
3466
3664
|
h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '开始时间'),
|
|
3467
|
-
h('input', { 'data-bd-input': '', type: 'time', value: scheduleStart || '18:00', onChange: (e) => { setScheduleStart(e.target.value);
|
|
3665
|
+
h('input', { 'data-bd-input': '', type: 'time', value: scheduleStart || '18:00', onChange: (e) => { setScheduleStart(e.target.value); patchLatest({ scheduleStart: e.target.value }, '定时开始时间') } }),
|
|
3468
3666
|
h('span', { 'data-bd-label': '' }, '结束时间'),
|
|
3469
|
-
h('input', { 'data-bd-input': '', type: 'time', value: scheduleEnd || '09:00', onChange: (e) => { setScheduleEnd(e.target.value);
|
|
3667
|
+
h('input', { 'data-bd-input': '', type: 'time', value: scheduleEnd || '09:00', onChange: (e) => { setScheduleEnd(e.target.value); patchLatest({ scheduleEnd: e.target.value }, '定时结束时间') } })) : null,
|
|
3470
3668
|
h('div', { 'data-bd-row' : '' },
|
|
3471
3669
|
h('span', { 'data-bd-badge': '', 'data-live': st.schedule && st.schedule.inWindow ? '' : undefined }, st.schedule && st.schedule.inWindow ? '当前在窗口内' : '当前不在窗口内'),
|
|
3472
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) : '尚未启动'),
|
|
3473
3671
|
st.schedule && st.schedule.nextRunAt ? h('span', { 'data-bd-note': '' }, '下次检查:' + petFmtTime(st.schedule.nextRunAt)) : null)) : null)
|
|
3474
3672
|
const basicCards = [
|
|
3475
|
-
() => h('div', { 'data-bd-
|
|
3476
|
-
heading('spark', '桌宠'),
|
|
3673
|
+
() => h('div', { 'data-bd-pet-preferences': '' },
|
|
3477
3674
|
ToggleRow({
|
|
3478
3675
|
label: '显示桌宠', on: !(st && st.petShow === false), disabled: Boolean(busy),
|
|
3479
3676
|
note: st && st.petShow === false
|
|
@@ -3485,10 +3682,10 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3485
3682
|
label: '闲聊提示', on: !(st && st.config && st.config.idleChatter === false), disabled: Boolean(busy),
|
|
3486
3683
|
note: '按已配置频率显示闲聊,频率由陪伴节奏控制。',
|
|
3487
3684
|
onChange: (v) => patch({ idleChatter: v }, '闲聊泡泡'),
|
|
3488
|
-
}), h(PetCompanionSettings, { st, busy, patch })),
|
|
3489
|
-
() => h('div', { 'data-bd-sec': '' },
|
|
3490
|
-
heading('spark', '
|
|
3491
|
-
h('div', { 'data-bd-note': '' }, '
|
|
3685
|
+
}), h(PetCompanionSettings, { st, busy, patch, say })),
|
|
3686
|
+
() => h('div', { 'data-bd-sec': '', 'data-bd-basic-card': 'character' },
|
|
3687
|
+
heading('spark', '角色与陪伴'),
|
|
3688
|
+
h('div', { 'data-bd-note': '' }, '选择她陪你冲浪的方式,仅在鲸鱼摸鱼模式中生效。'),
|
|
3492
3689
|
h('div', { 'data-bd-field': '' },
|
|
3493
3690
|
h('span', { 'data-bd-label': '' }, '陪伴档位'),
|
|
3494
3691
|
h('div', { 'data-bd-segments': '', role: 'group', 'aria-label': '陪伴档位' },
|
|
@@ -3500,24 +3697,16 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3500
3697
|
h('div', { 'data-bd-row': '' },
|
|
3501
3698
|
h('span', { 'data-bd-grow': '' }),
|
|
3502
3699
|
h('button', { 'data-bd-btn': '', 'data-sm': '', 'data-primary': '', disabled: Boolean(busy), onClick: () => patch({ interactionStyle: interactionStyle || 'companion' }, '陪伴档位') }, '保存档位')),
|
|
3503
|
-
h('
|
|
3504
|
-
'
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
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': '' }, '只限制留片与报告收录的精选数量,不限制继续浏览或独立互动,也不是每天要完成的数量。'),
|
|
3508
3706
|
h('div', { 'data-bd-row': '' },
|
|
3509
|
-
h('span', { 'data-bd-
|
|
3510
|
-
persona && persona.length > 400 ? `已写 ${persona.length} 字(每轮都占一份 token)` : ''),
|
|
3511
|
-
h('button', {
|
|
3512
|
-
'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy),
|
|
3513
|
-
onClick: () => patch({ persona: String(persona || '').trim() }, '人格设定'),
|
|
3514
|
-
}, '保存'))),
|
|
3515
|
-
() => h('div', { 'data-bd-sec': '' },
|
|
3516
|
-
heading('list', '限量与报告'),
|
|
3517
|
-
h('div', { 'data-bd-row': '' },
|
|
3518
|
-
h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '一天最多挑几条'),
|
|
3707
|
+
h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '每日精选上限'),
|
|
3519
3708
|
h('input', {
|
|
3520
|
-
'data-bd-input': '', 'data-bd-num': '', type: 'number', 'aria-label': '
|
|
3709
|
+
'data-bd-input': '', 'data-bd-num': '', type: 'number', 'aria-label': '每日精选上限',
|
|
3521
3710
|
min: numLim('perDay', 0, 50)[0], max: numLim('perDay', 0, 50)[1],
|
|
3522
3711
|
value: perDay === null ? '5' : perDay, onChange: (e) => setPerDay(e.target.value),
|
|
3523
3712
|
})),
|
|
@@ -3530,30 +3719,15 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3530
3719
|
h('option', { value: 'full' }, '完整'),
|
|
3531
3720
|
h('option', { value: 'brief' }, '精简'),
|
|
3532
3721
|
h('option', { value: 'off' }, '不写'))),
|
|
3533
|
-
h('
|
|
3534
|
-
h('button', {
|
|
3535
|
-
'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy),
|
|
3536
|
-
onClick: () => patch({ perDay: clampField('perDay', perDay, 0, 50), reportLevel: rl || 'full' }, '限量与报告'),
|
|
3537
|
-
}, '保存'))),
|
|
3538
|
-
() => h('div', { 'data-bd-sec': '' },
|
|
3539
|
-
heading('dots', '长期偏好'),
|
|
3540
|
-
h('div', { 'data-bd-note': '' }, '关键词与参考文件提供长期背景,不限制她刷什么。参考路径不改变新会话所属工作区。'),
|
|
3541
|
-
h('textarea', {
|
|
3542
|
-
'data-bd-input': '', 'aria-label': '长期偏好关键词', rows: 3, placeholder: '一行一个',
|
|
3543
|
-
value: kws === null ? '' : kws, onChange: (e) => setKws(e.target.value),
|
|
3544
|
-
}),
|
|
3545
|
-
h('input', {
|
|
3722
|
+
h('details', { 'data-bd-advanced': '', 'data-bd-reference-details': '' }, h('summary', {}, '参考文件', h('span', { 'data-bd-note': '' }, wsDir ? ' · 已设置' : ' · 未设置')), h('div', {}, h('input', {
|
|
3546
3723
|
'data-bd-input': '', 'aria-label': '参考文件或目录', placeholder: '参考文件或目录(可选)',
|
|
3547
3724
|
value: wsDir === null ? '' : wsDir, onChange: (e) => setWsDir(e.target.value),
|
|
3548
|
-
}),
|
|
3725
|
+
}), h('div', { 'data-bd-note': '' }, '为报告提供背景资料,不改变新会话所属工作区。'))),
|
|
3549
3726
|
h('div', { 'data-bd-row': '' },
|
|
3550
3727
|
h('button', {
|
|
3551
|
-
'data-bd-btn': '', disabled: Boolean(busy),
|
|
3552
|
-
onClick:
|
|
3553
|
-
|
|
3554
|
-
await patch({ keywords: list, workspace: String(wsDir || '').trim() }, '长期偏好')
|
|
3555
|
-
},
|
|
3556
|
-
}, '保存'))),
|
|
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
|
+
}, '保存报告设置'))),
|
|
3557
3731
|
() => {
|
|
3558
3732
|
const info = st && st.pace && st.pace.explanation
|
|
3559
3733
|
const saved = Number(cfg.paceFactor ?? info?.factor ?? 1)
|
|
@@ -3590,12 +3764,13 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3590
3764
|
!valid ? h('div', { 'data-bd-msg': '', 'data-warn': '' }, `请填 ${lo}–${hi} 之间的数字;空白或越界草稿不会保存。`) : null,
|
|
3591
3765
|
h('div', { 'data-bd-note': '' }, info?.notes.interval || '保存只影响下一次成功发送后新钉下的间隔与随机抖动;已钉下的等待不会重算。'),
|
|
3592
3766
|
info ? h('div', { 'data-bd-sub': '', 'data-bd-pace-preview': '' },
|
|
3593
|
-
h('div', { 'data-bd-h': '' }, changed ? '
|
|
3767
|
+
h('div', { 'data-bd-h': '' }, changed ? '间隔草稿与当前小时额度(不是当前倒计时)' : '当前间隔与小时额度(不是当前倒计时)'),
|
|
3594
3768
|
...Object.entries(info.kinds).map(([key, v]) => h('div', { key, 'data-bd-note': '', 'data-bd-pace-range': key },
|
|
3595
3769
|
h('b', {}, v.label),
|
|
3596
3770
|
`:基础 ${range(v, 1)} · 已保存 ${range(v, saved)}`,
|
|
3597
3771
|
changed ? ` · 草稿 ${range(v, draft)}` : '',
|
|
3598
|
-
` ·
|
|
3772
|
+
` · 当前本地额度 ${v.hourCap} 条/滚动小时`)),
|
|
3773
|
+
h('div', { 'data-bd-note': '' }, '小时额度按后端当前策略显示,不随间隔倍数改变;草稿只预览间隔,实际等待见发送状态。'))
|
|
3599
3774
|
: h('div', { 'data-bd-note': '' }, '节奏说明尚未就绪;不会猜基础范围,完整重启 DSH 后重试。'),
|
|
3600
3775
|
h('div', { 'data-bd-row': '' },
|
|
3601
3776
|
h('button', {
|
|
@@ -3615,6 +3790,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3615
3790
|
h('div', { 'data-bd-h': '', 'data-bd-grow': '' }, icon('pulse'), h('b', {}, '它刚才在干嘛'),
|
|
3616
3791
|
h('span', { 'data-bd-badge': '', 'data-live': fyLive ? '' : undefined }, fyLive ? '在刷' : '没在刷')),
|
|
3617
3792
|
h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: onClearAct, disabled: Boolean(busy) }, '清屏')),
|
|
3793
|
+
/* ⚠️ P3-37:接口不存在时轮询已停,把话说清楚(文案对照图库页的 404 提示) */
|
|
3794
|
+
actRouteMissing ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'status', 'data-bd-act-missing': '' }, '活动接口不存在——可能宿主版本较旧,重启 DSH 后重试。') : null,
|
|
3618
3795
|
fyLive && actLive && actLive.thinking
|
|
3619
3796
|
? h('div', { 'data-bd-thinking': '' },
|
|
3620
3797
|
h('i', { 'data-bd-dot': '' }),
|
|
@@ -3740,10 +3917,32 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3740
3917
|
h('span', { 'data-bd-badge': '', 'data-live': s.done ? '' : undefined }, s.done ? '✓ 已完成' : '待办'),
|
|
3741
3918
|
h('span', { 'data-bd-grow': '', style: { fontWeight: s.done ? 400 : 600 } }, s.label),
|
|
3742
3919
|
s.done ? null : h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => setTab(s.view) }, '配置')))) : null,
|
|
3743
|
-
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
|
+
} }, '保存全局人格')))
|
|
3744
3942
|
}
|
|
3745
3943
|
const speechPage = () => h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'speech' },
|
|
3746
|
-
pageHead('互动与投递', '
|
|
3944
|
+
pageHead('互动与投递', '管理全局人格、公开发言、私信投递、自动回复和发送限制。'),
|
|
3945
|
+
personaCard(),
|
|
3747
3946
|
h(PetSendReviewPanel, { st, patch, busy, say }),
|
|
3748
3947
|
h('div', { 'data-bd-pet-grid': '' },
|
|
3749
3948
|
h('section', { 'data-bd-sec': '' }, heading('spark', '公开发言'), ToggleRow({
|
|
@@ -3823,7 +4022,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3823
4022
|
onChange: (v) => patch({ dmReplyFollowedOnly: v }, '仅回复已关注的用户'),
|
|
3824
4023
|
}))) : null,
|
|
3825
4024
|
h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => patch({ dmMode: dmMode || 'off', dmUid: String(dmUid || '').trim() }, '私信投递设置') }, '保存投递设置')),
|
|
3826
|
-
basicCards[
|
|
4025
|
+
basicCards[3](), h('section', { 'data-bd-sec': '' }, heading('pulse', '发送状态'), (() => {
|
|
3827
4026
|
const pc = st && st.pace
|
|
3828
4027
|
if (!pc || pc.ok === false) return h('div', { 'data-bd-note': '' }, '发送节奏读取失败。可重启 DSH 后重新加载。')
|
|
3829
4028
|
const cd = pc.cooldown
|
|
@@ -3926,39 +4125,53 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3926
4125
|
const f = st?.feiyu || {}
|
|
3927
4126
|
const bg = st?.backgroundRun || {}
|
|
3928
4127
|
const sessionOn = Boolean(f.active)
|
|
3929
|
-
const backgroundOn = Boolean(st?.bgBusy)
|
|
4128
|
+
const backgroundOn = Boolean(st?.bgBusy || bg.continuationPending)
|
|
3930
4129
|
const windowOn = Boolean(st?.bgWindow?.start && st?.bgWindow?.end)
|
|
3931
|
-
const
|
|
3932
|
-
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 || '等待手动开始'
|
|
3933
4142
|
const schedule = c.scheduleEnabled ? (c.scheduleAllDay ? '全天检查' : `${c.scheduleStart || '18:00'}–${c.scheduleEnd || '09:00'}`) : '未开启'
|
|
3934
|
-
const selectedBackground = backgroundOn || (!sessionOn && runMode === 'background')
|
|
4143
|
+
const selectedBackground = backgroundOn || (!sessionOn && !waitingSession && runMode === 'background')
|
|
3935
4144
|
const budgetValue = sessionOn ? f.tokens?.budget : backgroundOn ? bg.tokenBudget : selectedBackground ? c.backgroundTokenBudget : c.sessionTokenBudget
|
|
3936
4145
|
const budget = Number(budgetValue) > 0 ? Number(budgetValue).toLocaleString() + ' token / 趟' : '本趟不限(受总闸约束)'
|
|
3937
4146
|
return h('section', { 'data-bd-sec': '', 'data-bd-run-plan': '' },
|
|
3938
|
-
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 ? '等待续刷' : '待命')),
|
|
3939
4148
|
h('div', { 'data-bd-run-plan-grid': '' },
|
|
3940
4149
|
h('div', {}, h('span', {}, '现在'), h('strong', {}, mode)),
|
|
3941
|
-
h('div', {}, h('span', {}, '下一步'), h('strong', {},
|
|
4150
|
+
h('div', {}, h('span', {}, '下一步'), h('strong', {}, next)),
|
|
3942
4151
|
h('div', {}, h('span', {}, '模型'), h('strong', {}, sessionOn ? f.modelActive || f.model || '当前会话模型' : backgroundOn ? bg.model || '后台模型' : (selectedBackground ? c.backgroundModelId : c.sessionModelId) || '沿用宿主默认')),
|
|
3943
4152
|
h('div', {}, h('span', {}, '预算'), h('strong', {}, budget)),
|
|
3944
4153
|
h('div', {}, h('span', {}, '定时'), h('strong', {}, schedule)),
|
|
3945
4154
|
h('div', {}, h('span', {}, '完成反馈'), h('strong', {}, selectedBackground ? '桌宠提醒 + 运行日志' : '当前会话 + 运行日志'))),
|
|
3946
|
-
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 ? '切换设置页不会停止任务。等待续刷与接力也可用暂停或停止按钮取消。' : '当前会话、后台连续刷、定时窗口分别保存,互相不会覆盖参数。'))
|
|
3947
4157
|
}
|
|
3948
4158
|
const runView = () => h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'run' },
|
|
3949
4159
|
pageHead('运行方式', '会话运行、后台运行与定时计划。'),
|
|
3950
4160
|
st.toolScope?.checked && !st.toolScope.supported ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, '当前 DSH 不支持会话工具隔离。会话运行不可用,后台运行和本地管理仍可使用。') : null,
|
|
3951
4161
|
h('div', { 'data-bd-run-launches': '' }, ...['session', 'background'].map(mode => {
|
|
3952
4162
|
const isSession = mode === 'session', options = runSettings(mode)
|
|
3953
|
-
const
|
|
4163
|
+
const state = runState(mode)
|
|
4164
|
+
const active = isSession ? Boolean(st.feiyu?.active || state.waiting) : Boolean(st.bgBusy || st.backgroundRun?.continuationPending)
|
|
3954
4165
|
const title = isSession ? '新开会话刷' : '后台刷'
|
|
3955
4166
|
return h('section', { key: mode, 'data-bd-run-launch': mode },
|
|
3956
|
-
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)),
|
|
3957
4168
|
h('p', {}, isSession ? '在独立会话中运行,保留对话与执行进度。' : '在后台执行,支持连续时段与每日定时计划。'),
|
|
4169
|
+
state.warning || state.waiting || !isSession && Number(st.bgWindow?.holdUntil) > Date.now() ? h('div', { 'data-bd-note': '' }, state.note) : null,
|
|
3958
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 上限')),
|
|
3959
4171
|
h('button', { 'data-bd-btn': '', 'data-primary': '', 'data-bd-launch-action': mode, disabled: Boolean(busy) || active || (isSession && st.toolScope?.checked && !st.toolScope.supported),
|
|
3960
4172
|
onClick: () => startRun(mode) }, busy === 'start-' + mode ? '正在启动…' : isSession ? '新开会话刷' : '开始后台刷'))
|
|
3961
4173
|
})),
|
|
4174
|
+
h(PetCuriosityJournal, { st, busy, onExplore: curiosityId => { setRunMode('session'); return startRun('session', { curiosityId }) } }),
|
|
3962
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))),
|
|
3963
4176
|
runPage(runMode), runMode === 'background' ? scheduleCard() : null,
|
|
3964
4177
|
h('details', { 'data-bd-run-details': '', open: Boolean(st.recovery?.needsAction) }, h('summary', {}, '运行计划与恢复'), runPlan(), weeklyRecoveryCard({ showReview: false })))
|
|
@@ -3984,7 +4197,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
3984
4197
|
h('span', { 'data-bd-grow': '' }),
|
|
3985
4198
|
st.feiyu?.active ? h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => controlRun('stop') }, '暂停会话') : null,
|
|
3986
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),
|
|
3987
|
-
logMode === 'events' ? settingLogCard() : basicCards[
|
|
4200
|
+
logMode === 'events' ? settingLogCard() : basicCards[4]())
|
|
3988
4201
|
const videoPage = () => h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'video' },
|
|
3989
4202
|
h('div', { 'data-bd-pghead': '' }, icon('film'), h('div', {}, h('b', {}, '视频工作室'),
|
|
3990
4203
|
h('p', {}, '视频制作、制作记录与本地成片。'))), pvCard())
|
|
@@ -4058,7 +4271,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4058
4271
|
}
|
|
4059
4272
|
|
|
4060
4273
|
const StablePetSettingsModal = typeof React.memo === 'function' ? React.memo(PetSettingsModal, (a, b) =>
|
|
4061
|
-
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
|
|
4062
4275
|
&& a.status?.[0] === b.status?.[0] && a.status?.[1] === b.status?.[1] && a.status?.[2] === b.status?.[2]) : PetSettingsModal
|
|
4063
4276
|
|
|
4064
4277
|
/* 🐾 桌宠本体:状态灯 / 拖拽 / 弹菜单 / 新开会话刷(B 流程)都在这里编排。
|
|
@@ -4067,10 +4280,11 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4067
4280
|
function PetRoot({ ctx }) {
|
|
4068
4281
|
const [st, setSt, fails] = useStatus(5000)
|
|
4069
4282
|
const [pos, setPos] = React.useState(null) // null = 还没从 localStorage/默认位初始化
|
|
4070
|
-
const [panel, setPanel] = React.useState(null) // null | menu |
|
|
4283
|
+
const [panel, setPanel] = React.useState(null) // null | menu | status | settings(互斥)
|
|
4071
4284
|
const [dragging, setDragging] = React.useState(false)
|
|
4072
4285
|
const [sessBusy, setSessBusy] = React.useState(false)
|
|
4073
4286
|
const [settingsTab, setSettingsTab] = React.useState('run')
|
|
4287
|
+
const [settingsRunMode, setSettingsRunMode] = React.useState('session')
|
|
4074
4288
|
const [bubbles, setBubbles] = React.useState([])
|
|
4075
4289
|
const [toasts, toast] = useToasts()
|
|
4076
4290
|
const drag = React.useRef(null)
|
|
@@ -4236,18 +4450,19 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4236
4450
|
try {
|
|
4237
4451
|
let r1 = null
|
|
4238
4452
|
try { r1 = await jpost('/pet/session-new', newSessionContext(ctx)) }
|
|
4239
|
-
catch (e) { toast('创建会话失败:' + String(e?.message ?? e), 'warn'); return }
|
|
4453
|
+
catch (e) { toast('创建会话失败:' + String(e?.message ?? e) + (e?.sessionId ? ';已创建的会话仍保留:' + e.sessionId : ''), 'warn'); return }
|
|
4240
4454
|
if (!r1 || r1.ok === false) {
|
|
4241
|
-
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')
|
|
4242
4456
|
return
|
|
4243
4457
|
}
|
|
4244
4458
|
/* 切过去看它刷:uiWorkspace 是宿主服务,可能没有/名字可能不同 ⇒ 全程防御式;
|
|
4245
4459
|
* 切不过去也不拦开刷 —— 会话照样开了,只是没自动跳过去。 */
|
|
4460
|
+
let opened = false
|
|
4246
4461
|
try {
|
|
4247
4462
|
const ws = (typeof ctx.get === 'function' ? ctx.get('uiWorkspace') : null)
|
|
4248
|
-
if (ws && typeof ws.openSession === 'function') ws.openSession(r1.sessionId)
|
|
4463
|
+
if (ws && typeof ws.openSession === 'function') { await ws.openSession(r1.sessionId); opened = true }
|
|
4249
4464
|
} catch { /* 切换会话失败不影响开刷 */ }
|
|
4250
|
-
toast('已开会话,鱼进去了', 'good')
|
|
4465
|
+
toast(opened ? '已开会话,鱼进去了' : '会话已创建(' + r1.sessionId + '),无法自动打开时可从侧栏进入。', opened ? 'good' : 'info')
|
|
4251
4466
|
let r2 = null
|
|
4252
4467
|
try { r2 = await jpost('/pet/start', { mode: 'session', sessionId: r1.sessionId }) }
|
|
4253
4468
|
catch (e) { toast('开刷没成:' + String(e?.message ?? e), 'warn'); return }
|
|
@@ -4421,10 +4636,10 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4421
4636
|
item('session', 'play', '新开会话刷',
|
|
4422
4637
|
sessBusy ? '正在开会话…' : `${modelTxt} · 预算 ${petFmtWan(cfg.sessionTokenBudget ?? cfg.tokenBudget)}`,
|
|
4423
4638
|
() => { setPanel(null); runNewSession() }, sessBusy, sessBusy),
|
|
4424
|
-
item('background', 'moon', '后台刷', '运行时长与定时参数', () => setPanel('
|
|
4639
|
+
item('background', 'moon', '后台刷', '运行时长与定时参数', () => { setSettingsTab('run'); setSettingsRunMode('background'); setPanel('settings') }),
|
|
4425
4640
|
item('logs', 'list', '运行记录', '', () => { setSettingsTab('logs'); setPanel('settings') }),
|
|
4426
4641
|
item('status', 'pulse', '运行状态', '', () => setPanel('status')),
|
|
4427
|
-
item('settings', 'gear', '工作台', '', () => { setSettingsTab('run'); setPanel('settings') }),
|
|
4642
|
+
item('settings', 'gear', '工作台', '', () => { setSettingsTab('run'); setSettingsRunMode('session'); setPanel('settings') }),
|
|
4428
4643
|
h('hr', { 'data-bd-pet-sep': '' }),
|
|
4429
4644
|
item('hide', 'hide', '隐藏桌宠', '', hidePet),
|
|
4430
4645
|
h('footer', { 'data-bd-menu-token': '' },
|
|
@@ -4466,10 +4681,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4466
4681
|
同时 [data-bd-pet][data-align] / [data-flip] 这些选择器也要求卡片是它的后代。 */
|
|
4467
4682
|
panel === 'menu' ? menuCard : null,
|
|
4468
4683
|
panel === 'menu' && preview ? h(PetMenuPreview, { key: preview.kind, kind: preview.kind, anchor: preview.anchor, st, fails, tokenValue, tokenNote, onEnter: holdPreview, onLeave: leavePreview }) : null,
|
|
4469
|
-
panel === 'bg' ? h(PetBgPanel, { st, toast, onDone: () => setPanel('log') }) : null,
|
|
4470
|
-
panel === 'log' ? h(PetLogPanel, { toast }) : null,
|
|
4471
4684
|
panel === 'status' ? h(PetStatusCard, { st, fails }) : null,
|
|
4472
|
-
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,
|
|
4473
4686
|
h('div', { key: 'toast', 'data-bd-pet-toast': '', role: 'status', 'aria-live': 'polite', 'aria-atomic': false },
|
|
4474
4687
|
toasts.map((t) => h('div', { key: t.id, 'data-tone': t.tone || undefined }, t.text))),
|
|
4475
4688
|
]))
|