dsh-session-rollover 1.1.1 → 1.2.1
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 +16 -0
- package/README.md +5 -3
- package/client.js +39 -12
- package/compatibility/policy.json +22 -0
- package/compatibility/runtime-0.2.2-rc.1.json +32 -0
- package/docs/COMPATIBILITY.md +77 -0
- package/docs/DETAIL.md +1 -1
- package/docs/DETAIL.zh-CN.md +1 -1
- package/docs/DEVELOPMENT.md +3 -0
- package/docs/PUBLISHING.md +22 -5
- package/marketplace/Oak-liuhj__dsh-session-rollover.yml +2 -2
- package/marketplace/SUBMISSION.md +33 -2
- package/package.json +7 -3
- package/scripts/check-dsh-compatibility.mjs +44 -0
- package/scripts/check-package.mjs +51 -0
- package/scripts/check-release.mjs +18 -1
- package/scripts/dsh-contracts.mjs +69 -0
- package/src/client/rollover-ui.js +38 -11
- package/src/server/compatibility.mjs +84 -0
- package/src/server/host-adapter.mjs +8 -4
- package/src/server/notice-route.mjs +6 -0
- package/src/server/plugin.mjs +33 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 1.2.1 — 2026-11-14
|
|
4
|
+
|
|
5
|
+
- 按维护策略将 DSH 安装兼容范围调整为 `0.2.1-rc.5 || >=0.2.2-rc.1 <0.3.0-0`,覆盖 `0.2.2-rc.3` 及范围内后续版本。
|
|
6
|
+
- 将安装范围与历史实机验收名单分离,发布检查改为核对显式范围;保留验收证据采集时的插件版本,不将历史记录视为当前版本实机验收。
|
|
7
|
+
- 同步中英文插件说明和升级文档;保留运行时关键能力探测、分卷暂停保护和既有数据路径。
|
|
8
|
+
|
|
9
|
+
## 1.2.0 — 2026-11-11
|
|
10
|
+
|
|
11
|
+
- 增加对 DSH `0.2.2-rc.1` 的确切版本支持,完成独立 home / profile 的真实宿主验收;保留旧版范围与原数据路径。
|
|
12
|
+
- 修复新版宿主重建 `uiConversation` 时交接卡插槽未随子作用域释放、重复占用 keyed slot 的问题。
|
|
13
|
+
- 集中宿主能力探测与读取结构预检,不兼容时在建卷和改名前暂停分卷,保留历史快照与只读拦截。
|
|
14
|
+
- 新版优先读取工作目录、活动 Agent 模型的领域服务,保留旧投影接口回退;可选服务热替换时避免旧清理回调误删新服务。
|
|
15
|
+
- 会话状态及面板补充兼容诊断,HTTP 建卷明确返回 503,缺失界面服务不直接中断渲染。
|
|
16
|
+
- 增加所有后续 npm DSH 版本的公开声明检查、Windows/Linux 与 Node 22/24 回归工作流和升级验收指南;不自动扩大兼容范围或发布。
|
|
17
|
+
- 增加实际 tgz 的隔离安装、稳定导出和面板资源校验;兼容策略及新模块随包分发。
|
|
18
|
+
|
|
3
19
|
## 1.1.1 — 2026-11-07
|
|
4
20
|
|
|
5
21
|
- 公开包名改为 `dsh-session-rollover`,作者 Oak-liuhj;不改变运行数据路径、`sessionRollover` 服务和兼容导出。
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
适合持续编码、问题排查和多步骤任务。支持 Web 与桌面客户端,无需为插件单独配置 API Key。
|
|
10
10
|
|
|
11
|
-
**版本 1.
|
|
11
|
+
**版本 1.2.1** · **兼容范围:DSH 0.2.1-rc.5 或 ≥0.2.2-rc.1、<0.3.0-0** · **维护者 Oak-liuhj**
|
|
12
12
|
|
|
13
13
|
[使用指南](./docs/DETAIL.zh-CN.md) · [English guide](./docs/DETAIL.md) · [更新记录](./CHANGELOG.md) · [问题反馈](https://github.com/Oak-liuhj/dsh-session-rollover/issues)
|
|
14
14
|
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
|
|
45
45
|
需要已配置模型、可正常对话的 DSH 宿主。浏览器入口随包预构建,安装插件时无需自行构建。
|
|
46
46
|
|
|
47
|
+
`1.2.1` 的安装兼容范围为 `0.2.1-rc.5 || >=0.2.2-rc.1 <0.3.0-0`,包括 `0.2.2-rc.3` 及范围内后续预发布、正式和补丁版本,不包含 `0.3.0` 的任何预发布或正式版本。升级时保留原数据目录。历史实机验收版本为 `0.2.1-rc.5`、`0.2.2-rc.1`;扩大安装范围不等于所有版本均已实测。后续逐版检查继续运行,关键接口变化时暂停分卷并提示。维护机制与证据边界见 [DSH 升级适配](docs/COMPATIBILITY.md)。
|
|
48
|
+
|
|
47
49
|
可在 DSH 插件市场搜索 `dsh-session-rollover` 安装并启用,也可通过插件包名、GitHub 源或本地安装包安装。
|
|
48
50
|
|
|
49
51
|
### 通过 npm 包名安装
|
|
@@ -75,7 +77,7 @@ dsh plugin --profile desktop add github:Oak-liuhj/dsh-session-rollover
|
|
|
75
77
|
将路径替换为实际 tgz 文件位置:
|
|
76
78
|
|
|
77
79
|
```powershell
|
|
78
|
-
dsh plugin --profile desktop add "D:/release/dsh-session-rollover-1.
|
|
80
|
+
dsh plugin --profile desktop add "D:/release/dsh-session-rollover-1.2.1.tgz"
|
|
79
81
|
```
|
|
80
82
|
|
|
81
83
|
桌面用户安装后,请通过桌面客户端重启宿主。已安装旧包 `@deepseek-ai/dsh-session-rollover` 时,应先替换旧依赖,避免同时加载新旧两份插件;升级前备份配置与插件数据。详细步骤见 [升级指南](./docs/PUBLISHING.md#从-scoped-旧包迁移)。
|
|
@@ -143,7 +145,7 @@ dsh plugin --profile desktop remove dsh-session-rollover
|
|
|
143
145
|
|
|
144
146
|
## 兼容性与开发
|
|
145
147
|
|
|
146
|
-
|
|
148
|
+
历史基线 DSH `0.2.1-rc.5` 的验证覆盖自动分卷、待办续接、来源导航、双向只读开关及浅深主题;`1.2.0` 对 DSH `0.2.2-rc.1` 的实机验收记录见[升级适配指南](docs/COMPATIBILITY.md)。旧宿主可能不显示插件长说明,可从会话 `⋯ → 使用说明` 查看内置指南。
|
|
147
149
|
|
|
148
150
|
现有验证使用隔离宿主与本地模型夹具,完整 Bot 群端、生产模型和后续宿主版本仍需单独复验。详见 [功能验证](./docs/r3-validation.md) 与 [市场展示验证](./docs/marketplace-validation.md)。
|
|
149
151
|
|
package/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by npm run build; edit src/client/. Version 1.
|
|
1
|
+
// Generated by npm run build; edit src/client/. Version 1.2.1.
|
|
2
2
|
// dsh-session-rollover · 浏览器半边(B6 一次性提示)。
|
|
3
3
|
// 通道:server 半把「该提示什么」落盘,这里同源 GET /api-dsh-session-rollover/pending 取走渲染。
|
|
4
4
|
// 不碰宿主 Toast 实现、不建 React hooks 面(D-36:bindInjectSources 会剥掉 hooks 字段),纯 DOM。
|
|
@@ -311,6 +311,18 @@ function createRolloverUI(React) {
|
|
|
311
311
|
const entries = new Map();
|
|
312
312
|
let navigate = null;
|
|
313
313
|
let renderInline = false;
|
|
314
|
+
const clientWarnings = new Set();
|
|
315
|
+
function unavailable(feature, error) {
|
|
316
|
+
clientWarnings.add(feature);
|
|
317
|
+
console.warn('[dsh-session-rollover] client capability unavailable:', feature, error?.message ?? '');
|
|
318
|
+
}
|
|
319
|
+
function inject(ctx, names, setup) {
|
|
320
|
+
try {
|
|
321
|
+
ctx.inject(names, scope => {
|
|
322
|
+
try { return setup(scope); } catch (error) { unavailable(names.join(','), error); }
|
|
323
|
+
});
|
|
324
|
+
} catch (error) { unavailable(names.join(','), error); }
|
|
325
|
+
}
|
|
314
326
|
|
|
315
327
|
async function request(sessionId, action) {
|
|
316
328
|
const response = await fetch(action ? API : `${API}?session=${encodeURIComponent(sessionId)}`, action ? {
|
|
@@ -428,8 +440,8 @@ function createRolloverUI(React) {
|
|
|
428
440
|
h('p', {className: 'dsr-help-credit'}, 'dsh-session-rollover · Oak-liuhj')) : null,
|
|
429
441
|
h('div', {className: 'dsr-actions-body'},
|
|
430
442
|
h('button', {type: 'button', 'aria-expanded': showHelp, onClick: () => setShowHelp(!showHelp)}, showHelp ? '收起使用说明' : '使用说明'),
|
|
431
|
-
h('button', {type: 'button', disabled: busy || info.sealed || !navigate, onClick: () => action('rollover')}, '现在换会话'),
|
|
432
|
-
h('button', {type: 'button', disabled: busy || !navigate, onClick: () => action('new')}, '新建独立会话'),
|
|
443
|
+
h('button', {type: 'button', disabled: busy || info.sealed || !navigate || info.compatibility?.compatible === false, onClick: () => action('rollover')}, '现在换会话'),
|
|
444
|
+
h('button', {type: 'button', disabled: busy || !navigate || info.compatibility?.compatible === false, onClick: () => action('new')}, '新建独立会话'),
|
|
433
445
|
h('button', {type: 'button', onClick: () => dialog.current?.close()}, '关闭'))),
|
|
434
446
|
error ? h('span', {role: 'alert'}, error) : null);
|
|
435
447
|
}
|
|
@@ -489,6 +501,8 @@ function createRolloverUI(React) {
|
|
|
489
501
|
if (!info) return null;
|
|
490
502
|
return h('div', {className: 'dsr-dock'},
|
|
491
503
|
info.error ? h('div', {role: 'alert'}, info.error) : null,
|
|
504
|
+
info.compatibility?.compatible === false ? h('div', {role: 'alert', className: 'dsr-warning'}, info.compatibility.message) : null,
|
|
505
|
+
clientWarnings.size ? h('div', {role: 'alert', className: 'dsr-warning'}, '部分会话界面功能与当前 DSH 不兼容,请更新插件。') : null,
|
|
492
506
|
h(ReadonlyControl, {key: props.sessionId, info}),
|
|
493
507
|
info.capped ? h('div', {role: 'status', className: 'dsr-warning'}, '同簇已达 9 卷上限(9/9),请通过会话操作新建独立会话。') : null,
|
|
494
508
|
info.retryLimitReached ? h('div', {role: 'status', className: 'dsr-warning'}, '摘要生成失败,已停止自动重试。可在修正内容后手动换会话。') : null,
|
|
@@ -506,7 +520,9 @@ function createRolloverUI(React) {
|
|
|
506
520
|
|
|
507
521
|
/** 释放时仅撤销自己的禁用;保留模型选择等其他插件后来设置的阻塞原因。 */
|
|
508
522
|
function blockComposer(blocks, id, reason) {
|
|
523
|
+
if (typeof blocks?.storeFor !== 'function' || typeof blocks?.set !== 'function') throw new Error('composer blocks unavailable');
|
|
509
524
|
const store = blocks.storeFor(id);
|
|
525
|
+
if (typeof store?.getSnapshot !== 'function' || typeof store?.subscribe !== 'function') throw new Error('composer store unavailable');
|
|
510
526
|
let previous = store.getSnapshot();
|
|
511
527
|
const own = {reason};
|
|
512
528
|
let writing = false;
|
|
@@ -528,13 +544,15 @@ function createRolloverUI(React) {
|
|
|
528
544
|
style.setAttribute('data-dsh-plugin', 'dsh-session-rollover-ui');
|
|
529
545
|
document.head.appendChild(style);
|
|
530
546
|
ctx.effect?.(() => () => { style.remove(); for (const item of entries.values()) item.stop?.(); entries.clear(); });
|
|
531
|
-
|
|
547
|
+
inject(ctx, ['uiWorkspace'], scope => {
|
|
548
|
+
if (typeof scope.uiWorkspace?.openSession !== 'function') throw new Error('session navigation unavailable');
|
|
532
549
|
navigate = id => scope.uiWorkspace.openSession(id);
|
|
533
550
|
scope.effect?.(() => () => { navigate = null; });
|
|
534
551
|
});
|
|
535
|
-
|
|
552
|
+
inject(ctx, ['slots'], scope => {
|
|
536
553
|
const slots = scope.slots;
|
|
537
|
-
|
|
554
|
+
if (typeof slots?.inject !== 'function' || typeof slots?.register !== 'function') throw new Error('slots unavailable');
|
|
555
|
+
const mount = (name, Component, options = {}, owner = scope) => owner.effect(() => owner.slots.inject(name, () => owner.slots.register({name,
|
|
538
556
|
id: 'dsh-session-rollover', order: 35, ...options}, Component)));
|
|
539
557
|
mount('conversation.session.header.actions', Header);
|
|
540
558
|
mount('conversation.input.dock', Dock);
|
|
@@ -543,20 +561,27 @@ function createRolloverUI(React) {
|
|
|
543
561
|
mount('sidebar.session.row.leading', props => slots.snapshot?.('sidebar.session.row.trailing')?.length
|
|
544
562
|
? null : h(Badges, {...props, compact: true}));
|
|
545
563
|
// 使用宿主公开 composer block 服务;覆盖输入组件会破坏其子槽授权。
|
|
546
|
-
|
|
564
|
+
inject(scope, ['conversation'], inputScope => {
|
|
565
|
+
if (typeof inputScope.conversation?.blocks?.storeFor !== 'function' || typeof inputScope.conversation?.blocks?.set !== 'function') {
|
|
566
|
+
throw new Error('composer blocks unavailable');
|
|
567
|
+
}
|
|
547
568
|
function ComposerGuard(props) {
|
|
548
569
|
const info = useInfo(props.sessionId);
|
|
549
570
|
const reason = !info ? '正在读取会话状态…' : info.readonly
|
|
550
571
|
? '归档会话:不可输入,可通过上方开关恢复写入。' : info.error;
|
|
551
|
-
useEffect(() =>
|
|
552
|
-
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
if (props.sessionId && reason) {
|
|
574
|
+
try { return blockComposer(inputScope.conversation.blocks, props.sessionId, reason); }
|
|
575
|
+
catch (error) { unavailable('conversation.blocks', error); }
|
|
576
|
+
}
|
|
577
|
+
},
|
|
553
578
|
[props.sessionId, reason]);
|
|
554
579
|
return null;
|
|
555
580
|
}
|
|
556
581
|
inputScope.effect(() => slots.inject('conversation.input.dock', () => slots.register({
|
|
557
582
|
name: 'conversation.input.dock', id: 'dsr-readonly-guard', order: 34}, ComposerGuard)));
|
|
558
583
|
});
|
|
559
|
-
|
|
584
|
+
inject(scope, ['uiConversation'], textScope => {
|
|
560
585
|
if (!textScope.uiConversation.userText?.register) return;
|
|
561
586
|
textScope.effect(() => textScope.uiConversation.userText.register({id: 'dsr-handoff', order: -100,
|
|
562
587
|
match(text, from) {
|
|
@@ -564,8 +589,10 @@ function createRolloverUI(React) {
|
|
|
564
589
|
const payload = parseHandoff(text);
|
|
565
590
|
return payload ? {id: 'dsr-handoff', start: 0, end: text.length, data: payload} : undefined;
|
|
566
591
|
}}));
|
|
592
|
+
// 插槽必须随 uiConversation 的注入子作用域释放,不能挂在较长寿命的 slots 父作用域。
|
|
593
|
+
// 新宿主重连/热替换领域服务时会重建该子作用域;挂在父级会重复占用同一个 keyed slot。
|
|
567
594
|
mount('conversation.user.text.node', props => h(Card, {payload: props.node.data,
|
|
568
|
-
source: props.node.data.from_session_id && !props.node.data.cluster?.independent ? {sessionId: props.node.data.from_session_id, title: props.node.data.from_title ?? '上一卷'} : null}), {key: 'dsr-handoff'});
|
|
595
|
+
source: props.node.data.from_session_id && !props.node.data.cluster?.independent ? {sessionId: props.node.data.from_session_id, title: props.node.data.from_title ?? '上一卷'} : null}), {key: 'dsr-handoff'}, textScope);
|
|
569
596
|
renderInline = true;
|
|
570
597
|
textScope.effect?.(() => () => { renderInline = false; });
|
|
571
598
|
});
|
|
@@ -586,7 +613,7 @@ body[data-ds-dark-theme] :is(.dsr-header,.dsr-dock,.dsr-card,.dsr-row-badges),ht
|
|
|
586
613
|
@media(max-width:600px){.dsr-header{gap:3px;flex-wrap:nowrap;max-width:calc(100vw - 156px)}.dsr-header>.dsr-chip,.dsr-header>.dsr-source{display:none}.dsr-header>.dsr-chip:first-child{display:inline-flex;font-size:10px;padding:1px 3px}}
|
|
587
614
|
@media(prefers-reduced-motion:reduce){.dsr-card *{animation:none!important;transition:none!important}}
|
|
588
615
|
`;
|
|
589
|
-
return {apply, Header, Dock, Card, Badges, useInfo, parseHandoff, publish, blockComposer, CSS};
|
|
616
|
+
return {apply, Header, Dock, Card, Badges, useInfo, parseHandoff, publish, blockComposer, clientWarnings, CSS};
|
|
590
617
|
}
|
|
591
618
|
|
|
592
619
|
function apply(ctx) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"package": "@deepseek-ai/dsh",
|
|
4
|
+
"baseline": "0.2.1-rc.5",
|
|
5
|
+
"supportedRuntimeRange": "0.2.1-rc.5 || >=0.2.2-rc.1 <0.3.0-0",
|
|
6
|
+
"verifiedRuntimeVersions": ["0.2.1-rc.5", "0.2.2-rc.1"],
|
|
7
|
+
"registry": "https://registry.npmjs.org/",
|
|
8
|
+
"contracts": [
|
|
9
|
+
{"package": "@deepseek-ai/dsh-session", "methods": ["get", "list", "snapshotEvents"], "tokens": ["session/event"]},
|
|
10
|
+
{"package": "@deepseek-ai/dsh-session-title", "methods": ["get", "rename"]},
|
|
11
|
+
{"package": "@deepseek-ai/dsh-api-session-controller", "methods": ["create", "prompt", "selectModel"]},
|
|
12
|
+
{"package": "@deepseek-ai/dsh-session-projection", "methods": ["stateOf"]},
|
|
13
|
+
{"package": "@deepseek-ai/dsh-working-directory", "methods": ["get"]},
|
|
14
|
+
{"package": "@deepseek-ai/dsh-workspace", "methods": ["list"], "tokens": ["sessionIds"]},
|
|
15
|
+
{"package": "@deepseek-ai/dsh-agent", "methods": ["get", "current"], "tokens": ["agent/pre-step", "reject"],
|
|
16
|
+
"versionOverrides": {"0.2.1-rc.5": {"methods": ["get"]}}},
|
|
17
|
+
{"package": "@deepseek-ai/dsh-client-connection", "methods": ["admit"]},
|
|
18
|
+
{"package": "@deepseek-ai/dsh-client-ui-conversation", "methods": ["storeFor", "set"], "tokens": ["conversation.session.header.actions", "conversation.input.dock", "blocks"]},
|
|
19
|
+
{"package": "@deepseek-ai/dsh-client-ui-workspace", "methods": ["openSession"], "alternatives": [["sidebar.session.row.trailing", "sidebar.session.row.leading"]]},
|
|
20
|
+
{"package": "@deepseek-ai/dsh-client-ui-slots", "methods": ["register"]}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"passed": true,
|
|
3
|
+
"runtimeVerified": true,
|
|
4
|
+
"checkedAt": "2026-11-11T07:26:36.784Z",
|
|
5
|
+
"dsh": "0.2.2-rc.1",
|
|
6
|
+
"plugin": "1.2.0",
|
|
7
|
+
"platform": "Windows",
|
|
8
|
+
"environment": "installed DSH CLI with isolated home and profile",
|
|
9
|
+
"model": "localhost fixture only; no provider credentials or real model quality assessment",
|
|
10
|
+
"checks": {
|
|
11
|
+
"tenHumanMessagesOneRollover": true,
|
|
12
|
+
"workspaceAndDirectoryInherited": true,
|
|
13
|
+
"modelInherited": true,
|
|
14
|
+
"frozenTodosAndActiveStep": true,
|
|
15
|
+
"continuationTodoProgress": true,
|
|
16
|
+
"archivedReadonly": true,
|
|
17
|
+
"restoredWritePersistsAcrossRefresh": true,
|
|
18
|
+
"restoredVolumeDoesNotRolloverAgain": true,
|
|
19
|
+
"realHostRestartPreservesIndexAndSnapshot": true,
|
|
20
|
+
"anonymousHTTPStatus": 401,
|
|
21
|
+
"sourceNavigationAndCopyJSON": true,
|
|
22
|
+
"inlineSlotLifecycleFixedAndRechecked": true
|
|
23
|
+
},
|
|
24
|
+
"browser": "manual automation through computer-use; no plugin warnings after fix and real restart",
|
|
25
|
+
"limitation": "Host reconnect logged a native released-session-reference error; plugin UI recovered after page reload. No full Bot or production provider acceptance.",
|
|
26
|
+
"sourceDigests": {
|
|
27
|
+
"client.js": "25a484847caf9a2ec71b11817d3c147c83fd546f2f80582826df124d742c2b69",
|
|
28
|
+
"src/server/plugin.mjs": "6afd4b8d5e6c03082240ca2f7304bff9fd3ec0e28bfa32e1fe369458f4a48ae5",
|
|
29
|
+
"src/server/host-adapter.mjs": "d145284cfa392d64b00d45f40ab308b320d768dc23606bbdbb048c0db6b25dd6",
|
|
30
|
+
"src/server/compatibility.mjs": "5572940c2bb6fc16dd0393f369417bd9acc025ab65fd7df5f370ededf454ce38"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# DSH 升级适配与验收
|
|
2
|
+
|
|
3
|
+
插件按“声明兼容范围、运行前探测、逐版检查、真实宿主验收”维护。安装范围与实机验收记录分别维护;不能提前保证尚未发布的 DSH 一定兼容,也不根据版本号猜测新接口。
|
|
4
|
+
|
|
5
|
+
## 当前范围
|
|
6
|
+
|
|
7
|
+
- 历史真实宿主验收基线:`0.2.1-rc.5`;`1.2.0` 已在本机安装的 `0.2.2-rc.1` 独立 home / profile 中完成实机验收。
|
|
8
|
+
- `1.2.1` 按维护者指定策略声明 `0.2.1-rc.5 || >=0.2.2-rc.1 <0.3.0-0`,允许范围内后续 RC、正式和补丁版本安装;排除 `0.3.0` 的全部预发布和正式版本。
|
|
9
|
+
- DSH 的版本判定使用 `includePrerelease: true`:该范围也包含 `0.2.3-alpha.1` 等后续 0.2.x 预发布版本;普通 SemVer 工具的默认预发布排除规则不同,复核时需使用与宿主相同的选项。`0.2.1` 分支仅保留确切的 `0.2.1-rc.5`。
|
|
10
|
+
- `compatibility/policy.json` 的 `supportedRuntimeRange` 决定安装范围,`engines.dsh` 必须与其一致;`verifiedRuntimeVersions` 独立记录已完成实机验收的版本,不再用于拼接安装白名单。
|
|
11
|
+
- `verifiedRuntimeVersions` 保留 `0.2.1-rc.5`、`0.2.2-rc.1`;历史证据保留采集时的插件版本,不因扩大范围而修改为新发行版或加入未实测宿主。
|
|
12
|
+
- npm 声明扫描、夹具回归和真实宿主验收分别记录。扫描通过不自动修改版本约束,不自动发布 npm 包,也不自动改用户 profile。
|
|
13
|
+
- 已安装旧版的用户需更新到包含新兼容声明的插件包;npm 同一版本不会被覆盖更新。历史发布核验记录见 [投稿材料](../marketplace/SUBMISSION.md)。
|
|
14
|
+
|
|
15
|
+
## 1.2.1 范围调整(2026-11-14)
|
|
16
|
+
|
|
17
|
+
本次调整版本元数据、发布检查与文档,不修改分卷运行逻辑、能力保护或数据路径。`0.2.2-rc.3` 的十一个组件公开声明检查通过;这不是新版本的完整实机分卷验收,因此没有将它加入 `verifiedRuntimeVersions`。
|
|
18
|
+
|
|
19
|
+
## 本次验证(2026-11-11)
|
|
20
|
+
|
|
21
|
+
| 检查 | 结果与范围 |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| 本地回归 | Windows、Node.js 24.22.1,100 项全部通过(含旧宿主有 agents、无 model.current 时的投影回退) |
|
|
24
|
+
| 公开声明 | `0.2.1-rc.5`、`0.2.2-alpha.3`、`0.2.2-rc.1` 的十一个组件包均通过 |
|
|
25
|
+
| 发布包 | `1.2.0` 实际 tgz 隔离安装、服务端导出、预构建客户端和展示资源通过 |
|
|
26
|
+
| 新版真实宿主 | `1.2.0` 在 `0.2.2-rc.1` 验收通过,保留旧基线并增加该实测版本;当时的精确白名单已由 1.2.1 范围策略替代 |
|
|
27
|
+
| 远程 CI | Windows/Linux、Node 22/24 四组回归与打包、三个版本的声明检查均通过;见下方远程复验记录 |
|
|
28
|
+
|
|
29
|
+
声明报告和安装包校验报告分别保存在 `out/dsh-compatibility.json`、`out/package-verification.json`;它们不是实际宿主验收报告。远程矩阵已通过 [运行 42981276082](https://github.com/Oak-liuhj/dsh-session-rollover/actions/runs/42981276082)。首次 Linux 运行发现 Windows 专用的不可写目录测试夹具不成立,已改为文件阻挡父目录并通过复验;该修复只影响测试,不改变发布包的运行代码。`v1.2.0` tag 原始失败记录保留,详见 [发布记录](../marketplace/SUBMISSION.md)。
|
|
30
|
+
|
|
31
|
+
真实宿主记录见 [runtime-0.2.2-rc.1.json](../compatibility/runtime-0.2.2-rc.1.json):十条用户消息仅分卷一次,继承工作区、目录及模型,待办快照保持不变,新卷实时待办可继续推进,来源跳转、复制 JSON、双向归档开关和宿主重启恢复正常,匿名 HTTP 请求返回 401。本轮使用本地模型夹具,不代表真实服务商模型或 Bot 端完整验收。
|
|
32
|
+
|
|
33
|
+
重启复验发现并修复 `uiConversation` 子作用域卸载后交接卡 keyed slot 未释放的问题;插槽现在随该子作用域清理,重复注入回归与实机重启复验通过。宿主热重连期间仍观察到原生会话引用已释放的日志,刷新后恢复;这与插件重复插槽问题分开记录,不宣称宿主全局无错误。
|
|
34
|
+
|
|
35
|
+
## 运行保护
|
|
36
|
+
|
|
37
|
+
服务端集中在 `src/server/compatibility.mjs` 检查会话存储、标题、创建/提示/模型选择、投影、工作区接口,以及同步读取的返回结构。关键能力不满足时,自动与手动分卷在创建会话、改名、保存重试之前停止;HTTP 建卷操作返回 `503 host-incompatible`,面板显示更新提示。
|
|
38
|
+
|
|
39
|
+
已有的交接快照与只读状态仍可读取;已注册的服务端归档写入拦截不因分卷暂停而关闭。方法存在不证明事件语义仍相同;如果升级导致插件本身无法加载,这些插件内保护也无法运行,所以仍需遵守宿主版本约束。
|
|
40
|
+
|
|
41
|
+
新版宿主优先通过 `workingDirectory.get(session)` 和活动 Agent 的 `model.current()` 读取领域状态。缺少这些可选服务时,回退到已验证的 `sessionProjections.stateOf`。后者在较新源码中已弃用,仍纳入声明检查;将来移除时明确失败,由维护者适配,而不是静默换成空目录或默认模型。
|
|
42
|
+
|
|
43
|
+
基线 `0.2.1-rc.5` 的 Agent 声明尚无 `current()`,检查策略对此确切版本保留显式例外,运行时使用投影回退;后续版本继续检查领域服务接口。未知模型结构会暂停分卷,合法的空选择保持可用。
|
|
44
|
+
|
|
45
|
+
浏览器针对插槽、导航与输入阻塞服务做能力检查。缺少可选界面能力时记录带插件名的警告,并在可用的面板中提示;不让缺失服务直接导致 React 渲染错误。服务端诊断可通过 `ctx.sessionRollover.compatibility()` 获取;每个会话状态的 `compatibility` 还包含返回结构检查。
|
|
46
|
+
|
|
47
|
+
## 自动检查
|
|
48
|
+
|
|
49
|
+
仓库工作流 `.github/workflows/compatibility.yml` 包含:
|
|
50
|
+
|
|
51
|
+
1. Windows / Linux、Node.js 22 / 24 上运行完整回归、发布格式检查和 tgz 隔离安装校验。
|
|
52
|
+
2. 从官方 npm 注册表枚举基线发布时刻之后的每个 DSH 版本,包含 alpha、rc 与后续补丁;不只检查 `latest`,避免遗漏中间版本。
|
|
53
|
+
3. 下载各版本的十一个公开组件包,核对 SHA512 后只读扫描类型声明,检查插件使用的方法、事件名及界面插槽,并保存报告。不会执行上游安装脚本。
|
|
54
|
+
|
|
55
|
+
工作流在代码推送、PR、手动触发和每六小时的定时任务中运行;需要提交到 GitHub 默认分支并启用 Actions 才会生效。本地创建文件不等于已启用远程任务。注册表请求或归档校验失败均使任务失败,不视为兼容通过。版本数超过 200 时要求维护基线,不静默截断。
|
|
56
|
+
|
|
57
|
+
```powershell
|
|
58
|
+
npm test
|
|
59
|
+
npm run check:release
|
|
60
|
+
npm run check:package
|
|
61
|
+
npm run check:dsh -- all
|
|
62
|
+
npm run check:dsh -- 0.2.2-rc.1
|
|
63
|
+
node scripts/check-dsh-compatibility.mjs --list
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
需要网络与系统 `tar`;Windows 自带的 tar 可用。报告默认写入忽略提交的 `out/dsh-compatibility.json`,也可用 `DSH_COMPATIBILITY_REPORT` 指定位置。`level: public-declaration-smoke`、`runtimeVerified: false` 明确说明证据边界。声明扫描不会证明方法签名、授权、事件顺序或视觉渲染完全兼容。
|
|
67
|
+
|
|
68
|
+
`check:package` 在自有临时目录安装实际 tgz,复核导出、预构建客户端和全部发布资源,结束后清理隔离目录;包与报告保存在 `out/`。此检查也不代表真实 DSH 宿主验收。
|
|
69
|
+
|
|
70
|
+
## 每次 DSH 发布后的维护步骤
|
|
71
|
+
|
|
72
|
+
1. 查看每个新版本的声明扫描和回归结果;发现方法、事件或插槽变化时更新集中适配层,保留旧宿主路径的测试。
|
|
73
|
+
2. 用该版本的真实 DSH,在隔离 home / profile 和本地模型夹具中验证:插件装载、十条真实用户消息只分一次、模型与工作区继承、待办交接、来源导航、重启恢复、归档只读及恢复写入、HTTP owner 授权。不得使用生产会话验收。
|
|
74
|
+
3. 记录 DSH 与插件版本、平台、结果及证据位置。通过后才向 `verifiedRuntimeVersions` 添加确切版本及实机证据,保留证据采集时的插件版本。范围内新增实测版本无需修改 `engines.dsh`;超出范围时需单独审核并同步修改 `supportedRuntimeRange` 与清单。旧基线继续用于回归。
|
|
75
|
+
4. 更新插件版本、构建预生成客户端、完成打包与隔离安装验证,再发布新 npm 版本。用户通过正常插件升级获得适配;不得覆盖同一 npm 版本。
|
|
76
|
+
|
|
77
|
+
宿主升级后遇到兼容提示时,应保留数据目录,使用已验收版本组合或更新插件;不要删除快照、绕过宿主版本检查或同时加载两份插件。
|
package/docs/DETAIL.md
CHANGED
|
@@ -35,7 +35,7 @@ Default storage: `<DSH_HOME>/plugin-data/dsh-session-rollover`, or `~/.dsh/plugi
|
|
|
35
35
|
|
|
36
36
|
Summaries use deterministic extraction, without another model call or API key. Conversation continuation uses the model configured in DSH. Background jobs are recorded as handles and states; processes are not moved, resumed or restarted. Summary text is limited to 1500 characters and the full handoff message to 16 KiB; oversized handoffs fail with a notice. Automatic failures get at most two further retries, triggered by subsequent user messages.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Version `1.2.1` declares the DSH installation range `0.2.1-rc.5 || >=0.2.2-rc.1 <0.3.0-0`. It includes `0.2.2-rc.3` and subsequent versions within the range, and excludes all `0.3.0` prereleases and stable releases. Historical runtime validation covers `0.2.1-rc.5` and `0.2.2-rc.1`; the latter used an isolated host and local model fixture for ten-message rollover, model and workspace inheritance, todo continuation, archive controls and restart recovery. The declared range and tested versions are maintained separately; the range does not claim runtime validation for every host, production model or Bot group channel. Retain the data directory when upgrading. Older hosts may omit this long description; the conversation `⋯` menu includes a built-in usage guide.
|
|
39
39
|
|
|
40
40
|
Replace the old `@deepseek-ai/dsh-session-rollover` dependency and bundle rather than loading both versions. Back up profile configuration and plugin data before upgrading. See the Chinese [publishing and migration guide](./PUBLISHING.md).
|
|
41
41
|
|
package/docs/DETAIL.zh-CN.md
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
摘要是确定性摘录,不调用额外模型;继续对话仍使用 DSH 已配置的模型,无需额外 API Key。后台任务只继承句柄和状态,不迁移、恢复或重新启动进程。正文摘要上限 1500 字符、完整交接消息上限 16 KiB,超预算会拒绝交接并提示。自动失败最多再试两次,由后续用户轮次触发。
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
`1.2.1` 的 DSH 安装兼容范围为 `0.2.1-rc.5 || >=0.2.2-rc.1 <0.3.0-0`,包括 `0.2.2-rc.3` 及范围内后续版本,排除 `0.3.0` 的预发布与正式版本。历史实机验收覆盖 `0.2.1-rc.5`、`0.2.2-rc.1`;后者验证了十轮分卷、模型和工作区继承、待办续接、归档开关及重启恢复。安装范围与实测名单独立维护,不代表范围内所有宿主、模型或 Bot 群端均已验收。升级时保留数据目录。旧宿主可能不显示本长说明,可从会话 `⋯ → 使用说明` 查看内置指南。
|
|
42
42
|
|
|
43
43
|
## 升级与支持
|
|
44
44
|
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -29,6 +29,8 @@ R2 面板可继续调用原有 rollover/info 服务;无实时 `todo/write` 时
|
|
|
29
29
|
|
|
30
30
|
`connection` 为显式必需注入;`jobs`、`agentDefaultModel` 通过独立可选注入获取,避免 Cordis 未声明服务访问报错。
|
|
31
31
|
|
|
32
|
+
`workingDirectory`、`agents` 同样通过可选注入获取,用于新版领域状态读取。`sessionRollover.compatibility()` 返回关键方法的能力报告;会话 `info().compatibility` 还检查读取结果形状。逐版检查、CI 与真实宿主验收流程见 [升级适配指南](./COMPATIBILITY.md)。
|
|
33
|
+
|
|
32
34
|
## 项目结构
|
|
33
35
|
|
|
34
36
|
```text
|
|
@@ -76,6 +78,7 @@ docs/r3-demo.html 固定数据交互原型
|
|
|
76
78
|
```powershell
|
|
77
79
|
npm test
|
|
78
80
|
npm run check:release
|
|
81
|
+
npm run check:package
|
|
79
82
|
npm pack --dry-run
|
|
80
83
|
npm pack --pack-destination <输出目录>
|
|
81
84
|
```
|
package/docs/PUBLISHING.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# 发布与升级指南
|
|
2
2
|
|
|
3
|
-
公开包名为 `dsh-session-rollover
|
|
3
|
+
公开包名为 `dsh-session-rollover`,当前源码版本 1.2.1,维护者 Oak-liuhj。仓库:[Oak-liuhj/dsh-session-rollover](https://github.com/Oak-liuhj/dsh-session-rollover)。支持在宿主中直接填写 npm 包名安装;市场收录不代替 npm 发布。实际公开版本以 npm 官方源和 GitHub Release 为准,发布核验记录见 [投稿材料](../marketplace/SUBMISSION.md)。
|
|
4
4
|
|
|
5
5
|
## 构建与打包
|
|
6
6
|
|
|
7
7
|
```powershell
|
|
8
8
|
npm test
|
|
9
9
|
npm run check:release
|
|
10
|
+
npm run check:package
|
|
10
11
|
npm pack --pack-destination D:/release
|
|
11
12
|
```
|
|
12
13
|
|
|
@@ -21,8 +22,8 @@ npm pack --pack-destination D:/release
|
|
|
21
22
|
```powershell
|
|
22
23
|
npm login --auth-type=web --registry=https://registry.npmjs.org/
|
|
23
24
|
npm whoami --registry=https://registry.npmjs.org/
|
|
24
|
-
npm publish "D:/release/dsh-session-rollover-1.
|
|
25
|
-
npm view dsh-session-rollover@1.
|
|
25
|
+
npm publish "D:/release/dsh-session-rollover-1.2.1.tgz" --access public --registry=https://registry.npmjs.org/
|
|
26
|
+
npm view dsh-session-rollover@1.2.1 version dist.integrity --registry=https://registry.npmjs.org/
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
4. 在隔离目录从 npm 安装并核对包名、版本、挂载补丁、预构建客户端与模块导入;只有 npm 返回该版本且下载验证通过,才确认包名安装可用。
|
|
@@ -30,13 +31,29 @@ npm view dsh-session-rollover@1.1.1 version dist.integrity --registry=https://re
|
|
|
30
31
|
|
|
31
32
|
`publishConfig.registry` 固定为 npm 官方源;公开包名为 `dsh-session-rollover`。npm 已发布的同一版本不能覆盖,后续发布需先提升版本号。认证和双重验证失败时保留待发布包,完成认证后继续发布,不绕过验证。
|
|
32
33
|
|
|
34
|
+
### 已进入暂存区时
|
|
35
|
+
|
|
36
|
+
使用支持 `npm stage` 的 CLI,先查询已有暂存记录,再确认同一个包:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
npm stage list dsh-session-rollover --json --registry=https://registry.npmjs.org/
|
|
40
|
+
npm stage view <stage-id> --json --registry=https://registry.npmjs.org/
|
|
41
|
+
npm stage approve <stage-id> --registry=https://registry.npmjs.org/
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
核对版本、`latest` 标签和 shasum 与已审核 tgz 一致。`staged` 仅表示待审批,同一暂存版本不能再次普通发布;保留原 tgz 和 stage id,完成审批后再执行下载与隔离安装验证。首次暂存可能产生 `0.0.0-stage` 占位版本,不能以包名可查询作为正式版本发布成功的依据。
|
|
45
|
+
|
|
46
|
+
CLI 登录授权、注册安全密钥和本次发布的本人认证是不同步骤。账号已启用 2FA 但审批仍要求配置时,先核对账号状态并尝试全新登录,不反复注册密钥;认证 URL 失效也不代表 tgz 丢失。若全新登录后问题仍存在,保存不含凭据的错误、时间和 stage id,向 [npm 支持](https://www.npmjs.com/support)反馈。不要通过关闭 2FA 或创建绕过认证的令牌处理审批异常。
|
|
47
|
+
|
|
48
|
+
认证请求有有效期,应在本人准备好设备验证后再启动命令,并让 npm 直接打开系统浏览器。不要先生成地址再等待聊天确认;手工打开时核对完整地址,避免回调参数混入中文标点或正文。有效期已过时重新运行原审批命令,不复用旧地址。
|
|
49
|
+
|
|
33
50
|
## 从 scoped 旧包迁移
|
|
34
51
|
|
|
35
52
|
1. 记录真实运行的 profile,桌面通常是 `~/.dsh/profiles/desktop`。备份该 profile 的 package.json、锁文件、Cordis 配置和旧插件目录,并备份 `<DSH_HOME>/plugin-data/dsh-session-rollover`;自定义 dataDir 也需备份。
|
|
36
53
|
2. 通过桌面插件管理器/宿主支持的依赖管理渠道,将 `@deepseek-ai/dsh-session-rollover` 替换为新包。CLI 可使用 `dsh plugin --profile desktop remove @deepseek-ai/dsh-session-rollover`,然后 `dsh plugin --profile desktop add <本地tgz或GitHub源>`。分步操作时先停止宿主,避免短暂缺包。
|
|
37
54
|
3. profile 的 `dsh.profile.bundles` 只保留新包一次;显式 Cordis 节点的 `name` 改为新包名,原 `id: session-rollover`、用户配置值和相邻插件保留。不可同时加载两个包来注册同一个服务。不能只覆盖 node_modules 而保留过期依赖来源。
|
|
38
55
|
4. 从桌面客户端重启实际宿主;不要用普通 Web profile 的重启操作接管桌面生命周期。
|
|
39
|
-
5.
|
|
56
|
+
5. 核实已加载目标版本、面板图标/简介、会话 `⋯ → 使用说明`、旧卷快照和只读状态。不要为了安装验证向生产会话发送测试消息。
|
|
40
57
|
|
|
41
58
|
本次更名不迁移数据;数据目录、`sessionRollover`、HTTP 路由、Cordis id 和旧 `src/*` 导出保持兼容。卸载也不删除数据。回滚时停止宿主,恢复备份的 profile 配置、锁文件和依赖,再重启;不要覆盖更新后已产生的用户数据。
|
|
42
59
|
|
|
@@ -60,4 +77,4 @@ npm view dsh-session-rollover@1.1.1 version dist.integrity --registry=https://re
|
|
|
60
77
|
3. 等仓库创建满一天并确保 topic 为 `dsh-plugin`。按 [投稿材料](../marketplace/SUBMISSION.md) 向官方插件目录发起 PR。
|
|
61
78
|
4. 确认市场抓取后的简介、图标、截图及安装源正确;外部收录前不要宣称已上架。
|
|
62
79
|
|
|
63
|
-
`engines.dsh`
|
|
80
|
+
`engines.dsh` 与 `compatibility/policy.json` 的 `supportedRuntimeRange` 一致,当前为 `0.2.1-rc.5 || >=0.2.2-rc.1 <0.3.0-0`;`verifiedRuntimeVersions` 独立记录真实宿主验收,不决定安装范围,也不因包版本更新而改写历史证据中的插件版本。声明扫描通过后仍按 [升级验收](./COMPATIBILITY.md) 补充实机记录。许可证声明为 `(Apache-2.0 AND MIT)`,必须同时分发 LICENSE、LICENSE.upstream 和 NOTICE。
|
|
@@ -2,6 +2,6 @@ url: https://github.com/Oak-liuhj/dsh-session-rollover
|
|
|
2
2
|
name: Oak-liuhj/dsh-session-rollover
|
|
3
3
|
category: session
|
|
4
4
|
description:
|
|
5
|
-
en: 'Split long DSH conversations
|
|
6
|
-
zh: '
|
|
5
|
+
en: 'Split long DSH conversations every 10 user messages by default, with todo handoff snapshots, source links and read-only archives; declares DSH 0.2.1-rc.5 or >=0.2.2-rc.1 <0.3.0-0 compatibility.'
|
|
6
|
+
zh: '长会话默认每 10 条用户消息自动分卷,支持待办快照续接、来源回溯及归档只读;声明兼容 DSH 0.2.1-rc.5 或 >=0.2.2-rc.1 <0.3.0-0。'
|
|
7
7
|
icon: https://raw.githubusercontent.com/Oak-liuhj/dsh-session-rollover/main/assets/icon.png
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## 投稿记录
|
|
4
4
|
|
|
5
|
+
2026-11-11 已提交市场更新 PR:[awesome-dsh-plugin/awesome-dsh-plugin#14240](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/14240)。只修改现有条目的中英文简介,补充宿主兼容检查;正文说明 1.2.0 已发布及支持的 DSH 版本,并附真实宿主与远程 CI 证据。分支为 `Oak-liuhj:codex/update-dsh-session-rollover-1-2-0`,提交为 `5cd4d474f448a42fc2828978d6e215f9078447e8`。提交不代表已合并,npm/version 元数据仍由市场自动采集。
|
|
6
|
+
|
|
7
|
+
2026-11-11 复核:首次收录 PR 已于 **2026-11-10 00:46:29 UTC** 合并,官方 main 的条目存在,在线 `plugins.json` 已包含本插件。市场条目指向本仓库,不锁定插件版本,后续发布无需重复投稿同名条目。
|
|
8
|
+
|
|
9
|
+
本次发布后再次下载目录,条目的 `npm`、`version` 展示字段仍为 `null`,需等待市场重新抓取元数据;不据此宣称市场已显示 1.2.0。GitHub main 已包含新版代码,npm 官方源按包名可安装 1.2.0。
|
|
10
|
+
|
|
5
11
|
2026-11-09 已提交首次上架 PR:[awesome-dsh-plugin/awesome-dsh-plugin#13453](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/13453)。
|
|
6
12
|
|
|
7
13
|
- 插件版本:`1.1.1`,投稿时已发布源码提交为 `541321a91e77942ba23bcc19267b2d5a8a79ccbf`。
|
|
@@ -9,11 +15,36 @@
|
|
|
9
15
|
- 唯一新增文件:`data/plugins/Oak-liuhj__dsh-session-rollover.yml`。
|
|
10
16
|
- 校验与审核的实时状态以 PR 页面为准。提交 PR 不等于市场已收录;正式收录还需维护者合并及目录后续构建。
|
|
11
17
|
|
|
18
|
+
## npm 发布记录
|
|
19
|
+
|
|
20
|
+
### 1.2.0
|
|
21
|
+
|
|
22
|
+
2026-11-11,`dsh-session-rollover@1.2.0` 已发布,npm 官方源 `latest` 为 `1.2.0`,声明支持 DSH `0.2.1-rc.5 || 0.2.2-rc.1`。
|
|
23
|
+
|
|
24
|
+
- [npm 1.2.0](https://www.npmjs.com/package/dsh-session-rollover/v/1.2.0) 与 [GitHub Release v1.2.0](https://github.com/Oak-liuhj/dsh-session-rollover/releases/tag/v1.2.0) 均已公开;tag 指向 `5bfde1e93ca4021fc7fcab59609925125bc2f740`。
|
|
25
|
+
- 两处实际下载的 tgz 均与审核包逐字节哈希一致;SHA256 为 `b35a9fa386d176e7dcc133e1a8bf36ac853ac7841786059bf302563c65c984a3`,SHA1 为 `7b1b69e17abfbc76a960f277a31092f70187f669`。GitHub 附有 `SHA256SUMS`。
|
|
26
|
+
- 已从 npm 官方源按 `dsh-session-rollover@1.2.0` 隔离安装,并核对版本、宿主范围、导出入口及全部发布资源。未改动生产 profile。
|
|
27
|
+
- 发布后的远程检查发现一个测试夹具将 Windows 的不存在盘符当作跨平台不可写路径;Linux 实际能创建该目录。提交 `0b526f057fdc1662ed3a7e7f2f973525578556c0` 改用文件阻挡父目录,不改变已发布的运行代码或安装包,也不移动既有 tag。
|
|
28
|
+
- [远程复验](https://github.com/Oak-liuhj/dsh-session-rollover/actions/runs/42981276082) 全部通过:Windows/Linux、Node 22/24 四组回归与打包检查,以及三个 DSH 版本的公开声明检查。`v1.2.0` tag 的原始 CI 仍保留测试夹具失败记录。
|
|
29
|
+
|
|
30
|
+
桌面用户更新到 1.2.0 后重启 DSH;镜像尚未同步时,可使用 GitHub Release 附件或 GitHub 源。后续 DSH 版本仍需逐版真实宿主验收。
|
|
31
|
+
|
|
32
|
+
### 1.1.1
|
|
33
|
+
|
|
34
|
+
2026-11-11 **13:51(北京时间)**,`dsh-session-rollover@1.1.1` 已正式发布到 [npm](https://www.npmjs.com/package/dsh-session-rollover/v/1.1.1)。npm 发布账号为 `oak-ai`,插件作者与 GitHub 仓库所有者为 `Oak-liuhj`。
|
|
35
|
+
|
|
36
|
+
- npm 官方源与腾讯镜像的 `latest` 均已确认为 `1.1.1`。
|
|
37
|
+
- 下载的 tgz 与审核通过的原包一致,SHA1 为 `cbe802fe44f8ae14da3eebc8d6a92e3468609ab5`,SHA512 integrity 也已核对。
|
|
38
|
+
- 两个源均已通过按包名隔离安装;已验证服务端入口导入、挂载补丁、预构建客户端、图标、双语资源及许可证文件。
|
|
39
|
+
- 用户可在「填入插件 npm 包名」中输入 `dsh-session-rollover`;CLI 桌面 profile 示例为 `dsh plugin --profile desktop add dsh-session-rollover`。
|
|
40
|
+
|
|
41
|
+
本次验证未改动生产 DSH profile;正式版本发布与市场目录收录分别核实,npm 的 `0.0.0-stage` 占位版本不作为发布成功依据。
|
|
42
|
+
|
|
12
43
|
## 投稿规则与材料
|
|
13
44
|
|
|
14
45
|
投稿目录为 [awesome-dsh-plugin/awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin),市场应用代码仓库 `dsh-market/dsh-market` 不是插件条目的投稿目标。规则来源:[contributing.md](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/blob/main/contributing.md)(核对于 2026-11-07)。
|
|
15
46
|
|
|
16
|
-
|
|
47
|
+
首次投稿仅新增 `data/plugins/Oak-liuhj__dsh-session-rollover.yml`,后续更新修改同一文件,内容使用本目录同名文件;不手改自动生成的 README,不增加 npm/version/tags 等非目录字段。投稿规则已于 2026-11-11 再次核对,npm 关联无需另行通知维护者。
|
|
17
48
|
|
|
18
49
|
建议 PR 标题:`Add Oak-liuhj/dsh-session-rollover`
|
|
19
50
|
|
|
@@ -23,4 +54,4 @@
|
|
|
23
54
|
|
|
24
55
|
提交前确认公开 main 分支或安装目标包含 `client.js`、图标和截图;仓库 topic 包含 `dsh-plugin`,创建满一天。仓库创建时间核对为 2026-11-06 06:45:34 UTC,最早满一天时间为北京时间 **2026-11-07 14:45:34**。npm 发布可选,未发布 npm 时使用 GitHub 安装源。
|
|
25
56
|
|
|
26
|
-
本目录的 YAML 已通过上述 PR
|
|
57
|
+
本目录的 YAML 已通过上述 PR 收录。市场收录、npm 发布和 GitHub Release 是独立状态;首次市场投稿未包含 npm 发布,后续 npm 与 GitHub Release 的核验见上方版本记录。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-rollover",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH 会话分卷与续接:自动分卷、待办交接快照、来源回溯和归档只读,支持 Web 与桌面客户端。",
|
|
6
6
|
"main": "index.js",
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"docs/screenshots/help-light.png",
|
|
40
40
|
"docs/screenshots/help-dark.png",
|
|
41
41
|
"marketplace/",
|
|
42
|
+
"compatibility/",
|
|
43
|
+
"docs/COMPATIBILITY.md",
|
|
42
44
|
"docs/r3-demo.html",
|
|
43
45
|
"docs/screenshots/prototype-fixed.png",
|
|
44
46
|
"docs/live-host-validation.json",
|
|
@@ -51,7 +53,9 @@
|
|
|
51
53
|
"pretest": "npm run build",
|
|
52
54
|
"prepack": "npm run build && node scripts/check-release.mjs",
|
|
53
55
|
"test": "node --test \"test/**/*.test.mjs\"",
|
|
54
|
-
"check:release": "npm run build:check && node scripts/check-release.mjs"
|
|
56
|
+
"check:release": "npm run build:check && node scripts/check-release.mjs",
|
|
57
|
+
"check:dsh": "node scripts/check-dsh-compatibility.mjs",
|
|
58
|
+
"check:package": "npm run check:release && node scripts/check-package.mjs"
|
|
55
59
|
},
|
|
56
60
|
"dsh": {
|
|
57
61
|
"manifestVersion": 1,
|
|
@@ -96,6 +100,6 @@
|
|
|
96
100
|
"registry": "https://registry.npmjs.org/"
|
|
97
101
|
},
|
|
98
102
|
"engines": {
|
|
99
|
-
"dsh": "0.2.1-rc.5"
|
|
103
|
+
"dsh": "0.2.1-rc.5 || >=0.2.2-rc.1 <0.3.0-0"
|
|
100
104
|
}
|
|
101
105
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {registryJSON, publishedTargets, checkPackage} from './dsh-contracts.mjs';
|
|
5
|
+
const policy = JSON.parse(await fs.readFile(new URL('../compatibility/policy.json', import.meta.url), 'utf8'));
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const list = args.includes('--list');
|
|
8
|
+
const target = args.find(value => !value.startsWith('--')) ?? 'latest';
|
|
9
|
+
const metadata = await registryJSON(policy.registry + encodeURIComponent(policy.package));
|
|
10
|
+
const versions = publishedTargets(metadata, policy.baseline);
|
|
11
|
+
if (!versions.length || versions.length > 200) throw new Error('DSH compatibility matrix requires baseline maintenance');
|
|
12
|
+
if (list) {
|
|
13
|
+
// 超出 GitHub matrix 上限时明确失败,要求维护者调整基线,不静默漏掉新版本。
|
|
14
|
+
console.log(JSON.stringify({version: versions}));
|
|
15
|
+
} else {
|
|
16
|
+
const selected = target === 'all' ? versions : [metadata['dist-tags']?.[target] ?? target];
|
|
17
|
+
const directory = await fs.mkdtemp(path.join(tmpdir(), 'dsh-contracts-'));
|
|
18
|
+
const report = {level: 'public-declaration-smoke', runtimeVerified: false,
|
|
19
|
+
checkedAt: new Date().toISOString(), baseline: policy.baseline, targets: []};
|
|
20
|
+
try {
|
|
21
|
+
for (const version of selected) {
|
|
22
|
+
const results = [];
|
|
23
|
+
for (const contract of policy.contracts) {
|
|
24
|
+
try { results.push(await checkPackage(contract, version, policy, directory)); }
|
|
25
|
+
catch (error) { results.push({package: contract.package, version, error: String(error.message)}); }
|
|
26
|
+
}
|
|
27
|
+
const passed = results.every(result => !result.error && !result.missing?.length);
|
|
28
|
+
report.targets.push({version, passed, packages: results});
|
|
29
|
+
console.log(`${version}: ${passed ? 'PASS' : 'FAIL'} (public declarations; not runtime verification)`);
|
|
30
|
+
for (const result of results.filter(r => r.error || r.missing?.length)) console.error(JSON.stringify(result));
|
|
31
|
+
}
|
|
32
|
+
report.passed = report.targets.every(target => target.passed);
|
|
33
|
+
const output = process.env.DSH_COMPATIBILITY_REPORT ?? path.resolve('out/dsh-compatibility.json');
|
|
34
|
+
await fs.mkdir(path.dirname(output), {recursive: true});
|
|
35
|
+
await fs.writeFile(output, JSON.stringify(report, null, 2) + '\n');
|
|
36
|
+
if (!report.passed) process.exitCode = 1;
|
|
37
|
+
} finally {
|
|
38
|
+
// 只删除 mkdtemp 创建并持有的独立目录。
|
|
39
|
+
if (path.dirname(path.resolve(directory)) !== path.resolve(tmpdir()) || !path.basename(directory).startsWith('dsh-contracts-')) {
|
|
40
|
+
throw new Error('Refusing cleanup outside the owned temporary directory');
|
|
41
|
+
}
|
|
42
|
+
await fs.rm(directory, {recursive: true, force: true});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** 校验真正交付的 tgz:隔离安装、稳定导出、清单和预构建浏览器入口;不接触 DSH profile。 */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {tmpdir} from 'node:os';
|
|
6
|
+
import {execFile} from 'node:child_process';
|
|
7
|
+
import {promisify} from 'node:util';
|
|
8
|
+
import {pathToFileURL, fileURLToPath} from 'node:url';
|
|
9
|
+
import {createHash} from 'node:crypto';
|
|
10
|
+
const run = promisify(execFile);
|
|
11
|
+
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
12
|
+
const npm = process.env.npm_execpath;
|
|
13
|
+
assert.ok(npm && path.basename(npm) === 'npm-cli.js', 'Run this check with npm run check:package');
|
|
14
|
+
const owned = await fs.mkdtemp(path.join(tmpdir(), 'dsh-rollover-package-'));
|
|
15
|
+
const output = path.join(root, 'out');
|
|
16
|
+
try {
|
|
17
|
+
// 调用方已完成 build:check 和发布检查;这里禁用生命周期,确保检查同一份构建产物。
|
|
18
|
+
const packed = JSON.parse((await run(process.execPath, [npm, 'pack', '--json', '--ignore-scripts', '--pack-destination', owned],
|
|
19
|
+
{cwd: root, windowsHide: true, maxBuffer: 4 * 1024 * 1024})).stdout)[0];
|
|
20
|
+
const archive = path.join(owned, path.basename(packed.filename));
|
|
21
|
+
const install = path.join(owned, 'install');
|
|
22
|
+
await fs.mkdir(install);
|
|
23
|
+
await fs.writeFile(path.join(install, 'package.json'), '{"name":"dsh-rollover-package-check","private":true}\n');
|
|
24
|
+
await run(process.execPath, [npm, 'install', archive, '--ignore-scripts', '--no-audit', '--no-fund', '--package-lock=false'],
|
|
25
|
+
{cwd: install, windowsHide: true, maxBuffer: 4 * 1024 * 1024});
|
|
26
|
+
const installed = path.join(install, 'node_modules', 'dsh-session-rollover');
|
|
27
|
+
const manifest = JSON.parse(await fs.readFile(path.join(installed, 'package.json'), 'utf8'));
|
|
28
|
+
const entry = await import(pathToFileURL(path.join(installed, manifest.main)).href);
|
|
29
|
+
assert.equal(entry.default, entry.SessionRolloverPlugin);
|
|
30
|
+
assert.equal(typeof entry.default, 'function');
|
|
31
|
+
const files = packed.files.map(file => file.path);
|
|
32
|
+
for (const required of ['client.js', 'src/server/compatibility.mjs', 'compatibility/policy.json', 'docs/COMPATIBILITY.md']) {
|
|
33
|
+
assert.ok(files.includes(required), `Missing packed resource: ${required}`);
|
|
34
|
+
}
|
|
35
|
+
// 从安装后的目录重新校验全部面板资源和 ModuleLoader,避免只检查源码目录。
|
|
36
|
+
await run(process.execPath, [path.join(installed, 'scripts/check-release.mjs')], {cwd: installed, windowsHide: true});
|
|
37
|
+
const bytes = await fs.readFile(archive);
|
|
38
|
+
await fs.mkdir(output, {recursive: true});
|
|
39
|
+
await fs.copyFile(archive, path.join(output, packed.filename));
|
|
40
|
+
const report = {passed: true, level: 'isolated-package-install', runtimeVerified: false,
|
|
41
|
+
checkedAt: new Date().toISOString(), platform: process.platform, node: process.version,
|
|
42
|
+
name: manifest.name, version: manifest.version, files: files.length,
|
|
43
|
+
sha256: createHash('sha256').update(bytes).digest('hex'), artifact: packed.filename};
|
|
44
|
+
await fs.writeFile(path.join(output, 'package-verification.json'), JSON.stringify(report, null, 2) + '\n');
|
|
45
|
+
console.log(`Package PASS: ${manifest.name}@${manifest.version}; isolated install, exports, client and resources (${files.length} files).`);
|
|
46
|
+
} finally {
|
|
47
|
+
if (path.dirname(path.resolve(owned)) !== path.resolve(tmpdir()) || !path.basename(owned).startsWith('dsh-rollover-package-')) {
|
|
48
|
+
throw new Error('Refusing cleanup outside the owned temporary directory');
|
|
49
|
+
}
|
|
50
|
+
await fs.rm(owned, {recursive: true, force: true});
|
|
51
|
+
}
|
|
@@ -25,7 +25,24 @@ assert.equal(pkg.bugs.url, repo + '/issues');
|
|
|
25
25
|
assert.ok(pkg.keywords.includes('dsh-plugin'));
|
|
26
26
|
assert.equal(pkg.dsh.manifestVersion, 1);
|
|
27
27
|
assert.equal(pkg.dsh.client.platform, 'web');
|
|
28
|
-
|
|
28
|
+
const compatibility = JSON.parse(await readFile(await localFile('compatibility/policy.json'), 'utf8'));
|
|
29
|
+
assert.equal(compatibility.schemaVersion, 2);
|
|
30
|
+
assert.equal(compatibility.baseline, pkg.dsh.verifiedDsh);
|
|
31
|
+
assert.ok(compatibility.verifiedRuntimeVersions.includes(compatibility.baseline));
|
|
32
|
+
assert.ok(compatibility.verifiedRuntimeVersions.every(version => /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(version)));
|
|
33
|
+
// 安装范围由显式策略决定;实机验收名单独立维护,新增验收记录无需收窄范围。
|
|
34
|
+
assert.equal(typeof compatibility.supportedRuntimeRange, 'string');
|
|
35
|
+
assert.ok(compatibility.supportedRuntimeRange.trim());
|
|
36
|
+
assert.equal(pkg.engines.dsh, compatibility.supportedRuntimeRange);
|
|
37
|
+
for (const version of compatibility.verifiedRuntimeVersions.filter(version => version !== compatibility.baseline)) {
|
|
38
|
+
const evidence = JSON.parse(await readFile(await localFile(`compatibility/runtime-${version}.json`), 'utf8'));
|
|
39
|
+
assert.equal(evidence.passed, true);
|
|
40
|
+
assert.equal(evidence.runtimeVerified, true);
|
|
41
|
+
assert.equal(evidence.dsh, version);
|
|
42
|
+
// 保留证据采集时真实的插件版本;旧验收记录不冒充当前发行版验收。
|
|
43
|
+
assert.match(evidence.plugin, /^\d+\.\d+\.\d+$/);
|
|
44
|
+
}
|
|
45
|
+
await localFile('docs/COMPATIBILITY.md');
|
|
29
46
|
assert.deepEqual(Object.keys(pkg.dependencies ?? {}), []);
|
|
30
47
|
assert.equal(pkg.scripts.prepare, undefined);
|
|
31
48
|
assert.equal(pkg.scripts.postinstall, undefined);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** 检查 npm 公共声明中的依赖面;不执行上游包,不把声明扫描当作真实宿主验收。 */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {execFile} from 'node:child_process';
|
|
5
|
+
import {promisify} from 'node:util';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
const run = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
export async function registryJSON(url) {
|
|
11
|
+
const response = await fetch(url, {signal: AbortSignal.timeout(30000)});
|
|
12
|
+
assert.equal(response.status, 200, `Registry unavailable: ${url} (${response.status})`);
|
|
13
|
+
return response.json();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 按官方发布时刻枚举基线之后的每个版本,包含 alpha/rc 和后续补丁。 */
|
|
17
|
+
export function publishedTargets(metadata, baseline) {
|
|
18
|
+
const since = Date.parse(metadata.time?.[baseline]);
|
|
19
|
+
assert.ok(Number.isFinite(since), 'Missing baseline publication time');
|
|
20
|
+
const versions = Object.keys(metadata.versions ?? {});
|
|
21
|
+
assert.ok(versions.includes(baseline), 'Missing baseline package version');
|
|
22
|
+
for (const version of versions) {
|
|
23
|
+
assert.match(version, /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
24
|
+
assert.ok(Number.isFinite(Date.parse(metadata.time?.[version])), `Missing publication time: ${version}`);
|
|
25
|
+
}
|
|
26
|
+
return versions.filter(version => Date.parse(metadata.time?.[version]) >= since)
|
|
27
|
+
.sort((a, b) => Date.parse(metadata.time[a]) - Date.parse(metadata.time[b]));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function inspectDeclarations(text, contract) {
|
|
31
|
+
// 去除注释,避免已删除的方法只在弃用说明中出现时误判通过。
|
|
32
|
+
const source = text.replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g, '');
|
|
33
|
+
const missing = [];
|
|
34
|
+
for (const method of contract.methods ?? []) {
|
|
35
|
+
assert.match(method, /^\w+$/);
|
|
36
|
+
if (!new RegExp(`\\b${method}\\s*(?:<[^;{}]*?>\\s*)?\\(`).test(source)) missing.push(method + '()');
|
|
37
|
+
}
|
|
38
|
+
for (const token of contract.tokens ?? []) if (!source.includes(token)) missing.push(token);
|
|
39
|
+
for (const group of contract.alternatives ?? []) {
|
|
40
|
+
if (!group.some(token => source.includes(token))) missing.push(group.join(' | '));
|
|
41
|
+
}
|
|
42
|
+
return missing;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function checkPackage(contract, version, policy, directory) {
|
|
46
|
+
const metadata = await registryJSON(policy.registry + encodeURIComponent(contract.package) + '/' + encodeURIComponent(version));
|
|
47
|
+
assert.equal(metadata.version, version);
|
|
48
|
+
assert.equal(new URL(metadata.dist.tarball).hostname, new URL(policy.registry).hostname);
|
|
49
|
+
assert.match(metadata.dist.integrity, /^sha512-[A-Za-z0-9+/=]+$/);
|
|
50
|
+
const response = await fetch(metadata.dist.tarball, {signal: AbortSignal.timeout(30000)});
|
|
51
|
+
assert.equal(response.status, 200);
|
|
52
|
+
const archive = Buffer.from(await response.arrayBuffer());
|
|
53
|
+
assert.equal('sha512-' + createHash('sha512').update(archive).digest('base64'), metadata.dist.integrity);
|
|
54
|
+
const file = path.join(directory, contract.package.replace(/[^\w-]/g, '_') + '.tgz');
|
|
55
|
+
await fs.writeFile(file, archive);
|
|
56
|
+
const {stdout} = await run('tar', ['-tzf', file], {windowsHide: true, maxBuffer: 4 * 1024 * 1024});
|
|
57
|
+
const declarations = stdout.split(/\r?\n/).filter(name => /^package\/lib\/.*\.d\.ts$/.test(name));
|
|
58
|
+
assert.ok(declarations.length > 0, 'Missing public type declarations');
|
|
59
|
+
let text = '';
|
|
60
|
+
for (let offset = 0; offset < declarations.length; offset += 40) {
|
|
61
|
+
const names = declarations.slice(offset, offset + 40);
|
|
62
|
+
assert.ok(names.every(name => !name.split('/').includes('..') && !name.includes('\\')));
|
|
63
|
+
// 只读 tar stdout,不把上游归档解压到任何宿主目录。
|
|
64
|
+
text += (await run('tar', ['-xOf', file, ...names], {windowsHide: true, maxBuffer: 16 * 1024 * 1024})).stdout + '\n';
|
|
65
|
+
}
|
|
66
|
+
return {package: contract.package, version, integrity: metadata.dist.integrity,
|
|
67
|
+
declarations: declarations.length,
|
|
68
|
+
missing: inspectDeclarations(text, {...contract, ...contract.versionOverrides?.[version]})};
|
|
69
|
+
}
|
|
@@ -5,6 +5,18 @@ export function createRolloverUI(React) {
|
|
|
5
5
|
const entries = new Map();
|
|
6
6
|
let navigate = null;
|
|
7
7
|
let renderInline = false;
|
|
8
|
+
const clientWarnings = new Set();
|
|
9
|
+
function unavailable(feature, error) {
|
|
10
|
+
clientWarnings.add(feature);
|
|
11
|
+
console.warn('[dsh-session-rollover] client capability unavailable:', feature, error?.message ?? '');
|
|
12
|
+
}
|
|
13
|
+
function inject(ctx, names, setup) {
|
|
14
|
+
try {
|
|
15
|
+
ctx.inject(names, scope => {
|
|
16
|
+
try { return setup(scope); } catch (error) { unavailable(names.join(','), error); }
|
|
17
|
+
});
|
|
18
|
+
} catch (error) { unavailable(names.join(','), error); }
|
|
19
|
+
}
|
|
8
20
|
|
|
9
21
|
async function request(sessionId, action) {
|
|
10
22
|
const response = await fetch(action ? API : `${API}?session=${encodeURIComponent(sessionId)}`, action ? {
|
|
@@ -122,8 +134,8 @@ export function createRolloverUI(React) {
|
|
|
122
134
|
h('p', {className: 'dsr-help-credit'}, 'dsh-session-rollover · Oak-liuhj')) : null,
|
|
123
135
|
h('div', {className: 'dsr-actions-body'},
|
|
124
136
|
h('button', {type: 'button', 'aria-expanded': showHelp, onClick: () => setShowHelp(!showHelp)}, showHelp ? '收起使用说明' : '使用说明'),
|
|
125
|
-
h('button', {type: 'button', disabled: busy || info.sealed || !navigate, onClick: () => action('rollover')}, '现在换会话'),
|
|
126
|
-
h('button', {type: 'button', disabled: busy || !navigate, onClick: () => action('new')}, '新建独立会话'),
|
|
137
|
+
h('button', {type: 'button', disabled: busy || info.sealed || !navigate || info.compatibility?.compatible === false, onClick: () => action('rollover')}, '现在换会话'),
|
|
138
|
+
h('button', {type: 'button', disabled: busy || !navigate || info.compatibility?.compatible === false, onClick: () => action('new')}, '新建独立会话'),
|
|
127
139
|
h('button', {type: 'button', onClick: () => dialog.current?.close()}, '关闭'))),
|
|
128
140
|
error ? h('span', {role: 'alert'}, error) : null);
|
|
129
141
|
}
|
|
@@ -183,6 +195,8 @@ export function createRolloverUI(React) {
|
|
|
183
195
|
if (!info) return null;
|
|
184
196
|
return h('div', {className: 'dsr-dock'},
|
|
185
197
|
info.error ? h('div', {role: 'alert'}, info.error) : null,
|
|
198
|
+
info.compatibility?.compatible === false ? h('div', {role: 'alert', className: 'dsr-warning'}, info.compatibility.message) : null,
|
|
199
|
+
clientWarnings.size ? h('div', {role: 'alert', className: 'dsr-warning'}, '部分会话界面功能与当前 DSH 不兼容,请更新插件。') : null,
|
|
186
200
|
h(ReadonlyControl, {key: props.sessionId, info}),
|
|
187
201
|
info.capped ? h('div', {role: 'status', className: 'dsr-warning'}, '同簇已达 9 卷上限(9/9),请通过会话操作新建独立会话。') : null,
|
|
188
202
|
info.retryLimitReached ? h('div', {role: 'status', className: 'dsr-warning'}, '摘要生成失败,已停止自动重试。可在修正内容后手动换会话。') : null,
|
|
@@ -200,7 +214,9 @@ export function createRolloverUI(React) {
|
|
|
200
214
|
|
|
201
215
|
/** 释放时仅撤销自己的禁用;保留模型选择等其他插件后来设置的阻塞原因。 */
|
|
202
216
|
function blockComposer(blocks, id, reason) {
|
|
217
|
+
if (typeof blocks?.storeFor !== 'function' || typeof blocks?.set !== 'function') throw new Error('composer blocks unavailable');
|
|
203
218
|
const store = blocks.storeFor(id);
|
|
219
|
+
if (typeof store?.getSnapshot !== 'function' || typeof store?.subscribe !== 'function') throw new Error('composer store unavailable');
|
|
204
220
|
let previous = store.getSnapshot();
|
|
205
221
|
const own = {reason};
|
|
206
222
|
let writing = false;
|
|
@@ -222,13 +238,15 @@ export function createRolloverUI(React) {
|
|
|
222
238
|
style.setAttribute('data-dsh-plugin', 'dsh-session-rollover-ui');
|
|
223
239
|
document.head.appendChild(style);
|
|
224
240
|
ctx.effect?.(() => () => { style.remove(); for (const item of entries.values()) item.stop?.(); entries.clear(); });
|
|
225
|
-
|
|
241
|
+
inject(ctx, ['uiWorkspace'], scope => {
|
|
242
|
+
if (typeof scope.uiWorkspace?.openSession !== 'function') throw new Error('session navigation unavailable');
|
|
226
243
|
navigate = id => scope.uiWorkspace.openSession(id);
|
|
227
244
|
scope.effect?.(() => () => { navigate = null; });
|
|
228
245
|
});
|
|
229
|
-
|
|
246
|
+
inject(ctx, ['slots'], scope => {
|
|
230
247
|
const slots = scope.slots;
|
|
231
|
-
|
|
248
|
+
if (typeof slots?.inject !== 'function' || typeof slots?.register !== 'function') throw new Error('slots unavailable');
|
|
249
|
+
const mount = (name, Component, options = {}, owner = scope) => owner.effect(() => owner.slots.inject(name, () => owner.slots.register({name,
|
|
232
250
|
id: 'dsh-session-rollover', order: 35, ...options}, Component)));
|
|
233
251
|
mount('conversation.session.header.actions', Header);
|
|
234
252
|
mount('conversation.input.dock', Dock);
|
|
@@ -237,20 +255,27 @@ export function createRolloverUI(React) {
|
|
|
237
255
|
mount('sidebar.session.row.leading', props => slots.snapshot?.('sidebar.session.row.trailing')?.length
|
|
238
256
|
? null : h(Badges, {...props, compact: true}));
|
|
239
257
|
// 使用宿主公开 composer block 服务;覆盖输入组件会破坏其子槽授权。
|
|
240
|
-
|
|
258
|
+
inject(scope, ['conversation'], inputScope => {
|
|
259
|
+
if (typeof inputScope.conversation?.blocks?.storeFor !== 'function' || typeof inputScope.conversation?.blocks?.set !== 'function') {
|
|
260
|
+
throw new Error('composer blocks unavailable');
|
|
261
|
+
}
|
|
241
262
|
function ComposerGuard(props) {
|
|
242
263
|
const info = useInfo(props.sessionId);
|
|
243
264
|
const reason = !info ? '正在读取会话状态…' : info.readonly
|
|
244
265
|
? '归档会话:不可输入,可通过上方开关恢复写入。' : info.error;
|
|
245
|
-
useEffect(() =>
|
|
246
|
-
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (props.sessionId && reason) {
|
|
268
|
+
try { return blockComposer(inputScope.conversation.blocks, props.sessionId, reason); }
|
|
269
|
+
catch (error) { unavailable('conversation.blocks', error); }
|
|
270
|
+
}
|
|
271
|
+
},
|
|
247
272
|
[props.sessionId, reason]);
|
|
248
273
|
return null;
|
|
249
274
|
}
|
|
250
275
|
inputScope.effect(() => slots.inject('conversation.input.dock', () => slots.register({
|
|
251
276
|
name: 'conversation.input.dock', id: 'dsr-readonly-guard', order: 34}, ComposerGuard)));
|
|
252
277
|
});
|
|
253
|
-
|
|
278
|
+
inject(scope, ['uiConversation'], textScope => {
|
|
254
279
|
if (!textScope.uiConversation.userText?.register) return;
|
|
255
280
|
textScope.effect(() => textScope.uiConversation.userText.register({id: 'dsr-handoff', order: -100,
|
|
256
281
|
match(text, from) {
|
|
@@ -258,8 +283,10 @@ export function createRolloverUI(React) {
|
|
|
258
283
|
const payload = parseHandoff(text);
|
|
259
284
|
return payload ? {id: 'dsr-handoff', start: 0, end: text.length, data: payload} : undefined;
|
|
260
285
|
}}));
|
|
286
|
+
// 插槽必须随 uiConversation 的注入子作用域释放,不能挂在较长寿命的 slots 父作用域。
|
|
287
|
+
// 新宿主重连/热替换领域服务时会重建该子作用域;挂在父级会重复占用同一个 keyed slot。
|
|
261
288
|
mount('conversation.user.text.node', props => h(Card, {payload: props.node.data,
|
|
262
|
-
source: props.node.data.from_session_id && !props.node.data.cluster?.independent ? {sessionId: props.node.data.from_session_id, title: props.node.data.from_title ?? '上一卷'} : null}), {key: 'dsr-handoff'});
|
|
289
|
+
source: props.node.data.from_session_id && !props.node.data.cluster?.independent ? {sessionId: props.node.data.from_session_id, title: props.node.data.from_title ?? '上一卷'} : null}), {key: 'dsr-handoff'}, textScope);
|
|
263
290
|
renderInline = true;
|
|
264
291
|
textScope.effect?.(() => () => { renderInline = false; });
|
|
265
292
|
});
|
|
@@ -280,5 +307,5 @@ body[data-ds-dark-theme] :is(.dsr-header,.dsr-dock,.dsr-card,.dsr-row-badges),ht
|
|
|
280
307
|
@media(max-width:600px){.dsr-header{gap:3px;flex-wrap:nowrap;max-width:calc(100vw - 156px)}.dsr-header>.dsr-chip,.dsr-header>.dsr-source{display:none}.dsr-header>.dsr-chip:first-child{display:inline-flex;font-size:10px;padding:1px 3px}}
|
|
281
308
|
@media(prefers-reduced-motion:reduce){.dsr-card *{animation:none!important;transition:none!important}}
|
|
282
309
|
`;
|
|
283
|
-
return {apply, Header, Dock, Card, Badges, useInfo, parseHandoff, publish, blockComposer, CSS};
|
|
310
|
+
return {apply, Header, Dock, Card, Badges, useInfo, parseHandoff, publish, blockComposer, clientWarnings, CSS};
|
|
284
311
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// 只探测已声明的宿主服务;不根据版本号猜测方法签名,也不在探测时写会话。
|
|
2
|
+
const REQUIRED = {
|
|
3
|
+
sessions: ['get', 'list'],
|
|
4
|
+
sessionTitle: ['get', 'rename'],
|
|
5
|
+
sessionController: ['create', 'prompt', 'selectModel'],
|
|
6
|
+
sessionProjections: ['stateOf'],
|
|
7
|
+
workspaceRegistry: ['list'],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const INCOMPATIBLE_MESSAGE = '当前 DSH 的会话接口与插件不兼容,已暂停分卷。原会话与归档数据保留,请更新插件后重试。';
|
|
11
|
+
|
|
12
|
+
/** 方法存在只代表能力探测通过;事件语义、权限和界面仍需逐版本验收。 */
|
|
13
|
+
export function inspectHost(ctx, session) {
|
|
14
|
+
const missing = [];
|
|
15
|
+
for (const [service, methods] of Object.entries(REQUIRED)) {
|
|
16
|
+
for (const method of methods) {
|
|
17
|
+
try { if (typeof ctx[service]?.[method] !== 'function') missing.push(`${service}.${method}`); }
|
|
18
|
+
catch { missing.push(`${service}.${method}`); }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (session && typeof session.snapshotEvents !== 'function') missing.push('session.snapshotEvents');
|
|
22
|
+
return {contractVersion: 1, compatible: missing.length === 0, missing,
|
|
23
|
+
message: missing.length ? INCOMPATIBLE_MESSAGE : null};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 在创建新卷之前检查同步读取面的形状;遇到变化时停止,不用空数据继续交接。 */
|
|
27
|
+
export function preflightHost(ctx, session, services = ctx) {
|
|
28
|
+
const report = inspectHost(ctx, session);
|
|
29
|
+
if (!report.compatible) return report;
|
|
30
|
+
try {
|
|
31
|
+
if (!Array.isArray(session.snapshotEvents())) throw new Error('session.snapshotEvents:array-required');
|
|
32
|
+
const sessions = ctx.sessions.list();
|
|
33
|
+
if (!Array.isArray(sessions) || sessions.some(s => !s || typeof s.id !== 'string' || !s.id)) {
|
|
34
|
+
throw new Error('sessions.list:session-id-required');
|
|
35
|
+
}
|
|
36
|
+
const workspaces = ctx.workspaceRegistry.list();
|
|
37
|
+
if (!Array.isArray(workspaces) || workspaces.some(w => !w || typeof w.id !== 'string' || !w.id ||
|
|
38
|
+
!Array.isArray(w.sessionIds) || w.sessionIds.some(id => typeof id !== 'string'))) {
|
|
39
|
+
throw new Error('workspaceRegistry.list:sessionIds-required');
|
|
40
|
+
}
|
|
41
|
+
const title = ctx.sessionTitle.get(session);
|
|
42
|
+
if (title != null && (typeof title !== 'object' || typeof title.title !== 'string')) {
|
|
43
|
+
throw new Error('sessionTitle.get:title-required');
|
|
44
|
+
}
|
|
45
|
+
const cwd = readWorkingDirectory(ctx, services, session);
|
|
46
|
+
if (cwd != null && typeof cwd !== 'string') throw new Error('workingDirectory:string-required');
|
|
47
|
+
readModelSelection(ctx, services, session);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return {...report, compatible: false, missing: [String(error?.message ?? error)], message: INCOMPATIBLE_MESSAGE};
|
|
50
|
+
}
|
|
51
|
+
return report;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 新版优先使用领域服务;旧宿主回退到已验证的公开投影入口。 */
|
|
55
|
+
export function readWorkingDirectory(ctx, services, session) {
|
|
56
|
+
return typeof services.workingDirectory?.get === 'function'
|
|
57
|
+
? services.workingDirectory.get(session)
|
|
58
|
+
: ctx.sessionProjections.stateOf(session, 'workingDirectory');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateModel(selection) {
|
|
62
|
+
if (selection != null && (typeof selection.provider !== 'string' || typeof selection.model !== 'string' ||
|
|
63
|
+
!selection.provider || !selection.model ||
|
|
64
|
+
(selection.reasoningEffort != null && typeof selection.reasoningEffort !== 'string'))) {
|
|
65
|
+
throw new Error('modelSelection:provider-model-required');
|
|
66
|
+
}
|
|
67
|
+
return selection;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function readModelSelection(ctx, services, session) {
|
|
71
|
+
const agent = services.agents?.get?.(session.id);
|
|
72
|
+
// 旧宿主也可能提供 agents,但尚无 model.current;此时仍走经过结构校验的投影入口。
|
|
73
|
+
if (typeof agent?.model?.current === 'function') {
|
|
74
|
+
const current = validateModel(agent.model.current());
|
|
75
|
+
if (current != null) return current;
|
|
76
|
+
}
|
|
77
|
+
const selection = ctx.sessionProjections.stateOf(session, 'modelSelection');
|
|
78
|
+
if (selection == null) return undefined;
|
|
79
|
+
if (typeof selection !== 'object' || Array.isArray(selection) ||
|
|
80
|
+
!('pending' in selection || 'lastUsed' in selection)) throw new Error('modelSelection:projection-shape-required');
|
|
81
|
+
validateModel(selection.pending);
|
|
82
|
+
validateModel(selection.lastUsed);
|
|
83
|
+
return selection.pending ?? selection.lastUsed;
|
|
84
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {eventType} from '../domain/turns.mjs';
|
|
3
3
|
import {openToolCalls} from '../domain/inflight.mjs';
|
|
4
4
|
import {lastTodos} from '../domain/todos.mjs';
|
|
5
|
+
import {preflightHost, readWorkingDirectory, readModelSelection} from './compatibility.mjs';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 为一个来源会话创建适配器。新卷继承其工作区和模型选择。
|
|
@@ -14,6 +15,8 @@ import {lastTodos} from '../domain/todos.mjs';
|
|
|
14
15
|
* @returns {object} performRollover 所需的异步宿主接口
|
|
15
16
|
*/
|
|
16
17
|
export function createHostAdapter({ctx, session, state, sessionOf, log, services = ctx}) {
|
|
18
|
+
const compatibility = preflightHost(ctx, session, services);
|
|
19
|
+
if (!compatibility.compatible) throw new Error(`host-incompatible: ${compatibility.missing.join(', ')}`);
|
|
17
20
|
const id = session.id;
|
|
18
21
|
const sourceEvents = session.snapshotEvents().slice();
|
|
19
22
|
const registry = services.jobs;
|
|
@@ -101,16 +104,17 @@ export function createHostAdapter({ctx, session, state, sessionOf, log, services
|
|
|
101
104
|
},
|
|
102
105
|
createSession: async () => {
|
|
103
106
|
// 优先归入来源工作区;来源不属于工作区时才按其 cwd 建会话。
|
|
104
|
-
const cwd = ctx
|
|
107
|
+
const cwd = readWorkingDirectory(ctx, services, session);
|
|
105
108
|
const workspace = ctx.workspaceRegistry.list().find(w => w.sessionIds.includes(session.id));
|
|
106
|
-
|
|
109
|
+
const created = await ctx.sessionController.create(workspace ? {workspaceId: workspace.id} : typeof cwd === 'string' ? {cwd} : {});
|
|
110
|
+
if (!/^[\w-]{1,160}$/.test(created?.sessionId ?? '')) throw new Error('host-incompatible: create.sessionId');
|
|
111
|
+
return created;
|
|
107
112
|
},
|
|
108
113
|
injectFirstMessage: async (sessionId, text) => {
|
|
109
114
|
// 宿主 create() 建的是空壳会话,没有 model selection,会落到默认 provider
|
|
110
115
|
// (实测 "deepseek-official" 无 key → MISSING_CREDENTIAL)。先继承旧卷的模型,再注入。
|
|
111
116
|
try {
|
|
112
|
-
const
|
|
113
|
-
const pick = sel?.pending ?? sel?.lastUsed;
|
|
117
|
+
const pick = readModelSelection(ctx, services, session);
|
|
114
118
|
if (pick?.provider && pick?.model) {
|
|
115
119
|
await ctx.sessionController.selectModel({
|
|
116
120
|
sessionId, provider: pick.provider, model: pick.model,
|
|
@@ -54,6 +54,12 @@ export async function handleSessionRequest(plugin, req, res) {
|
|
|
54
54
|
try { input = JSON.parse(body); } catch { return send(400, {error: 'invalid-json'}); }
|
|
55
55
|
if (!/^[\w-]{1,160}$/.test(input?.sessionId ?? '')) return send(400, {error: 'invalid-session'});
|
|
56
56
|
if (!plugin.sessionOf(input.sessionId)) return send(404, {error: 'session-unavailable'});
|
|
57
|
+
if (['rollover', 'new'].includes(input.action)) {
|
|
58
|
+
const compatibility = plugin.handoffInfo(input.sessionId).compatibility;
|
|
59
|
+
if (compatibility && !compatibility.compatible) {
|
|
60
|
+
return send(503, {error: 'host-incompatible', message: compatibility.message, compatibility});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
57
63
|
if (input.action === 'readonly' && typeof input.readonly === 'boolean') {
|
|
58
64
|
if (!plugin.handoffInfo(input.sessionId).sealed) return send(409, {error: 'session-not-archived'});
|
|
59
65
|
return send(200, plugin.setReadonly(input.sessionId, input.readonly));
|
package/src/server/plugin.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {transitionInFlight} from '../domain/mode.mjs';
|
|
|
11
11
|
import {createNoticeStore} from '../storage/notices.mjs';
|
|
12
12
|
import {createHostAdapter} from './host-adapter.mjs';
|
|
13
13
|
import {attachNoticeRoutes, handlePendingNotices} from './notice-route.mjs';
|
|
14
|
+
import {inspectHost, preflightHost, INCOMPATIBLE_MESSAGE} from './compatibility.mjs';
|
|
14
15
|
|
|
15
16
|
const inflightKey = (id) => `rollover:${id}`;
|
|
16
17
|
|
|
@@ -27,6 +28,7 @@ const NOTICE_TEXT = {
|
|
|
27
28
|
'cluster-cap': '同簇已达 9 卷上限,请新建独立会话继续。',
|
|
28
29
|
'storage-unavailable': '分卷数据目录不可写,已保留原会话,请检查目录后重试。',
|
|
29
30
|
'user-renamed': '已保留手动设置的会话名称。',
|
|
31
|
+
'host-incompatible': INCOMPATIBLE_MESSAGE,
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -52,9 +54,10 @@ export default class SessionRolloverPlugin {
|
|
|
52
54
|
this.ctx = ctx;
|
|
53
55
|
this.services = typeof ctx.inject === 'function' ? {} : ctx;
|
|
54
56
|
if (ctx.inject) {
|
|
55
|
-
for (const name of ['jobs', 'agentDefaultModel']) ctx.inject([name], scope => {
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
for (const name of ['jobs', 'agentDefaultModel', 'workingDirectory', 'agents']) ctx.inject([name], scope => {
|
|
58
|
+
const service = scope[name];
|
|
59
|
+
this.services[name] = service;
|
|
60
|
+
scope.effect?.(() => () => { if (this.services[name] === service) delete this.services[name]; });
|
|
58
61
|
});
|
|
59
62
|
}
|
|
60
63
|
this.config = {mode: 'auto', threshold: DEFAULT_THRESHOLD, ...config};
|
|
@@ -72,6 +75,7 @@ export default class SessionRolloverPlugin {
|
|
|
72
75
|
rollover: (id) => this.rolloverNow(id),
|
|
73
76
|
info: (id) => this.handoffInfo(id),
|
|
74
77
|
setReadonly: (id, value) => this.setReadonly(id, value),
|
|
78
|
+
compatibility: () => inspectHost(this.ctx),
|
|
75
79
|
});
|
|
76
80
|
ctx.effect(() => () => {
|
|
77
81
|
this.busy.clear();
|
|
@@ -87,6 +91,7 @@ export default class SessionRolloverPlugin {
|
|
|
87
91
|
this.log('boot', {
|
|
88
92
|
dataDir: this.dataDir, mode: this.config.mode, threshold: this.config.threshold,
|
|
89
93
|
degraded: this.persistence.degraded, notices: this.notices.path, noticesDegraded: this.notices.degraded,
|
|
94
|
+
compatibility: inspectHost(ctx),
|
|
90
95
|
});
|
|
91
96
|
}
|
|
92
97
|
|
|
@@ -126,13 +131,21 @@ export default class SessionRolloverPlugin {
|
|
|
126
131
|
|
|
127
132
|
/** 启动修复缓存缺项;失败注入产生的待清理卷不参与有效父链。 */
|
|
128
133
|
reconcileIndex() {
|
|
134
|
+
if (!inspectHost(this.ctx).compatible) return;
|
|
135
|
+
let sessions;
|
|
136
|
+
try { sessions = this.ctx.sessions.list(); } catch { return; }
|
|
137
|
+
if (!Array.isArray(sessions)) return;
|
|
129
138
|
let changed = false;
|
|
130
139
|
const abandoned = new Set(this.state.pendingList().map(r => r.sessionId));
|
|
131
140
|
const snapshots = new Map();
|
|
132
|
-
for (const session of
|
|
141
|
+
for (const session of sessions) {
|
|
142
|
+
if (!session || typeof session.id !== 'string') continue;
|
|
133
143
|
if (abandoned.has(session.id)) continue;
|
|
134
144
|
let h;
|
|
135
|
-
|
|
145
|
+
let events;
|
|
146
|
+
try { events = session.snapshotEvents(); } catch { continue; }
|
|
147
|
+
if (!Array.isArray(events)) continue;
|
|
148
|
+
for (const e of events) {
|
|
136
149
|
const text = textOf(e);
|
|
137
150
|
if (!text.startsWith('【分卷交接】')) continue;
|
|
138
151
|
try { h = JSON.parse(/```json\s*([\s\S]*?)```/.exec(text)?.[1]); } catch { /* 非本插件快照 */ }
|
|
@@ -204,20 +217,24 @@ export default class SessionRolloverPlugin {
|
|
|
204
217
|
const parent = handoff?.cluster?.independent ? null : handoff?.from_session_id ?? record?.parent_session_id;
|
|
205
218
|
let title = handoff?.from_title || parent;
|
|
206
219
|
const source = parent && this.sessionOf(parent);
|
|
207
|
-
if (source) title = this.ctx.sessionTitle.get(source)?.title || title;
|
|
220
|
+
if (source) { try { title = this.ctx.sessionTitle.get(source)?.title || title; } catch {} }
|
|
208
221
|
const session = this.sessionOf(id);
|
|
209
|
-
|
|
222
|
+
let events = [];
|
|
223
|
+
try { const snapshot = session?.snapshotEvents?.(); if (Array.isArray(snapshot)) events = snapshot; } catch {}
|
|
224
|
+
let sessionTitle = '';
|
|
225
|
+
try { sessionTitle = session ? this.ctx.sessionTitle.get(session)?.title ?? '' : ''; } catch {}
|
|
210
226
|
const sealed = record?.rollover_state === 'sealed';
|
|
211
227
|
const turnCount = countTurns(events);
|
|
212
228
|
const children = record ? this.index.cluster(record.cluster_id).filter(r => r.parent_session_id === id) : [];
|
|
213
229
|
const retry = this.state.retryGet(id).attempts;
|
|
214
230
|
return {source: parent ? {sessionId: parent, title} : null, handoff,
|
|
215
231
|
readonly: this.isReadonly(id), sealed, sessionId: id,
|
|
216
|
-
title:
|
|
232
|
+
title: sessionTitle,
|
|
233
|
+
compatibility: session ? preflightHost(this.ctx, session, this.services) : inspectHost(this.ctx),
|
|
217
234
|
turnCount, threshold: this.config.threshold, volume: record?.vol_no ?? null, cap: 9,
|
|
218
235
|
capped: record?.vol_no >= 9, summaryRetry: retry,
|
|
219
236
|
retryLimitReached: retry >= 3, degraded: this.persistence.degraded || !!this.stateDegraded?.has(id),
|
|
220
|
-
previousTurnCount:
|
|
237
|
+
previousTurnCount: source ? (() => { try { const e = source.snapshotEvents(); return Array.isArray(e) ? countTurns(e) : null; } catch { return null; } })() : null,
|
|
221
238
|
nextSessionId: children[0]?.sessionId ?? null};
|
|
222
239
|
}
|
|
223
240
|
|
|
@@ -241,6 +258,8 @@ export default class SessionRolloverPlugin {
|
|
|
241
258
|
async rolloverNow(id) {
|
|
242
259
|
const session = this.sessionOf(id);
|
|
243
260
|
if (!session) return {status: 'failed', reason: 'session-unavailable'};
|
|
261
|
+
const compatibility = preflightHost(this.ctx, session, this.services);
|
|
262
|
+
if (!compatibility.compatible) return {status: 'failed', reason: 'host-incompatible', compatibility};
|
|
244
263
|
if (this.busy.has(inflightKey(id))) return {status: 'skipped', reason: 'busy'};
|
|
245
264
|
if (this.index.get(id)?.rollover_state === 'sealed') return {status: 'skipped', reason: 'readonly'};
|
|
246
265
|
this.busy.add(inflightKey(id));
|
|
@@ -300,6 +319,11 @@ export default class SessionRolloverPlugin {
|
|
|
300
319
|
const session = agent?.session;
|
|
301
320
|
if (!session?.id) return;
|
|
302
321
|
const id = session.id;
|
|
322
|
+
const compatibility = preflightHost(this.ctx, session, this.services);
|
|
323
|
+
if (!compatibility.compatible) {
|
|
324
|
+
this.notifyOnce(session, {reason: 'host-incompatible'});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
303
327
|
if (this.busy.has(inflightKey(id))) return;
|
|
304
328
|
// 已封卷的 -N 永不再分卷:B9 的手动开关只放行「在旧卷继续写」,不等于再开新卷。
|
|
305
329
|
try {
|