mingdao-harness 0.6.43 → 0.6.45
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/docs/CODE-SIGNING.md +19 -8
- package/docs/CONFIG.md +9 -1
- package/package.json +1 -1
- package/src/commands/repl.js +2 -2
- package/src/config.js +46 -7
- package/src/session-events.js +33 -0
- package/src/session.js +63 -3
- package/src/web/routes/domains/sessions.js +2 -2
- package/src/web/server.js +1 -1
package/docs/CODE-SIGNING.md
CHANGED
|
@@ -100,14 +100,20 @@ macOS 作业的 `--mac dmg` 在检测到 `CSC_LINK` + Apple 凭证时自动完
|
|
|
100
100
|
> 两者互不替代:代码签名不能阻止应用从被投毒的更新源装一个**同样签了名**的包,来源签名也不负责
|
|
101
101
|
> 消除 SmartScreen 提示。
|
|
102
102
|
>
|
|
103
|
-
>
|
|
104
|
-
> `
|
|
105
|
-
>
|
|
106
|
-
>
|
|
107
|
-
>
|
|
108
|
-
>
|
|
109
|
-
>
|
|
110
|
-
>
|
|
103
|
+
> **当前状态(v0.6.45 起,已接入)**:签名由 CI 产出(`scripts/sign-release-assets.mjs` → GitHub Release),
|
|
104
|
+
> `scripts/publish-update-sigs.mjs <版本>` 再把每版的 `<安装包>.sig` 发到
|
|
105
|
+
> `https://harness.mingdao.ai/updates/`。客户端取到签名并用**内置公钥**(密钥环,0.6.36 起内置
|
|
106
|
+
> `current` 公钥)验证,通过即走 `install`,**不再弹"未验证来源签名"**。
|
|
107
|
+
>
|
|
108
|
+
> 已实测(2026-11-08):`latest.yml` → `mingdao-setup-0.6.44-x64.exe`(80,202,343 字节)
|
|
109
|
+
> → `/updates/mingdao-setup-0.6.44-x64.exe.sig` → 用 `current` 公钥验签**通过**。
|
|
110
|
+
>
|
|
111
|
+
> 仍是**默认档 `warn-and-install`** 的原因:签名缺失(发布侧故障 / 网络取不到 / feed 被剥离签名)
|
|
112
|
+
> 时宁可提示后放行,也不把用户挡在更新之外。要 fail-closed,设 `MINGDAO_REQUIRE_UPDATE_SIGNATURE=1`。
|
|
113
|
+
>
|
|
114
|
+
> **客户端的版本门槛**:`current` 公钥是 **0.6.36** 起内置的。0.6.36+ 的客户端能验签;
|
|
115
|
+
> **≤0.6.35 的客户端**会看到"更新包来源不可信,已拒绝"(它们只认 `legacy-2025` 那把已丢弃私钥的公钥),
|
|
116
|
+
> 需要手动下载一次。回滚方式:删掉源站 `/updates/*.sig`,立即回到"提示后照常安装"。
|
|
111
117
|
|
|
112
118
|
### 5.1 为什么不是"再算一次 sha512"
|
|
113
119
|
|
|
@@ -149,6 +155,11 @@ node scripts/update-sign.mjs --sign desktop/dist/mingdao-<ver>-x86_64.AppImage
|
|
|
149
155
|
node scripts/update-sign.mjs --verify desktop/dist/mingdao-setup-<ver>-x64.exe --pub update-signing-pub.pem
|
|
150
156
|
|
|
151
157
|
# ④ 上传:安装包 + 它的 .sig **一起**放进 feed 目录(官网 /updates/,与 latest*.yml 同级)
|
|
158
|
+
# 现实做法(v0.6.45 起):CI 已把 .sig 传进 Release,用脚本一次性搬到 feed 目录:
|
|
159
|
+
MINGDAO_GITHUB_TOKEN=... MDH_SITE_HOST=mingdao-server \
|
|
160
|
+
node scripts/publish-update-sigs.mjs <版本> # 加 --dry-run 只列计划
|
|
161
|
+
# · 只发**本版安装包**的签名;`latest*.yml.sig` 不进 /updates/(否则 `rm *.sig` 的回滚语义会变模糊)
|
|
162
|
+
# · 镜像的 /updates 代理到源站,无需重复上传
|
|
152
163
|
```
|
|
153
164
|
|
|
154
165
|
判据与文案在 `desktop/update-verify.js`(纯函数,`test/smoke.js` §135 逐条钉边界):
|
package/docs/CONFIG.md
CHANGED
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
可选字段:`temperature`、`maxOutputTokens`、`includeUsage`(流式请求 usage 统计,个别网关不支持
|
|
29
29
|
`stream_options` 时设 `false`)、`autoTitle`(自动生成会话标题,默认开)、`notify`(任务桌面通知,默认开)、
|
|
30
30
|
`autoCompact`(上下文自动压缩,默认开,见下)、`audit`(工具调用审计,默认开,见下)、
|
|
31
|
-
`eventLog
|
|
31
|
+
`eventLog`(会话事件日志双写,默认开,见下)、`eventSource`(会话读取真源,`messages`(默认)/
|
|
32
|
+
`events`,见下——**灰度开关,风险提示**:`events` 会用事件日志派生消息,一旦与消息文件对拍不一致
|
|
33
|
+
就 warn 并回退到消息数组;只在验证「事件流能否当唯一真源」时临时开启,**不要长期依赖**)。
|
|
32
34
|
|
|
33
35
|
### WebUI 设置项的「默认值」列(只读,不落盘)
|
|
34
36
|
|
|
@@ -105,6 +107,12 @@ MingDao 先用 executor 模型(路由关闭时为当前模型)把被裁段
|
|
|
105
107
|
resume / 空会话 / `tool_calls` 等场景与消息数组 deepEqual。
|
|
106
108
|
- **回滚开关**:`"eventLog": false`(默认开)或环境变量 `MINGDAO_EVENT_LOG=0`。关闭后**完全**
|
|
107
109
|
停止事件写入(含 `message.append` 与 `messages.replace` 双写),会话文件照常写入。
|
|
110
|
+
- **读取真源(灰度开关,默认 `messages`)**:`"eventSource": "events"` 时 `loadSession()` 尝试用事件日志
|
|
111
|
+
重放派生消息;只有 `session.start.data.format >= 2` **且**与 `.jsonl` 消息数组逐条对拍一致才
|
|
112
|
+
采用。日志缺失 / 格式戳缺失或过旧 / 读失败 → **静默**回退消息数组(灰度期常态,不刷 warn);
|
|
113
|
+
对拍不一致 → warn 一行(含文件与首个差异位置:`第 N 条`)并回退。返回值形状不变。默认
|
|
114
|
+
`"messages"` 保持只读消息数组、与 0.6.x 逐字节一致;**翻默认要等 `scripts/event-parity.mjs`
|
|
115
|
+
的真实会话证据(不再有 incomplete)**,不要仅凭配置开启就认为事件已是唯一真源。
|
|
108
116
|
- **兼容**:新增类型只追加、不改变既有事件形状;读侧对未知 type 一律跳过并计入 `skipped`,
|
|
109
117
|
因此旧版本读到 `messages.replace` 只会当它不存在,旧日志(没有该类型)在新版本照常可读。
|
|
110
118
|
旧会话没有 events 文件时读取返回空流;事件侧车不会出现在会话列表/最近会话中;事件文件不
|
package/package.json
CHANGED
package/src/commands/repl.js
CHANGED
|
@@ -164,7 +164,7 @@ export async function runRepl(ctx) {
|
|
|
164
164
|
'选择要恢复的会话:',
|
|
165
165
|
list.map((s) => ({ value: s.file, label: `${relativeTime(s.mtime)} · ${sessionPreview(s.file)}` }))
|
|
166
166
|
);
|
|
167
|
-
const loaded = loadSession(choice);
|
|
167
|
+
const loaded = loadSession(choice, cfg);
|
|
168
168
|
if (loaded.messages.length) {
|
|
169
169
|
session = loaded;
|
|
170
170
|
io.print(style(`✓ 已载入会话 ${path.basename(choice)}(${loaded.messages.length} 条消息)`, C.green));
|
|
@@ -174,7 +174,7 @@ export async function runRepl(ctx) {
|
|
|
174
174
|
if (!session && opts.continueSession) {
|
|
175
175
|
const latest = latestSession(home);
|
|
176
176
|
if (latest) {
|
|
177
|
-
const loaded = loadSession(latest.file);
|
|
177
|
+
const loaded = loadSession(latest.file, cfg);
|
|
178
178
|
if (loaded.messages.length) {
|
|
179
179
|
session = loaded;
|
|
180
180
|
io.print(style(`已载入会话 ${latest.name}(${loaded.messages.length} 条消息)`, C.dim));
|
package/src/config.js
CHANGED
|
@@ -227,6 +227,43 @@ function applyModelLabels(/** @type {any} */ cfg) {
|
|
|
227
227
|
return cfg;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* 会话读取真源(v0.7.0 B2-b 灰度开关)。
|
|
232
|
+
*
|
|
233
|
+
* `messages`(默认):继续只读 `.jsonl` 消息数组——与 0.6.x 完全一致,事件侧车坏掉也不影响。
|
|
234
|
+
* `events`:用事件日志重放派生消息,但**必须与消息文件对拍一致**才采用;不一致就 warn 并
|
|
235
|
+
* 回退到消息数组。这只用于灰度验证「事件流能否当唯一真源」,**不是**新的默认值:
|
|
236
|
+
* 翻默认要等真实会话的 `scripts/event-parity.mjs` 证据(pass 覆盖足够广、incomplete 清零)。
|
|
237
|
+
*
|
|
238
|
+
* 校验口径与 `config.eventLog` 一致:只认字面量 `'events'`,其余(缺省 / 大小写不符 /
|
|
239
|
+
* 任意非法值)一律按 `'messages'` 处理,绝不因为一个拼错的值把读取路径切到实验分支。
|
|
240
|
+
*/
|
|
241
|
+
export const EVENT_SOURCE_MESSAGES = 'messages';
|
|
242
|
+
export const EVENT_SOURCE_EVENTS = 'events';
|
|
243
|
+
/** 默认真源:`messages`。改这里等于改所有 ENOENT/未配置用户的读取路径,必须等灰度证据。 */
|
|
244
|
+
export const DEFAULT_EVENT_SOURCE = EVENT_SOURCE_MESSAGES;
|
|
245
|
+
const EVENT_SOURCE_SET = new Set([EVENT_SOURCE_MESSAGES, EVENT_SOURCE_EVENTS]);
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 归一化 `config.eventSource`:合法枚举原样返回,其余一律回落默认值 `messages`。
|
|
249
|
+
* 纯函数,读配置/写配置/直接调用 `loadSession(file, cfg)` 三条路径共用同一判据。
|
|
250
|
+
* @param {any} value
|
|
251
|
+
* @returns {'messages'|'events'}
|
|
252
|
+
*/
|
|
253
|
+
export function normalizeEventSource(value) {
|
|
254
|
+
return EVENT_SOURCE_SET.has(value) ? value : DEFAULT_EVENT_SOURCE;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 就地归一化配置对象里的 `eventSource`(字段不存在时**不新增**,保持旧配置文件字节不变)。
|
|
259
|
+
* @param {any} cfg
|
|
260
|
+
*/
|
|
261
|
+
function applyEventSource(/** @type {any} */ cfg) {
|
|
262
|
+
if (!isConfigObject(cfg) || cfg.eventSource === undefined) return cfg;
|
|
263
|
+
cfg.eventSource = normalizeEventSource(cfg.eventSource);
|
|
264
|
+
return cfg;
|
|
265
|
+
}
|
|
266
|
+
|
|
230
267
|
// ---------------------------------------------------------------------------
|
|
231
268
|
// v0.7.0 §1.3:配置写入的「基线 + 增量」。
|
|
232
269
|
//
|
|
@@ -350,12 +387,12 @@ function prepareConfigForWrite(cfg) {
|
|
|
350
387
|
quarantineCorruptConfig(`保存配置时读不出来:${strict.error}`);
|
|
351
388
|
base = {};
|
|
352
389
|
}
|
|
353
|
-
if (!sameHome) return cfg; // 无基线(新配置/字面量)或换了 home → 保持原语义:整份写入
|
|
390
|
+
if (!sameHome) return applyEventSource(cfg); // 无基线(新配置/字面量)或换了 home → 保持原语义:整份写入
|
|
354
391
|
/** @type {{set: [string[], any][], del: string[][]}} */
|
|
355
392
|
const patch = { set: [], del: [] };
|
|
356
393
|
collectConfigPatch(snapshot.data, cfg, [], patch);
|
|
357
394
|
applyConfigPatch(base, patch);
|
|
358
|
-
return base;
|
|
395
|
+
return applyEventSource(base);
|
|
359
396
|
}
|
|
360
397
|
|
|
361
398
|
/**
|
|
@@ -379,7 +416,7 @@ function applyEventLogGate(cfg) {
|
|
|
379
416
|
export function loadConfig() {
|
|
380
417
|
const r = readConfigStrict();
|
|
381
418
|
if (!r.ok) return null;
|
|
382
|
-
const cfg = applyModelLabels(applyCachedModelDefaults(r.data));
|
|
419
|
+
const cfg = applyEventSource(applyModelLabels(applyCachedModelDefaults(r.data)));
|
|
383
420
|
rememberConfig(cfg); // v0.7.0 §1.3:记下基线,saveConfig 只合并增量
|
|
384
421
|
applyEventLogGate(cfg); // v0.7.0 §1.1:config.eventLog=false 在「先落盘、后建 agent」路径也生效
|
|
385
422
|
return cfg;
|
|
@@ -401,7 +438,7 @@ export function loadConfig() {
|
|
|
401
438
|
export function loadConfigForWrite(why = '某个写配置的命令') {
|
|
402
439
|
const r = readConfigStrict();
|
|
403
440
|
if (r.ok) {
|
|
404
|
-
const cfg = r.data || {};
|
|
441
|
+
const cfg = applyEventSource(r.data || {});
|
|
405
442
|
rememberConfig(cfg);
|
|
406
443
|
applyEventLogGate(cfg);
|
|
407
444
|
return cfg;
|
|
@@ -418,7 +455,7 @@ export function loadConfigForWrite(why = '某个写配置的命令') {
|
|
|
418
455
|
export function ensureMinimalConfig() {
|
|
419
456
|
const strict = readConfigStrict();
|
|
420
457
|
if (strict.ok && strict.data) {
|
|
421
|
-
const cfg = applyModelLabels(strict.data);
|
|
458
|
+
const cfg = applyEventSource(applyModelLabels(strict.data));
|
|
422
459
|
rememberConfig(cfg);
|
|
423
460
|
return cfg;
|
|
424
461
|
}
|
|
@@ -457,6 +494,7 @@ export function ensureMinimalConfig() {
|
|
|
457
494
|
export function saveConfig(cfg) {
|
|
458
495
|
try {
|
|
459
496
|
ensureHome();
|
|
497
|
+
applyEventSource(cfg); // B2-b:写回前也归一化,保证内存基线与落盘值一致
|
|
460
498
|
withStateFileLockSync(configLockFile(), () => {
|
|
461
499
|
writeConfigUnlocked(prepareConfigForWrite(cfg));
|
|
462
500
|
rememberConfig(cfg); // 成功后把这版对象当作下次增量比较的基线
|
|
@@ -479,6 +517,7 @@ export function saveConfig(cfg) {
|
|
|
479
517
|
export async function saveConfigAsync(cfg) {
|
|
480
518
|
try {
|
|
481
519
|
ensureHome();
|
|
520
|
+
applyEventSource(cfg); // B2-b:写回前也归一化,保证内存基线与落盘值一致
|
|
482
521
|
await withFileLock(configLockFile(), () => {
|
|
483
522
|
writeConfigUnlocked(prepareConfigForWrite(cfg));
|
|
484
523
|
rememberConfig(cfg);
|
|
@@ -513,7 +552,7 @@ export function updateConfigSync(/** @type {(cfg: any) => any} */ mutate) {
|
|
|
513
552
|
cfg = {};
|
|
514
553
|
}
|
|
515
554
|
const next = typeof mutate === 'function' ? mutate(cfg) : cfg;
|
|
516
|
-
const out = isConfigObject(next) ? next : cfg;
|
|
555
|
+
const out = applyEventSource(isConfigObject(next) ? next : cfg);
|
|
517
556
|
writeConfigUnlocked(out);
|
|
518
557
|
rememberConfig(out);
|
|
519
558
|
return { ok: true, error: null, config: out };
|
|
@@ -543,7 +582,7 @@ export async function updateConfig(/** @type {(cfg: any) => any} */ mutate) {
|
|
|
543
582
|
cfg = {};
|
|
544
583
|
}
|
|
545
584
|
const next = typeof mutate === 'function' ? mutate(cfg) : cfg;
|
|
546
|
-
const out = isConfigObject(next) ? next : cfg;
|
|
585
|
+
const out = applyEventSource(isConfigObject(next) ? next : cfg);
|
|
547
586
|
writeConfigUnlocked(out);
|
|
548
587
|
rememberConfig(out);
|
|
549
588
|
return { ok: true, error: null, config: out };
|
package/src/session-events.js
CHANGED
|
@@ -777,6 +777,39 @@ export function replayToMessages(events) {
|
|
|
777
777
|
return messages;
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
+
/**
|
|
781
|
+
* 比较两个消息数组(v0.7.0 B2-b 从 `scripts/event-parity.mjs` 抽出的共享判据)。
|
|
782
|
+
*
|
|
783
|
+
* 逐条按 `role + content + tool_calls`(tool_calls 只比 id,排序后)比较;`_` 开头的运行时注记
|
|
784
|
+
* 不落盘、不参与比较。**必须与 scripts/event-parity.mjs 的 `diffMessages` 保持同一语义**:
|
|
785
|
+
* 对拍工具说「等价」的会话,`loadSession({eventSource:'events'})` 才能采用派生结果;两边判据
|
|
786
|
+
* 一旦漂移,对拍结论与灰度读取就会互相打脸。
|
|
787
|
+
*
|
|
788
|
+
* 为什么不直接 import 脚本:`scripts/` 不在 `package.json#files`(npm 安装形态没有它),
|
|
789
|
+
* 且 tsconfig 会把脚本拉进类型检查;因此判据放在 src 侧,脚本侧保留同名实现(硬约束:本切片
|
|
790
|
+
* 不改 `scripts/event-parity.mjs`)。
|
|
791
|
+
*
|
|
792
|
+
* @param {any[]} a
|
|
793
|
+
* @param {any[]} b
|
|
794
|
+
* @returns {{ok:boolean, index:number, reason:string}}
|
|
795
|
+
*/
|
|
796
|
+
export function diffMessages(a, b) {
|
|
797
|
+
const A = Array.isArray(a) ? a : [];
|
|
798
|
+
const B = Array.isArray(b) ? b : [];
|
|
799
|
+
if (A.length !== B.length) return { ok: false, index: Math.min(A.length, B.length), reason: `条数不同:消息 ${A.length} vs 重放 ${B.length}` };
|
|
800
|
+
const norm = (/** @type {any} */ m) => {
|
|
801
|
+
/** @type {{role:string, content:any, tool_calls?:string[], tool_call_id?:string}} */
|
|
802
|
+
const out = { role: String(m?.role ?? ''), content: m?.content ?? '' };
|
|
803
|
+
if (Array.isArray(m?.tool_calls)) out.tool_calls = m.tool_calls.map((/** @type {any} */ t) => String(t?.id ?? '')).sort();
|
|
804
|
+
if (m?.tool_call_id != null) out.tool_call_id = String(m.tool_call_id);
|
|
805
|
+
return JSON.stringify(out);
|
|
806
|
+
};
|
|
807
|
+
for (let i = 0; i < A.length; i += 1) {
|
|
808
|
+
if (norm(A[i]) !== norm(B[i])) return { ok: false, index: i, reason: `第 ${i + 1} 条不同` };
|
|
809
|
+
}
|
|
810
|
+
return { ok: true, index: -1, reason: '' };
|
|
811
|
+
}
|
|
812
|
+
|
|
780
813
|
/**
|
|
781
814
|
* 供后续 UI/诊断用的 turn 摘要(可选;纯函数)。只从事件自己推导,不读消息正文。
|
|
782
815
|
* @param {any[]} events
|
package/src/session.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { appendEvent, eventFileFor, eventLogEnabled, isEventLogName, repairEventLog, warnEventFailure, EVENT_LOG_FORMAT } from './session-events.js';
|
|
6
|
+
import { appendEvent, eventFileFor, eventLogEnabled, diffMessages, isEventLogName, readEvents, replayToMessages, repairEventLog, warnEventFailure, EVENT_LOG_FORMAT } from './session-events.js';
|
|
7
|
+
// v0.7.0 B2-b:读取真源开关。归一化放在 config.js(配置读取/写回/直接传 cfg 三条路径共用),
|
|
8
|
+
// 对拍比较函数抽在 session-events.js 的 `diffMessages`(与 scripts/event-parity.mjs 同一判据)。
|
|
9
|
+
import { normalizeEventSource, EVENT_SOURCE_EVENTS } from './config.js';
|
|
7
10
|
|
|
8
11
|
// —— 冲突备份名(单一事实来源,v0.4.6)——
|
|
9
12
|
// 同步冲突时写入的备份:<会话名>.server-<时间戳>[-<随机后缀>].jsonl(远端版本)与 .remote-(远端拉取版本)。
|
|
@@ -262,7 +265,44 @@ export function repairSessionEvents(file, cfg = null) {
|
|
|
262
265
|
}
|
|
263
266
|
}
|
|
264
267
|
|
|
265
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* v0.7.0 B2-b:从事件侧车派生消息(灰度读取真源)。
|
|
270
|
+
*
|
|
271
|
+
* 返回 `null` 表示**不采用事件源**——无侧车 / 格式戳缺失或早于本版 / 事件日志读失败。
|
|
272
|
+
* 这三种都是灰度期常态(旧会话本来就没有 `messages.replace`),调用方必须**静默**回退到
|
|
273
|
+
* 消息数组,不刷 warn;只有「格式够新、派生出来了、但与消息文件对拍不一致」才是需要可见的异常。
|
|
274
|
+
* @param {any} file 会话文件(.jsonl)
|
|
275
|
+
* @returns {any[]|null}
|
|
276
|
+
*/
|
|
277
|
+
function messagesFromEvents(file) {
|
|
278
|
+
const eventFile = eventFileFor(file);
|
|
279
|
+
if (!eventFile) return null;
|
|
280
|
+
try {
|
|
281
|
+
if (!fs.existsSync(eventFile)) return null;
|
|
282
|
+
const read = readEvents(eventFile);
|
|
283
|
+
if (!read.ok) return null;
|
|
284
|
+
// 与 scripts/event-parity.mjs 同一判据:format >= EVENT_LOG_FORMAT 才认为日志可完整回溯
|
|
285
|
+
// (旧日志没有 rewrite 记录,派生出来必然缺窗口,不能拿它当读取真源)。
|
|
286
|
+
const newFormat = read.events.some((ev) => ev.type === 'session.start' && Number(ev.data?.format) >= EVENT_LOG_FORMAT);
|
|
287
|
+
if (!newFormat) return null;
|
|
288
|
+
return replayToMessages(read.events);
|
|
289
|
+
} catch {
|
|
290
|
+
// 事件侧车是旁路:任何异常都只意味着「这次不用事件源」,绝不能让会话读不出来。
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 读取会话。默认(`eventSource` 缺省或 `messages`)只读 `.jsonl` 消息数组,行为与 0.6.x 一致。
|
|
297
|
+
*
|
|
298
|
+
* `cfg.eventSource === 'events'`(v0.7.0 B2-b 灰度开关)时额外尝试事件日志派生,但必须通过
|
|
299
|
+
* **对拍兜底**:派生结果与消息文件逐条等价才采用;不一致就 warn(带文件与首个差异位置)并
|
|
300
|
+
* 回退到消息数组。事件源缺失 / 格式旧 / 读失败一律静默回退。
|
|
301
|
+
*
|
|
302
|
+
* @param {any} file
|
|
303
|
+
* @param {any} [cfg] 传入时尊重 `cfg.eventSource` 与 `cfg.eventLog === false`(后者只影响修补器)
|
|
304
|
+
* @returns {{file:any, messages:any[]}} 形状与旧版完全一致
|
|
305
|
+
*/
|
|
266
306
|
export function loadSession(file, cfg = null) {
|
|
267
307
|
const raw = fs.readFileSync(file, 'utf8');
|
|
268
308
|
const messages = [];
|
|
@@ -275,10 +315,30 @@ export function loadSession(file, cfg = null) {
|
|
|
275
315
|
}
|
|
276
316
|
} catch {}
|
|
277
317
|
}
|
|
318
|
+
let result = messages;
|
|
319
|
+
if (normalizeEventSource(cfg?.eventSource) === EVENT_SOURCE_EVENTS) {
|
|
320
|
+
const derived = messagesFromEvents(file);
|
|
321
|
+
if (derived) {
|
|
322
|
+
const d = diffMessages(messages, derived);
|
|
323
|
+
if (d.ok) result = derived;
|
|
324
|
+
else {
|
|
325
|
+
// 对拍失败是**真异常**(新格式日志理应可完整重放):必须可见,并带首个差异位置。
|
|
326
|
+
// 但读取结果仍回退到消息数组——灰度期不能让实验分支改变用户看到的历史。
|
|
327
|
+
const at = d.index >= 0 ? `第 ${d.index + 1} 条` : '未知';
|
|
328
|
+
try {
|
|
329
|
+
console.warn(
|
|
330
|
+
`[MingDao] ⚠ 会话事件源对拍不一致(${file}):${d.reason};差异位置:${at}——已回退到消息数组` +
|
|
331
|
+
`(config.eventSource=events 仅灰度验证用,默认仍是 messages)。`
|
|
332
|
+
);
|
|
333
|
+
} catch {}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
278
337
|
// v0.6.40:消息读完后顺带闭合悬空事件。失败只 warn(repairSessionEvents 内部吞掉),
|
|
279
338
|
// 返回值与消息数组一字不动——事件侧车坏了/修不了,会话照样可读。
|
|
339
|
+
// v0.7.0 B2-b:放在事件派生之后,派生读到的是修补前的日志(与 scripts/event-parity.mjs 同口径)。
|
|
280
340
|
repairSessionEvents(file, cfg);
|
|
281
|
-
return { file, messages };
|
|
341
|
+
return { file, messages: result };
|
|
282
342
|
}
|
|
283
343
|
|
|
284
344
|
// 会话预览:第一条用户消息(用于会话选择器)
|
|
@@ -89,7 +89,7 @@ export async function handle({ req, res, method, p, url }, deps, shared) {
|
|
|
89
89
|
const file = url.searchParams.get('file');
|
|
90
90
|
if (!file) return json(res, 400, { error: '缺少 file 参数' });
|
|
91
91
|
try {
|
|
92
|
-
const loaded = loadSession(path.join(home, 'sessions', path.basename(file)));
|
|
92
|
+
const loaded = loadSession(path.join(home, 'sessions', path.basename(file)), deps.cfg);
|
|
93
93
|
// 载入会话时聚焦其工作空间(P3-4):全局默认切到该会话的目录,前端下拉同步显示
|
|
94
94
|
const wsDir = getSessionWorkspace(path.basename(file));
|
|
95
95
|
const ws = workspaceForDir(wsDir);
|
|
@@ -189,7 +189,7 @@ export async function handle({ req, res, method, p, url }, deps, shared) {
|
|
|
189
189
|
return json(res, 404, { error: `会话不存在:${file}` });
|
|
190
190
|
}
|
|
191
191
|
try {
|
|
192
|
-
const loaded = loadSession(path.join(home, 'sessions', file));
|
|
192
|
+
const loaded = loadSession(path.join(home, 'sessions', file), deps.cfg);
|
|
193
193
|
if (loaded.messages.length) {
|
|
194
194
|
const wsDir = getSessionWorkspace(file) || state.workingDir;
|
|
195
195
|
const { finalizeSession } = await import('../../../memory.js');
|
package/src/web/server.js
CHANGED
|
@@ -819,7 +819,7 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken,
|
|
|
819
819
|
const isNew = !body.file;
|
|
820
820
|
if (body.file) {
|
|
821
821
|
try {
|
|
822
|
-
const loaded = loadSession(path.join(home, 'sessions', path.basename(body.file)));
|
|
822
|
+
const loaded = loadSession(path.join(home, 'sessions', path.basename(body.file)), cfg);
|
|
823
823
|
if (loaded.messages.length) session = loaded;
|
|
824
824
|
} catch {}
|
|
825
825
|
}
|