@ilaomianyang/dsh-memory-graph 0.10.4 → 0.12.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/CHANGELOG.md +40 -0
- package/README.en.md +73 -12
- package/README.md +73 -12
- package/client.js +433 -1
- package/docs/update-settings.png +0 -0
- package/index.js +42 -3
- package/package.json +5 -3
- package/src/auto.js +78 -14
- package/src/capture.js +37 -175
- package/src/config.js +37 -0
- package/src/distillapi.js +4 -1
- package/src/distillrun.js +7 -3
- package/src/extract.js +42 -18
- package/src/graph.js +10 -5
- package/src/graphdata.js +10 -1
- package/src/history.js +467 -0
- package/src/pageapi.js +2 -2
- package/src/schema.js +55 -0
- package/src/tools.js +76 -2
- package/src/turns.js +265 -0
- package/src/update.js +406 -0
- package/src/vault.js +21 -11
- package/test/auto.mjs +68 -0
- package/test/client.mjs +15 -5
- package/test/fixtures/session.v4.jsonl.zstd +0 -0
- package/test/history.mjs +249 -0
- package/test/tools.mjs +2 -1
- package/test/update.mjs +401 -0
package/client.js
CHANGED
|
@@ -34,6 +34,13 @@ const DISTILL_API = '/api/memory-graph.distill'
|
|
|
34
34
|
*/
|
|
35
35
|
const PANEL_ID = 'memory-graph'
|
|
36
36
|
const PAGE_API = '/api/memory-graph.page'
|
|
37
|
+
/**
|
|
38
|
+
* Version and self-update. One path, split by method: GET reports what is
|
|
39
|
+
* installed and whether the registry has something newer, POST asks the host to
|
|
40
|
+
* install it. Both answers carry their own `message`, so this page never has to
|
|
41
|
+
* invent wording for a state the host already described.
|
|
42
|
+
*/
|
|
43
|
+
const UPDATE_API = '/api/memory-graph.update'
|
|
37
44
|
|
|
38
45
|
const ZH = (() => {
|
|
39
46
|
try {
|
|
@@ -195,7 +202,7 @@ const SECTIONS = [
|
|
|
195
202
|
path: ['extractor', 'provider'],
|
|
196
203
|
type: 'text',
|
|
197
204
|
placeholder: 'deepseek / wb / agy-local',
|
|
198
|
-
label: t('provider', 'provider'),
|
|
205
|
+
label: t('模型提供方 provider', 'provider'),
|
|
199
206
|
hint: t('只有关掉"跟随当前模型"才需要。', 'Only needed when "follow the current model" is off.'),
|
|
200
207
|
visible: (value) =>
|
|
201
208
|
getPath(value, ['extractor', 'transport']) === 'harness' && getPath(value, ['extractor', 'useCurrentModel']) === false,
|
|
@@ -373,6 +380,95 @@ const SECTIONS = [
|
|
|
373
380
|
label: t('记到哪个项目', 'Project'),
|
|
374
381
|
hint: t('自动蒸馏出的卡片归属哪个项目。', 'Which project automatic cards are filed under.'),
|
|
375
382
|
},
|
|
383
|
+
{
|
|
384
|
+
path: ['auto', 'catchUp'],
|
|
385
|
+
type: 'boolean',
|
|
386
|
+
label: t('追平模式', 'Catch up'),
|
|
387
|
+
hint: t(
|
|
388
|
+
'只要还攒着一整批未蒸馏的情节,就按下面自己的节奏一批批蒸下去,直到追平;日常的「定时蒸」「攒够就蒸」不受影响。导入过历史、或者很久没开的时候用它。',
|
|
389
|
+
'While a full batch of undistilled episodes is still waiting, keep distilling batch after batch on its own pace below until the backlog drains; the two rules above keep working as usual. Meant for imported histories and long gaps.',
|
|
390
|
+
),
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
path: ['auto', 'catchUpEveryMinutes'],
|
|
394
|
+
type: 'number',
|
|
395
|
+
min: 1,
|
|
396
|
+
max: 1440,
|
|
397
|
+
step: 1,
|
|
398
|
+
unit: t('分钟', 'min'),
|
|
399
|
+
label: t('追平间隔', 'Catch-up gap'),
|
|
400
|
+
hint: t(
|
|
401
|
+
'追平模式下每批之间隔多少分钟(默认 10)。这条节奏独立于上面的「最短间隔」。',
|
|
402
|
+
'Minutes between catch-up batches (default 10). This pace is independent of the minimum gap above.',
|
|
403
|
+
),
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
path: ['auto', 'maxBatchesPerDay'],
|
|
407
|
+
type: 'number',
|
|
408
|
+
min: 0,
|
|
409
|
+
max: 200,
|
|
410
|
+
step: 1,
|
|
411
|
+
unit: t('批', 'batches'),
|
|
412
|
+
label: t('每天最多几批', 'Daily batch cap'),
|
|
413
|
+
hint: t(
|
|
414
|
+
'一天最多自动蒸几批,0 = 不限(默认 6)。这是唯一给自动花钱设上限的旋钮。',
|
|
415
|
+
'Most automatic batches in one calendar day; 0 lifts the cap (default 6). The only knob that caps what automatic runs can spend.',
|
|
416
|
+
),
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
id: 'update',
|
|
422
|
+
title: t('版本与更新', 'Version & updates'),
|
|
423
|
+
hint: t(
|
|
424
|
+
'检查更新只发一次 GET,问的是 npm 上有没有新版本,不带任何记忆库内容或用户信息。安装走宿主自己的 pluginManager,和 dsh plugin 是同一条路。',
|
|
425
|
+
'A check is one GET against the registry — no vault contents and no user data in it. Installing goes through the host\'s own pluginManager, the same path dsh plugin takes.',
|
|
426
|
+
),
|
|
427
|
+
fields: [
|
|
428
|
+
{
|
|
429
|
+
path: ['update', 'enabled'],
|
|
430
|
+
type: 'boolean',
|
|
431
|
+
label: t('检查更新', 'Check for updates'),
|
|
432
|
+
hint: t(
|
|
433
|
+
'定期问 npm 上有没有新版本,只查不装。关掉之后不再联网查版本,页面也不会自动查,「检查更新」按钮一起变灰。',
|
|
434
|
+
'Periodically asks npm whether a newer version exists, and only asks. Off means no network checks and no automatic read on this page; the Check button greys out with it.',
|
|
435
|
+
),
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
path: ['update', 'checkEveryHours'],
|
|
439
|
+
type: 'number',
|
|
440
|
+
min: 1,
|
|
441
|
+
max: 720,
|
|
442
|
+
step: 1,
|
|
443
|
+
unit: t('小时', 'hours'),
|
|
444
|
+
label: t('多久查一次', 'Check every'),
|
|
445
|
+
hint: t(
|
|
446
|
+
'查到的结果保持这么久,期间打开设置页不再重复请求。',
|
|
447
|
+
'A result stays good for this long, so reopening this page does not ask again.',
|
|
448
|
+
),
|
|
449
|
+
visible: (value) => getPath(value, ['update', 'enabled']) !== false,
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
path: ['update', 'autoInstall'],
|
|
453
|
+
type: 'boolean',
|
|
454
|
+
label: t('自动安装新版本', 'Install updates automatically'),
|
|
455
|
+
hint: t(
|
|
456
|
+
'启动时发现有新版本就装(装完要重启 Harness 才生效);link: 本地安装会拒绝而不是覆盖。',
|
|
457
|
+
'Installs a newer version at startup; the running process still needs a restart. A link: install is refused rather than overwritten.',
|
|
458
|
+
),
|
|
459
|
+
visible: (value) => getPath(value, ['update', 'enabled']) !== false,
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
path: ['update', 'registry'],
|
|
463
|
+
type: 'text',
|
|
464
|
+
placeholder: t('留空 = 宿主的源', 'empty = the harness registry'),
|
|
465
|
+
label: t('用哪个 npm 源', 'Registry'),
|
|
466
|
+
hint: t(
|
|
467
|
+
'留空就先用 Harness 配的源,再退到 registry.npmjs.org。',
|
|
468
|
+
"Empty means the harness's own registry first, then registry.npmjs.org.",
|
|
469
|
+
),
|
|
470
|
+
visible: (value) => getPath(value, ['update', 'enabled']) !== false,
|
|
471
|
+
},
|
|
376
472
|
],
|
|
377
473
|
},
|
|
378
474
|
{
|
|
@@ -748,6 +844,63 @@ const CSS = `
|
|
|
748
844
|
cursor: pointer;
|
|
749
845
|
}
|
|
750
846
|
.mg-btn-ghost:disabled { opacity: 0.55; cursor: default; }
|
|
847
|
+
/* The version strip inside the update section: header, then this line, then the
|
|
848
|
+
fields. Not a card and not an mg-* class — it is not a config knob and the
|
|
849
|
+
suite reads mg-card / mg-row / mg-btn to mean knobs and the save bar. */
|
|
850
|
+
.mgu-strip {
|
|
851
|
+
display: flex;
|
|
852
|
+
flex-direction: column;
|
|
853
|
+
gap: 6px;
|
|
854
|
+
padding: 2px 14px 10px;
|
|
855
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.08));
|
|
856
|
+
}
|
|
857
|
+
.mgu-line { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
858
|
+
.mgu-main { flex: 1 1 auto; min-width: 0; }
|
|
859
|
+
.mgu-label { display: flex; align-items: center; gap: 6px; font-weight: 500; }
|
|
860
|
+
.mgu-hint { color: var(--dsw-alias-label-tertiary, #808080); font-size: 11.5px; margin-top: 2px; }
|
|
861
|
+
.mgu-tag {
|
|
862
|
+
font-size: 10.5px;
|
|
863
|
+
font-weight: 500;
|
|
864
|
+
padding: 1px 6px;
|
|
865
|
+
border-radius: 999px;
|
|
866
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(59, 110, 240, 0.14));
|
|
867
|
+
color: var(--dsw-alias-brand-primary, #3b6ef0);
|
|
868
|
+
}
|
|
869
|
+
.mgu-tag[data-kind="muted"] {
|
|
870
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.06));
|
|
871
|
+
color: var(--dsw-alias-label-secondary, #5a5a5a);
|
|
872
|
+
}
|
|
873
|
+
.mgu-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
|
|
874
|
+
.mgu-check {
|
|
875
|
+
height: 28px;
|
|
876
|
+
padding: 0 12px;
|
|
877
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.16));
|
|
878
|
+
border-radius: 7px;
|
|
879
|
+
background: transparent;
|
|
880
|
+
color: inherit;
|
|
881
|
+
font: inherit;
|
|
882
|
+
font-size: 12.5px;
|
|
883
|
+
cursor: pointer;
|
|
884
|
+
}
|
|
885
|
+
.mgu-check:disabled { opacity: 0.55; cursor: default; }
|
|
886
|
+
.mgu-go {
|
|
887
|
+
height: 28px;
|
|
888
|
+
padding: 0 14px;
|
|
889
|
+
border: none;
|
|
890
|
+
border-radius: 7px;
|
|
891
|
+
background: var(--dsw-alias-brand-primary, #3b6ef0);
|
|
892
|
+
color: var(--dsw-alias-label-primary-foreground, #fff);
|
|
893
|
+
font: inherit;
|
|
894
|
+
font-size: 12.5px;
|
|
895
|
+
font-weight: 500;
|
|
896
|
+
cursor: pointer;
|
|
897
|
+
}
|
|
898
|
+
.mgu-go:disabled { opacity: 0.55; cursor: default; }
|
|
899
|
+
.mgu-msg { font-size: 11.5px; }
|
|
900
|
+
.mgu-msg[data-kind="ok"] { color: var(--dsw-alias-state-success-primary, #1a7f37); }
|
|
901
|
+
.mgu-msg[data-kind="error"] { color: var(--dsw-alias-state-error-primary, #c62828); }
|
|
902
|
+
.mgu-msg[data-kind="busy"] { color: var(--dsw-alias-label-tertiary, #808080); }
|
|
903
|
+
.mgu-msg[data-kind="info"] { color: var(--dsw-alias-label-secondary, #5a5a5a); }
|
|
751
904
|
/* The whole-vault panel: a bar, then the served page filling the rest. */
|
|
752
905
|
.mgv-panel { display: flex; flex-direction: column; height: 100%; min-height: 0; }
|
|
753
906
|
.mgv-panel-bar {
|
|
@@ -1070,6 +1223,142 @@ function createPage(ctx, React) {
|
|
|
1070
1223
|
|
|
1071
1224
|
const dirtyCount = ALL_FIELDS.filter((field) => isDirty(field)).length
|
|
1072
1225
|
|
|
1226
|
+
// ---- version & updates ------------------------------------------------
|
|
1227
|
+
//
|
|
1228
|
+
// The host owns every decision here: whether a check is possible, what is
|
|
1229
|
+
// installed, whether it may install itself. This page only asks, prints the
|
|
1230
|
+
// answer the host gave, and keeps its own errors local — a settings page that
|
|
1231
|
+
// blanks out because one route is missing or slow is worse than one that says
|
|
1232
|
+
// which route failed.
|
|
1233
|
+
|
|
1234
|
+
const [updateInfo, setUpdateInfo] = useState(null)
|
|
1235
|
+
const [updateError, setUpdateError] = useState('')
|
|
1236
|
+
const [checking, setChecking] = useState(false)
|
|
1237
|
+
const [applying, setApplying] = useState(false)
|
|
1238
|
+
const [updateNote, setUpdateNote] = useState(null)
|
|
1239
|
+
const updateMounted = useRef(true)
|
|
1240
|
+
const updateTimers = useRef([])
|
|
1241
|
+
|
|
1242
|
+
const stopUpdateTimers = () => {
|
|
1243
|
+
for (const timer of updateTimers.current) clearTimeout(timer)
|
|
1244
|
+
updateTimers.current = []
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/** One GET. `manual` is the 检查更新 button: it asks the host to re-check the registry. */
|
|
1248
|
+
const readUpdate = (manual) => {
|
|
1249
|
+
if (manual === true) setChecking(true)
|
|
1250
|
+
const send = typeof fetch === 'function' ? fetch : null
|
|
1251
|
+
if (!send) {
|
|
1252
|
+
setChecking(false)
|
|
1253
|
+
setUpdateError(
|
|
1254
|
+
'这一页拿不到 fetch,版本只能手动查:npm view @ilaomianyang/dsh-memory-graph version · ' +
|
|
1255
|
+
'No fetch in this page, so read the version from the shell instead',
|
|
1256
|
+
)
|
|
1257
|
+
return
|
|
1258
|
+
}
|
|
1259
|
+
let pending
|
|
1260
|
+
try {
|
|
1261
|
+
pending = send(apiUrl(manual === true ? `${UPDATE_API}?force=1` : UPDATE_API), {
|
|
1262
|
+
method: 'GET',
|
|
1263
|
+
headers: { accept: 'application/json' },
|
|
1264
|
+
credentials: 'same-origin',
|
|
1265
|
+
cache: 'no-store',
|
|
1266
|
+
})
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
setChecking(false)
|
|
1269
|
+
setUpdateError(`读不到版本信息:${String((error && error.message) || error)}`)
|
|
1270
|
+
return
|
|
1271
|
+
}
|
|
1272
|
+
Promise.resolve(pending)
|
|
1273
|
+
.then((response) =>
|
|
1274
|
+
response
|
|
1275
|
+
.json()
|
|
1276
|
+
.catch(() => null)
|
|
1277
|
+
.then((body) => ({ response, body })),
|
|
1278
|
+
)
|
|
1279
|
+
.then(({ response, body }) => {
|
|
1280
|
+
if (!updateMounted.current) return
|
|
1281
|
+
setChecking(false)
|
|
1282
|
+
if (!response.ok || !body || body.ok !== true) {
|
|
1283
|
+
const why = (body && (body.error || body.message)) || `HTTP ${response.status}`
|
|
1284
|
+
setUpdateError(String(why))
|
|
1285
|
+
return
|
|
1286
|
+
}
|
|
1287
|
+
setUpdateError('')
|
|
1288
|
+
setUpdateInfo(body)
|
|
1289
|
+
})
|
|
1290
|
+
.catch((failure) => {
|
|
1291
|
+
if (!updateMounted.current) return
|
|
1292
|
+
setChecking(false)
|
|
1293
|
+
setUpdateError(
|
|
1294
|
+
`连不上宿主:${String((failure && failure.message) || failure)} · Could not reach the host`,
|
|
1295
|
+
)
|
|
1296
|
+
})
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/** One POST, then three GETs: the host finishes the install behind the answer. */
|
|
1300
|
+
const applyUpdate = () => {
|
|
1301
|
+
if (applying) return
|
|
1302
|
+
stopUpdateTimers()
|
|
1303
|
+
setApplying(true)
|
|
1304
|
+
setUpdateNote({ kind: 'busy', text: '正在更新,别关这一页… · Updating; keep this page open' })
|
|
1305
|
+
const send = typeof fetch === 'function' ? fetch : null
|
|
1306
|
+
if (!send) {
|
|
1307
|
+
setApplying(false)
|
|
1308
|
+
setUpdateNote({ kind: 'error', text: '这一页拿不到 fetch,更新只能手动做 · No fetch here; update by hand' })
|
|
1309
|
+
return
|
|
1310
|
+
}
|
|
1311
|
+
let pending
|
|
1312
|
+
try {
|
|
1313
|
+
pending = send(apiUrl(UPDATE_API), {
|
|
1314
|
+
method: 'POST',
|
|
1315
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1316
|
+
credentials: 'same-origin',
|
|
1317
|
+
body: '{}',
|
|
1318
|
+
})
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
setApplying(false)
|
|
1321
|
+
setUpdateNote({ kind: 'error', text: `更新没能执行:${String((error && error.message) || error)}` })
|
|
1322
|
+
return
|
|
1323
|
+
}
|
|
1324
|
+
Promise.resolve(pending)
|
|
1325
|
+
.then((response) =>
|
|
1326
|
+
response
|
|
1327
|
+
.json()
|
|
1328
|
+
.catch(() => null)
|
|
1329
|
+
.then((body) => ({ response, body })),
|
|
1330
|
+
)
|
|
1331
|
+
.then(({ response, body }) => {
|
|
1332
|
+
if (!updateMounted.current) return
|
|
1333
|
+
setApplying(false)
|
|
1334
|
+
setUpdateNote(describeUpdate(response, body))
|
|
1335
|
+
// 2s / 4s / 8s after the answer: the install is written behind it, so the
|
|
1336
|
+
// version line catches up without the user pressing anything.
|
|
1337
|
+
for (const wait of [2000, 4000, 8000]) {
|
|
1338
|
+
updateTimers.current.push(setTimeout(() => readUpdate(false), wait))
|
|
1339
|
+
}
|
|
1340
|
+
})
|
|
1341
|
+
.catch((failure) => {
|
|
1342
|
+
if (!updateMounted.current) return
|
|
1343
|
+
setApplying(false)
|
|
1344
|
+
setUpdateNote({
|
|
1345
|
+
kind: 'error',
|
|
1346
|
+
text: `连不上宿主:${String((failure && failure.message) || failure)} · Could not reach the host`,
|
|
1347
|
+
})
|
|
1348
|
+
})
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
useEffect(() => {
|
|
1352
|
+
updateMounted.current = true
|
|
1353
|
+
readUpdate(false)
|
|
1354
|
+
return () => {
|
|
1355
|
+
updateMounted.current = false
|
|
1356
|
+
stopUpdateTimers()
|
|
1357
|
+
}
|
|
1358
|
+
// Read once when the page opens; every later read is a button or a poll.
|
|
1359
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1360
|
+
}, [])
|
|
1361
|
+
|
|
1073
1362
|
// ---- states that are not the form ------------------------------------
|
|
1074
1363
|
|
|
1075
1364
|
const statePanel = (title, lines) =>
|
|
@@ -1224,6 +1513,102 @@ function createPage(ctx, React) {
|
|
|
1224
1513
|
|
|
1225
1514
|
const visibleFields = (section) => section.fields.filter((field) => !field.visible || field.visible(shownValue))
|
|
1226
1515
|
|
|
1516
|
+
// ---- the version strip ------------------------------------------------
|
|
1517
|
+
//
|
|
1518
|
+
// Drawn inside the `update` section, between its header and its rows: the
|
|
1519
|
+
// page already names the section, so this is one line — version, two buttons,
|
|
1520
|
+
// and whatever the host last said. It writes no config path, and the suite
|
|
1521
|
+
// reads `mg-card`/`mg-row`/`mg-btn` to mean "a knob" and "the save bar", so
|
|
1522
|
+
// this keeps its own `mgu-*` names and its own buttons.
|
|
1523
|
+
|
|
1524
|
+
const updateBlock = () => {
|
|
1525
|
+
const info = updateInfo
|
|
1526
|
+
const link = Boolean(info && info.installKind === 'link')
|
|
1527
|
+
const working = applying || Boolean(info && info.busy === true)
|
|
1528
|
+
// The host already decided both questions: `canCheck` is off when update
|
|
1529
|
+
// checks are disabled in the settings, `canInstall` is on only when there is
|
|
1530
|
+
// a newer version, the install came from the registry, and nothing else is
|
|
1531
|
+
// running. Asking it again here would only invent a second opinion.
|
|
1532
|
+
const canCheck = !info || info.canCheck !== false
|
|
1533
|
+
const canInstall = Boolean(info) && info.canInstall === true
|
|
1534
|
+
const blockedBehind = Boolean(info) && info.behind === true && info.canInstall !== true && !link && !working
|
|
1535
|
+
const versionText = info && info.current ? String(info.current) : checking ? '…' : '—'
|
|
1536
|
+
|
|
1537
|
+
const hints = []
|
|
1538
|
+
if (info) {
|
|
1539
|
+
if (link) {
|
|
1540
|
+
hints.push(
|
|
1541
|
+
'本地 link: 安装,自动更新会覆盖你的开发目录 · installed as a link:, so updating would overwrite your working copy',
|
|
1542
|
+
)
|
|
1543
|
+
} else if (info.installKind === 'registry') {
|
|
1544
|
+
hints.push('从 npm registry 安装 · installed from the npm registry')
|
|
1545
|
+
} else {
|
|
1546
|
+
hints.push('安装方式未知 · install kind unknown')
|
|
1547
|
+
}
|
|
1548
|
+
// "Cannot update itself" is only true when there is something to install
|
|
1549
|
+
// and the host still said no — busy means wait, not unable.
|
|
1550
|
+
if (working) hints.push('正在安装,装完再看这里 · installing now; this line updates when it lands')
|
|
1551
|
+
else if (blockedBehind) hints.push('这个安装不能自动更新 · this installation cannot update itself')
|
|
1552
|
+
if (info.registry) hints.push(`源 registry:${info.registry}`)
|
|
1553
|
+
if (info.checkedAt) hints.push(`上次检查 ${info.checkedAt}`)
|
|
1554
|
+
} else if (updateError === '') {
|
|
1555
|
+
hints.push('正在读版本… · reading the version…')
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
const status = updateError
|
|
1559
|
+
? { kind: 'error', text: updateError }
|
|
1560
|
+
: updateNote || (info && info.message ? { kind: 'info', text: String(info.message) } : null)
|
|
1561
|
+
|
|
1562
|
+
return h(
|
|
1563
|
+
'div',
|
|
1564
|
+
{ className: 'mgu-strip', 'data-update': updateError ? 'error' : info ? 'ready' : 'reading' },
|
|
1565
|
+
h(
|
|
1566
|
+
'div',
|
|
1567
|
+
{ className: 'mgu-line' },
|
|
1568
|
+
h(
|
|
1569
|
+
'div',
|
|
1570
|
+
{ className: 'mgu-main' },
|
|
1571
|
+
h(
|
|
1572
|
+
'div',
|
|
1573
|
+
{ className: 'mgu-label' },
|
|
1574
|
+
h('span', null, '当前版本 · Current version'),
|
|
1575
|
+
info && info.behind === true ? h('span', { className: 'mgu-tag' }, '有新版本 · behind') : null,
|
|
1576
|
+
info && info.behind === false
|
|
1577
|
+
? h('span', { className: 'mgu-tag', 'data-kind': 'muted' }, '已是最新 · up to date')
|
|
1578
|
+
: null,
|
|
1579
|
+
),
|
|
1580
|
+
hints.length > 0 ? h('div', { className: 'mgu-hint' }, hints.join(' · ')) : null,
|
|
1581
|
+
),
|
|
1582
|
+
h(
|
|
1583
|
+
'div',
|
|
1584
|
+
{ className: 'mgu-actions' },
|
|
1585
|
+
h('span', { className: 'mg-code' }, versionText),
|
|
1586
|
+
h(
|
|
1587
|
+
'button',
|
|
1588
|
+
{
|
|
1589
|
+
type: 'button',
|
|
1590
|
+
className: 'mgu-check',
|
|
1591
|
+
disabled: checking || working || !canCheck,
|
|
1592
|
+
onClick: () => readUpdate(true),
|
|
1593
|
+
},
|
|
1594
|
+
checking ? '检查中… · Checking…' : '检查更新 · Check',
|
|
1595
|
+
),
|
|
1596
|
+
h(
|
|
1597
|
+
'button',
|
|
1598
|
+
{
|
|
1599
|
+
type: 'button',
|
|
1600
|
+
className: 'mgu-go',
|
|
1601
|
+
disabled: !canInstall || working || checking || updateError !== '',
|
|
1602
|
+
onClick: () => applyUpdate(),
|
|
1603
|
+
},
|
|
1604
|
+
working ? '更新中… · Updating…' : '更新 · Update',
|
|
1605
|
+
),
|
|
1606
|
+
),
|
|
1607
|
+
),
|
|
1608
|
+
status ? h('div', { className: 'mgu-msg', 'data-kind': status.kind }, status.text) : null,
|
|
1609
|
+
)
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1227
1612
|
return h(
|
|
1228
1613
|
'div',
|
|
1229
1614
|
{ className: 'mg-page' },
|
|
@@ -1254,6 +1639,10 @@ function createPage(ctx, React) {
|
|
|
1254
1639
|
h('div', { className: 'mg-card-t' }, section.title),
|
|
1255
1640
|
section.hint ? h('div', { className: 'mg-card-d' }, section.hint) : null,
|
|
1256
1641
|
),
|
|
1642
|
+
// The version strip belongs to this section's header area, not to the
|
|
1643
|
+
// field table: it is not a config path. It reads and writes nothing the
|
|
1644
|
+
// form owns, so it sits between the header and the rows.
|
|
1645
|
+
section.id === 'update' ? updateBlock() : null,
|
|
1257
1646
|
h('div', { className: 'mg-rows' }, visibleFields(section).map(row)),
|
|
1258
1647
|
),
|
|
1259
1648
|
),
|
|
@@ -1367,6 +1756,49 @@ function apiUrl(path) {
|
|
|
1367
1756
|
return path
|
|
1368
1757
|
}
|
|
1369
1758
|
|
|
1759
|
+
/**
|
|
1760
|
+
* The POST answer as one line, Chinese first and the English after it — the same
|
|
1761
|
+
* style the rest of the plugin writes user-facing text in.
|
|
1762
|
+
*
|
|
1763
|
+
* The four `application` values are the host's contract and they are not
|
|
1764
|
+
* interchangeable: `restart-required` means the files changed but the running
|
|
1765
|
+
* process did not, so the line must ask for a restart instead of claiming the
|
|
1766
|
+
* new version is already live.
|
|
1767
|
+
*/
|
|
1768
|
+
function describeUpdate(response, body) {
|
|
1769
|
+
const version = body && body.version ? String(body.version) : ''
|
|
1770
|
+
const at = version === '' ? '' : ` ${version}`
|
|
1771
|
+
if (response && response.ok === true && body && body.ok === true) {
|
|
1772
|
+
const application = String(body.application || '')
|
|
1773
|
+
if (application === 'restart-required') {
|
|
1774
|
+
return {
|
|
1775
|
+
kind: 'ok',
|
|
1776
|
+
text: `已更新到${at},需要重启 Harness 生效 · Updated to${at}; restart DeepSeek Harness to apply it`,
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
if (application === 'applied') {
|
|
1780
|
+
return { kind: 'ok', text: `已更新到${at},已生效 · Updated to${at}, live now` }
|
|
1781
|
+
}
|
|
1782
|
+
if (application === 'cancelled') {
|
|
1783
|
+
return { kind: 'error', text: '更新已取消,什么都没改 · Update cancelled; nothing was changed' }
|
|
1784
|
+
}
|
|
1785
|
+
if (application === 'failed') {
|
|
1786
|
+
return {
|
|
1787
|
+
kind: 'error',
|
|
1788
|
+
text: `更新到${at}失败 · Update to${at} failed${body.error ? `:${body.error}` : ''}`,
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
const said = body.message || body.error
|
|
1792
|
+
return { kind: 'ok', text: said ? String(said) : `更新已执行${at} · Update ran${at}` }
|
|
1793
|
+
}
|
|
1794
|
+
const said = body && (body.message || body.error)
|
|
1795
|
+
const status = response && response.status ? `HTTP ${response.status}` : ''
|
|
1796
|
+
return {
|
|
1797
|
+
kind: 'error',
|
|
1798
|
+
text: said ? String(said) : `更新没能执行(${status})· The update did not run (${status})`,
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1370
1802
|
function fillOf(node) {
|
|
1371
1803
|
if (node.k === 'session') return '#111318'
|
|
1372
1804
|
if (node.k === 'episode') return EPISODE_FILL
|
|
Binary file
|
package/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import { createDistillHandler, DISTILL_PATH } from './src/distillapi.js'
|
|
|
30
30
|
import { createPageHandler, PAGE_PATH } from './src/pageapi.js'
|
|
31
31
|
import { createDistillRunner } from './src/distillrun.js'
|
|
32
32
|
import { createAutoDistiller, registerAutoDistill } from './src/auto.js'
|
|
33
|
+
import { createUpdateChecker, createUpdateHandler, UPDATE_PATH } from './src/update.js'
|
|
33
34
|
import { extractorServices } from './src/extract.js'
|
|
34
35
|
|
|
35
36
|
export const name = 'memory-graph'
|
|
@@ -133,7 +134,7 @@ export function apply(ctx, config) {
|
|
|
133
134
|
})
|
|
134
135
|
|
|
135
136
|
ctx.effect(() => {
|
|
136
|
-
const disposers = registerTools(ctx, { cfg, vault, redactor, runner })
|
|
137
|
+
const disposers = registerTools(ctx, { cfg, vault, redactor, runner, getUpdate: () => updates.peek() })
|
|
137
138
|
log('info', `${disposers.length} memory tools registered`)
|
|
138
139
|
return () => {
|
|
139
140
|
for (const dispose of disposers) {
|
|
@@ -146,6 +147,32 @@ export function apply(ctx, config) {
|
|
|
146
147
|
}
|
|
147
148
|
})
|
|
148
149
|
|
|
150
|
+
// Version and updates. `pluginManager` is read opportunistically: a profile
|
|
151
|
+
// without it (a headless composition, a test harness) still reports the version
|
|
152
|
+
// it is running, it just cannot install anything.
|
|
153
|
+
const pluginManager = () => {
|
|
154
|
+
try {
|
|
155
|
+
return ctx.get?.('pluginManager')
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const updates = createUpdateChecker({ cfg, log, getPluginManager: pluginManager })
|
|
161
|
+
|
|
162
|
+
ctx.effect(() => {
|
|
163
|
+
// One check at startup — it is cached for `update.checkEveryHours` after this,
|
|
164
|
+
// so the cost is one request per day, not one per page load. With
|
|
165
|
+
// `autoInstall` on, a newer version is installed here, before any turn runs:
|
|
166
|
+
// the one moment where swapping the plugin cannot interrupt a conversation.
|
|
167
|
+
updates
|
|
168
|
+
.onStart()
|
|
169
|
+
.then((result) => {
|
|
170
|
+
if (result && typeof result.behind === 'boolean' && result.behind) log('info', `有新版本 ${result.latest}(当前 ${result.current})`)
|
|
171
|
+
})
|
|
172
|
+
.catch(() => {})
|
|
173
|
+
return () => {}
|
|
174
|
+
})
|
|
175
|
+
|
|
149
176
|
// The automatic distiller: a tick that decides, never a timer that fires a run.
|
|
150
177
|
// Both triggers (a threshold of waiting episodes, and "every N minutes") default
|
|
151
178
|
// to off and are live Settings fields, so the ticker runs whether or not they
|
|
@@ -221,9 +248,21 @@ export function apply(ctx, config) {
|
|
|
221
248
|
requestBody: 'buffered',
|
|
222
249
|
fetch: createPageHandler({ getVault: () => vault }),
|
|
223
250
|
})
|
|
224
|
-
|
|
251
|
+
// Version and updates. GET is a read (and the only method the page needs on
|
|
252
|
+
// load); POST is the one verb that can install a package, so a prefetch or a
|
|
253
|
+
// link preview can never trigger it.
|
|
254
|
+
const disposeUpdate = registry.register({
|
|
255
|
+
path: UPDATE_PATH,
|
|
256
|
+
methods: ['GET', 'POST'],
|
|
257
|
+
requestBody: 'buffered',
|
|
258
|
+
fetch: createUpdateHandler({ getChecker: () => updates }),
|
|
259
|
+
})
|
|
260
|
+
log(
|
|
261
|
+
'info',
|
|
262
|
+
`session memory view served at ${GRAPH_PATH}, distill at ${DISTILL_PATH}, vault graph page at ${PAGE_PATH}, update at ${UPDATE_PATH}`,
|
|
263
|
+
)
|
|
225
264
|
return () => {
|
|
226
|
-
for (const dispose of [disposeRoute, disposeDistill, disposePage]) {
|
|
265
|
+
for (const dispose of [disposeRoute, disposeDistill, disposePage, disposeUpdate]) {
|
|
227
266
|
try {
|
|
228
267
|
dispose?.()
|
|
229
268
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ilaomianyang/dsh-memory-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "ISKING <admin@isking.cc>",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"client.js",
|
|
35
35
|
"src/*.js",
|
|
36
36
|
"test/*.mjs",
|
|
37
|
+
"test/fixtures/*",
|
|
37
38
|
"eval/*.mjs",
|
|
38
39
|
"eval/questions.json",
|
|
39
40
|
"eval/REPORT.md",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
"LICENSE"
|
|
48
49
|
],
|
|
49
50
|
"scripts": {
|
|
50
|
-
"test": "node test/smoke.mjs && node test/schema.mjs && node test/client.mjs && node test/capture.mjs && node test/tools.mjs && node test/extract.mjs && node test/graphdata.mjs && node test/page.mjs && node test/distillapi.mjs && node test/auto.mjs && node eval/run.mjs",
|
|
51
|
+
"test": "node test/smoke.mjs && node test/schema.mjs && node test/client.mjs && node test/capture.mjs && node test/tools.mjs && node test/extract.mjs && node test/graphdata.mjs && node test/page.mjs && node test/distillapi.mjs && node test/auto.mjs && node test/history.mjs && node test/update.mjs && node eval/run.mjs",
|
|
51
52
|
"eval": "node eval/run.mjs",
|
|
52
53
|
"live": "node test/live.mjs",
|
|
53
54
|
"live:supersede": "node test/live-supersede.mjs",
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
},
|
|
75
76
|
"license": "MIT",
|
|
76
77
|
"dependencies": {
|
|
77
|
-
"@deepseek-ai/schemastery": "~3.18.5"
|
|
78
|
+
"@deepseek-ai/schemastery": "~3.18.5",
|
|
79
|
+
"fzstd": "^0.1.1"
|
|
78
80
|
}
|
|
79
81
|
}
|