@starwish-ai/xingyuan-dsh 0.6.3 → 0.6.4
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 +28 -0
- package/README.en-US.md +3 -0
- package/README.md +3 -0
- package/lib/index.js +6 -1
- package/lib/session-log-repair.js +117 -40
- package/lib/types/index.d.ts +5 -4
- package/lib/types/session-log-repair.d.ts +21 -13
- package/package.json +31 -30
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.4] - 2026-09-11
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **dsh 0.1.5-rc.2 compatibility**: peer and dev dependencies moved from
|
|
12
|
+
`0.1.2-rc.1` to `^0.1.5-rc.2`. Every host/client API this plugin consumes was
|
|
13
|
+
re-verified against the installed 0.1.5-rc.2 build — tool registration,
|
|
14
|
+
settings namespaces, storage backend/domain, user questions, system prompt,
|
|
15
|
+
conversation cards and session tabs are unchanged; the client half needs no
|
|
16
|
+
code changes. XingYuan's prompt sections (orders 5–115) now naturally precede
|
|
17
|
+
the host's relocated sections (10000+).
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- **Session log self-heal now covers format v3**: dsh 0.1.5 names session
|
|
21
|
+
artifacts `session.v{N}.jsonl[.zstd]`; the repair module now selects the
|
|
22
|
+
highest stored generation per session instead of the fixed `session.jsonl`
|
|
23
|
+
names, so `xingyuan/*` events in new sessions keep receiving the `ignorable`
|
|
24
|
+
marker before a cold load.
|
|
25
|
+
- **Legacy sessions with XingYuan cards open again**: dsh 0.1.5's v0/v1/v2 → v3
|
|
26
|
+
migration chain rejects every unknown historical event, `ignorable` included,
|
|
27
|
+
which previously made a session containing `xingyuan/*` events fail to load as
|
|
28
|
+
a whole. Legacy artifacts are now de-poisoned ahead of migration: those event
|
|
29
|
+
rows are replaced with the official inert `hook/invoked` event (seq/time
|
|
30
|
+
preserved, payload valid across every migration edge), so the session migrates
|
|
31
|
+
and opens; other rows stay byte-identical and files without XingYuan events
|
|
32
|
+
are never written. Trade-off: historical cards in those old sessions no longer
|
|
33
|
+
replay (business data lives in SQLite; the pre-repair file is backed up under
|
|
34
|
+
`~/.dsh/xingyuan/session-backups/`).
|
|
35
|
+
|
|
8
36
|
## [0.6.3] - 2026-09-06
|
|
9
37
|
|
|
10
38
|
### Changed
|
package/README.en-US.md
CHANGED
|
@@ -25,6 +25,9 @@ Tools are only mounted on sessions using the XingYuan preset; other sessions are
|
|
|
25
25
|
|
|
26
26
|
## Install
|
|
27
27
|
|
|
28
|
+
> Requires DeepSeek Harness `0.1.5-rc.2` (peer dependencies are pinned to that
|
|
29
|
+
> release; check the upgrade notes in [AGENTS.md](./AGENTS.md) before bumping dsh).
|
|
30
|
+
|
|
28
31
|
```sh
|
|
29
32
|
dsh plugin --profile web add @starwish-ai/xingyuan-dsh
|
|
30
33
|
```
|
package/README.md
CHANGED
package/lib/index.js
CHANGED
|
@@ -65,7 +65,12 @@ export async function apply(ctx, config) {
|
|
|
65
65
|
try {
|
|
66
66
|
const report = await repairSessionLogs({ listLiveSessionIds: () => liveSessionIds(sessions) });
|
|
67
67
|
if (report.patched > 0) {
|
|
68
|
-
|
|
68
|
+
const parts = [];
|
|
69
|
+
if (report.eventsMarked > 0)
|
|
70
|
+
parts.push(`补标 ${report.eventsMarked} 条卡片事件`);
|
|
71
|
+
if (report.neutralized > 0)
|
|
72
|
+
parts.push(`旧格式会话迁移前替换 ${report.neutralized} 条卡片事件(历史卡片不再回放)`);
|
|
73
|
+
console.log(`[xingyuan] 会话日志自愈:${parts.join(',')}(${report.patched} 个会话,扫描 ${report.scanned})`);
|
|
69
74
|
}
|
|
70
75
|
for (const warning of report.warnings)
|
|
71
76
|
console.warn(`[xingyuan] 会话日志自愈跳过:${warning}`);
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 会话日志自愈(bundle 常驻层)。
|
|
3
3
|
*
|
|
4
|
-
* 背景:dsh
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* 背景:dsh 会话持久化读取端按「本仓库生成的官方事件类型白名单」拒绝未知事件;
|
|
5
|
+
* 仓库外插件事件按构造不在名单内,而写入端(Session.append)至今没有 ignorable
|
|
6
|
+
* 标记通道。0.1.5-rc.2 的规则分两档:
|
|
7
|
+
* - 当前格式(v3,工件名 session.v3.jsonl[.zstd]):读取端认 `ignorable: true`,
|
|
8
|
+
* 不认识的带标事件被安全跳过且数据原样保留;
|
|
9
|
+
* - 历史格式迁移(v0/v1/v2 → v3):迁移链**拒绝一切未知历史事件,ignorable
|
|
10
|
+
* 也不例外**(官方 alpha-historical-unknown-event-refusal 决策)——旧日志里的
|
|
11
|
+
* xingyuan/* 事件会让整个会话冷加载失败,且不产出 v3 后继、源文件原样保留。
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* 本模块在激活期扫描 $DSH_HOME/sessions,按工件版本分两种处理:
|
|
14
|
+
* - 当前版本工件:补标模式——xingyuan/* 未标记事件行补 `"ignorable": true`;
|
|
15
|
+
* - 历史版本工件:去毒模式——xingyuan/* 事件行替换为官方惰性事件 hook/invoked
|
|
16
|
+
* (seq/time 不变),迁移链即可安全穿过、会话照常打开;代价是该会话历史卡片
|
|
17
|
+
* 事件不再回放(业务数据在 sqlite,不丢失)。仅当目录内无当前版本工件时才动
|
|
18
|
+
* 历史工件(dsh 只按最新代读取,旧代已不被消费)。
|
|
14
19
|
*
|
|
15
20
|
* 安全约束(改前必读,勿删):
|
|
16
21
|
* - 不含星愿事件的会话文件零写入(字节与 mtime 均不变);
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
22
|
+
* - 只有星愿事件行被重序列化(补标仅追加字段;去毒仅换 type/data),其余行原样
|
|
23
|
+
* 保留字节;行序与 seq 由构造保证不变(本模块从不增删移动任何行);
|
|
19
24
|
* - 首次改写前把原件备份到 <home>/xingyuan/session-backups/<项目目录>/,
|
|
20
25
|
* 每会话保留最近 maxBackupPerSession 份;
|
|
21
26
|
* - 任何看不懂的情况一律整文件跳过不动:撕裂尾、坏帧、header 版本不符、
|
|
@@ -28,7 +33,8 @@
|
|
|
28
33
|
* (历史上一次实现曾把整个文件重压成单帧导致宿主启动崩溃,已由本模块的
|
|
29
34
|
* relayout-only 路径兜底自愈),即使无需补标也会重写为合法布局。
|
|
30
35
|
* - SESSION_FORMAT_VERSION 取自 @deepseek-ai/dsh-session。升级 dsh 时必须核对
|
|
31
|
-
*
|
|
36
|
+
* 本模块的前提与布局契约仍然成立(尤其迁移链是否改为接受 ignorable 历史事件——
|
|
37
|
+
* 若接受,去毒模式可回归补标)。
|
|
32
38
|
*/
|
|
33
39
|
import { constants as zlibConstants, zstdCompress, zstdDecompressSync } from 'node:zlib';
|
|
34
40
|
import { promisify } from 'node:util';
|
|
@@ -41,6 +47,21 @@ const zstdCompressAsync = promisify(zstdCompress);
|
|
|
41
47
|
const XINGYUAN_EVENT_PREFIX = 'xingyuan/';
|
|
42
48
|
/** 工件内明文的防御性上限(远超个人部署可能的会话体积)。 */
|
|
43
49
|
const MAX_PLAINTEXT_BYTES = 512 * 1024 * 1024;
|
|
50
|
+
/**
|
|
51
|
+
* 去毒模式下替换星愿事件的官方惰性事件类型与载荷:hook/invoked 只做日志记录,
|
|
52
|
+
* 跨 v0/v1/v2 迁移边无任何关系约束,是最安全的中性载体(payload 语义校验要求
|
|
53
|
+
* turn≥0、point/handlerId 非空、dialect ∈ {claude-code, codex})。
|
|
54
|
+
* handlerId 保留可追溯的替换来源,便于人工审计。
|
|
55
|
+
*/
|
|
56
|
+
const NEUTRAL_EVENT_TYPE = 'hook/invoked';
|
|
57
|
+
function neutralEvent(seq, time, headerId) {
|
|
58
|
+
return JSON.stringify({
|
|
59
|
+
type: NEUTRAL_EVENT_TYPE,
|
|
60
|
+
seq,
|
|
61
|
+
time,
|
|
62
|
+
data: { turn: 1, point: 'PreToolUse', dialect: 'codex', handlerId: `xingyuan-repair:${headerId}:${String(seq)}` },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
44
65
|
/**
|
|
45
66
|
* 每会话自愈标记文件名(放在会话目录内,dsh 不扫描它)。用于把启动期自愈从
|
|
46
67
|
* 「全量解压解析」降为「stat + 读 40 字节标记」:已修复的会话除非工件字节数
|
|
@@ -161,11 +182,12 @@ async function selfVerify(container, expectedPlaintext) {
|
|
|
161
182
|
return undefined;
|
|
162
183
|
}
|
|
163
184
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
185
|
+
* 对工件明文做行级处理。首行必须是 session header 且版本与工件名一致
|
|
186
|
+
* (mark 要求当前版本;neutralize 要求历史版本)。之后每个完整行必须能解析为
|
|
187
|
+
* 对象(任何一行解析失败都整体放弃)。只有星愿事件行会被重序列化,其余行保持
|
|
188
|
+
* 原字节。撕裂尾(无换行的最后一段)按原样保留。
|
|
167
189
|
*/
|
|
168
|
-
function patchPlaintext(plaintext) {
|
|
190
|
+
function patchPlaintext(plaintext, mode, artifactVersion) {
|
|
169
191
|
if (plaintext.length > MAX_PLAINTEXT_BYTES)
|
|
170
192
|
return skip('oversized', `${plaintext.length} bytes`);
|
|
171
193
|
const headerEnd = plaintext.indexOf(10);
|
|
@@ -179,20 +201,24 @@ function patchPlaintext(plaintext) {
|
|
|
179
201
|
return skip('format', 'header line is not valid JSON');
|
|
180
202
|
}
|
|
181
203
|
const headerRecord = header;
|
|
204
|
+
const expectedVersion = mode === 'mark' ? SESSION_FORMAT_VERSION : artifactVersion;
|
|
182
205
|
if (headerRecord === null || typeof headerRecord !== 'object' ||
|
|
183
206
|
headerRecord['type'] !== 'session' ||
|
|
184
|
-
headerRecord['version'] !==
|
|
207
|
+
headerRecord['version'] !== expectedVersion ||
|
|
208
|
+
(mode === 'neutralize' && expectedVersion >= SESSION_FORMAT_VERSION) ||
|
|
185
209
|
typeof headerRecord['id'] !== 'string' || headerRecord['id'] === '') {
|
|
186
|
-
return skip('format', `header version=${String(headerRecord?.['version'])} expect=${
|
|
210
|
+
return skip('format', `header version=${String(headerRecord?.['version'])} expect=${expectedVersion}`);
|
|
187
211
|
}
|
|
212
|
+
const headerId = headerRecord['id'];
|
|
188
213
|
const chunks = [plaintext.subarray(0, headerEnd + 1)];
|
|
189
214
|
let eventsMarked = 0;
|
|
215
|
+
let eventsNeutralized = 0;
|
|
190
216
|
let totalMarked = 0;
|
|
191
217
|
let cursor = headerEnd + 1;
|
|
192
218
|
let lineNumber = 1;
|
|
193
219
|
for (;;) {
|
|
194
220
|
const newline = plaintext.indexOf(10, cursor);
|
|
195
|
-
// 无换行的尾段是撕裂尾或尚未落盘的部分记录:交给 dsh
|
|
221
|
+
// 无换行的尾段是撕裂尾或尚未落盘的部分记录:交给 dsh 自己的截断修复,不在处理范围
|
|
196
222
|
if (newline === -1) {
|
|
197
223
|
chunks.push(plaintext.subarray(cursor));
|
|
198
224
|
break;
|
|
@@ -211,8 +237,12 @@ function patchPlaintext(plaintext) {
|
|
|
211
237
|
if (record === null || typeof record !== 'object' || typeof record['type'] !== 'string') {
|
|
212
238
|
return skip('unparsable', 'event line is not an object with a type field');
|
|
213
239
|
}
|
|
214
|
-
if (record['type'].startsWith(XINGYUAN_EVENT_PREFIX)) {
|
|
215
|
-
|
|
240
|
+
if (!record['type'].startsWith(XINGYUAN_EVENT_PREFIX)) {
|
|
241
|
+
chunks.push(rawLine, Buffer.from('\n', 'utf8'));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
totalMarked += 1;
|
|
245
|
+
if (mode === 'mark') {
|
|
216
246
|
if (record['ignorable'] !== true) {
|
|
217
247
|
record['ignorable'] = true;
|
|
218
248
|
eventsMarked += 1;
|
|
@@ -222,14 +252,21 @@ function patchPlaintext(plaintext) {
|
|
|
222
252
|
// 已是 ignorable:不触发补标(patched 判定只看 eventsMarked),只累计总数供回填
|
|
223
253
|
chunks.push(rawLine, Buffer.from('\n', 'utf8'));
|
|
224
254
|
}
|
|
255
|
+
continue;
|
|
225
256
|
}
|
|
226
|
-
|
|
227
|
-
|
|
257
|
+
// neutralize:seq/time 必须可原样承载,否则整文件放弃(宁可跳过不可伪造坐标)
|
|
258
|
+
const seq = record['seq'];
|
|
259
|
+
const time = record['time'];
|
|
260
|
+
if (!Number.isSafeInteger(seq) || !Number.isSafeInteger(time)) {
|
|
261
|
+
return skip('unparsable', `xingyuan event line ${lineNumber} lacks safe integer seq/time`);
|
|
228
262
|
}
|
|
263
|
+
eventsNeutralized += 1;
|
|
264
|
+
chunks.push(Buffer.from(neutralEvent(seq, time, headerId), 'utf8'), Buffer.from('\n', 'utf8'));
|
|
265
|
+
}
|
|
266
|
+
if (eventsMarked === 0 && eventsNeutralized === 0) {
|
|
267
|
+
return { kind: 'clean', plaintext, eventsMarked: 0, eventsNeutralized: 0, totalMarked };
|
|
229
268
|
}
|
|
230
|
-
|
|
231
|
-
return { kind: 'clean', plaintext, eventsMarked: 0, totalMarked };
|
|
232
|
-
return { kind: 'patched', plaintext: Buffer.concat(chunks), eventsMarked, totalMarked };
|
|
269
|
+
return { kind: 'patched', plaintext: Buffer.concat(chunks), eventsMarked, eventsNeutralized, totalMarked };
|
|
233
270
|
}
|
|
234
271
|
function skip(reason, detail) {
|
|
235
272
|
return { kind: 'skip', reason, detail };
|
|
@@ -267,14 +304,10 @@ async function subdirectories(dir) {
|
|
|
267
304
|
return [];
|
|
268
305
|
}
|
|
269
306
|
}
|
|
270
|
-
const SESSION_ARTIFACTS = [
|
|
271
|
-
{ filename: 'session.jsonl.zstd', compressed: true },
|
|
272
|
-
{ filename: 'session.jsonl', compressed: false },
|
|
273
|
-
];
|
|
274
307
|
/** 激活期自愈入口:永不抛出,异常折算进 report(warnings/skipped.error)。 */
|
|
275
308
|
export async function repairSessionLogs(options = {}) {
|
|
276
309
|
const log = options.log ?? (() => { });
|
|
277
|
-
const report = { scanned: 0, patched: 0, eventsMarked: 0, skipped: {}, warnings: [] };
|
|
310
|
+
const report = { scanned: 0, patched: 0, eventsMarked: 0, neutralized: 0, skipped: {}, warnings: [] };
|
|
278
311
|
const sessionsRoot = join(options.dshHome ?? resolveDshHome(), 'sessions');
|
|
279
312
|
const liveIds = safeLiveIds(options.listLiveSessionIds);
|
|
280
313
|
if (!existsSync(sessionsRoot))
|
|
@@ -312,9 +345,12 @@ export async function repairSessionLogs(options = {}) {
|
|
|
312
345
|
if (outcome.kind === 'patched') {
|
|
313
346
|
report.patched += 1;
|
|
314
347
|
report.eventsMarked += outcome.eventsMarked;
|
|
348
|
+
report.neutralized += outcome.eventsNeutralized;
|
|
315
349
|
log(outcome.relayoutOnly
|
|
316
350
|
? `[xingyuan] 已修复会话 ${outcome.sessionId} 的工件帧布局(首帧须为单行 header)`
|
|
317
|
-
:
|
|
351
|
+
: outcome.eventsNeutralized > 0
|
|
352
|
+
? `[xingyuan] 会话 ${outcome.sessionId} 为旧格式:${outcome.eventsNeutralized} 条卡片事件已替换为惰性事件以便 dsh 迁移(卡片不再回放)`
|
|
353
|
+
: `[xingyuan] 已为会话 ${outcome.sessionId} 补标 ${outcome.eventsMarked} 条卡片事件(ignorable)`);
|
|
318
354
|
}
|
|
319
355
|
else if (outcome.kind === 'clean') {
|
|
320
356
|
report.eventsMarked += outcome.eventsMarked;
|
|
@@ -332,13 +368,47 @@ export async function repairSessionLogs(options = {}) {
|
|
|
332
368
|
}
|
|
333
369
|
return report;
|
|
334
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* 解析官方工件名:`session.jsonl[.zstd]`(v0)或 `session.v<N>.jsonl[.zstd]`(N>0)。
|
|
373
|
+
* 非规范名(大写 v、前导零、v0 带版本段、迁移临时文件等)与高于当前版本的代
|
|
374
|
+
* 一律不认(口径同 dsh 的 parseGenerationLogFilename)。
|
|
375
|
+
*/
|
|
376
|
+
function parseArtifactName(name) {
|
|
377
|
+
const match = /^session(?:\.v([1-9]\d*))?\.jsonl(\.zstd)?$/.exec(name);
|
|
378
|
+
if (match === null)
|
|
379
|
+
return undefined;
|
|
380
|
+
const version = match[1] === undefined ? 0 : Number(match[1]);
|
|
381
|
+
if (!Number.isSafeInteger(version) || version > SESSION_FORMAT_VERSION)
|
|
382
|
+
return undefined;
|
|
383
|
+
return { version, compressed: match[2] !== undefined };
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* 选目录内最高代的合法工件(dsh 只按最新代读取/迁移):当前版本走补标,
|
|
387
|
+
* 更低版本走去毒,旧代在被更高代遮蔽时不处理(零写入原则)。同代并存时
|
|
388
|
+
* 优先压缩工件(与旧版选择顺序一致)。
|
|
389
|
+
*/
|
|
335
390
|
async function locateArtifact(sessionDir) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return { path, compressed: candidate.compressed };
|
|
391
|
+
let entries;
|
|
392
|
+
try {
|
|
393
|
+
entries = await readdir(sessionDir);
|
|
340
394
|
}
|
|
341
|
-
|
|
395
|
+
catch {
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
let best;
|
|
399
|
+
for (const name of entries) {
|
|
400
|
+
const parsed = parseArtifactName(name);
|
|
401
|
+
if (parsed === undefined)
|
|
402
|
+
continue;
|
|
403
|
+
if (best !== undefined) {
|
|
404
|
+
if (parsed.version < best.version)
|
|
405
|
+
continue;
|
|
406
|
+
if (parsed.version === best.version && (best.compressed || !parsed.compressed))
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
best = { path: join(sessionDir, name), version: parsed.version, compressed: parsed.compressed };
|
|
410
|
+
}
|
|
411
|
+
return best;
|
|
342
412
|
}
|
|
343
413
|
async function readMarker(sessionDir) {
|
|
344
414
|
try {
|
|
@@ -388,7 +458,7 @@ async function repairOneFile(artifact, sessionId, projectDirName, backupRoot, ma
|
|
|
388
458
|
else {
|
|
389
459
|
plaintext = raw;
|
|
390
460
|
}
|
|
391
|
-
const outcome = patchPlaintext(plaintext);
|
|
461
|
+
const outcome = patchPlaintext(plaintext, artifact.version === SESSION_FORMAT_VERSION ? 'mark' : 'neutralize', artifact.version);
|
|
392
462
|
if (outcome.kind === 'skip')
|
|
393
463
|
return { kind: 'skipped', reason: outcome.reason, detail: outcome.detail };
|
|
394
464
|
// 布局非法时即使无需补标也要重写为合法容器(其余情况才允许 clean 跳过)
|
|
@@ -433,8 +503,15 @@ async function repairOneFile(artifact, sessionId, projectDirName, backupRoot, ma
|
|
|
433
503
|
return { kind: 'skipped', reason: 'busy', detail: error instanceof Error ? error.message : String(error) };
|
|
434
504
|
}
|
|
435
505
|
await pruneBackups(dirnameOf(backupPath), `${sessionId}-`, maxBackupPerSession);
|
|
436
|
-
|
|
437
|
-
|
|
506
|
+
// neutralize 后文件内不再有星愿事件:标记计数归零,避免下次启动虚报「已补标」
|
|
507
|
+
await writeMarker(dirnameOf(artifact.path), {
|
|
508
|
+
artifactBytes: nextBytes.length,
|
|
509
|
+
eventsMarked: outcome.eventsNeutralized > 0 ? 0 : outcome.totalMarked,
|
|
510
|
+
});
|
|
511
|
+
if (outcome.eventsNeutralized > 0) {
|
|
512
|
+
return { kind: 'patched', sessionId, eventsMarked: 0, eventsNeutralized: outcome.eventsNeutralized, totalMarked: outcome.totalMarked };
|
|
513
|
+
}
|
|
514
|
+
return { kind: 'patched', sessionId, eventsMarked: outcome.eventsMarked, eventsNeutralized: 0, totalMarked: outcome.totalMarked, relayoutOnly: outcome.kind === 'clean' };
|
|
438
515
|
}
|
|
439
516
|
function dirnameOf(path) {
|
|
440
517
|
const index = path.lastIndexOf('/');
|
package/lib/types/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 1) 激活期把包内 preset 发布到用户根(preset-root.ts);
|
|
4
4
|
* 2) 打开 xingyuan 领域并发布同名服务;
|
|
5
5
|
* 3) 注册 /xingyuan/* 数据 API 与页面路由;
|
|
6
|
-
* 4)
|
|
6
|
+
* 4) 激活期对会话日志做自愈(当前格式补 ignorable、旧格式去毒,见 session-log-repair.ts)。
|
|
7
7
|
* (sqlite 后端在独立行 '@starwish-ai/xingyuan-dsh/sqlite',见 cordis.patch.yml。)
|
|
8
8
|
*/
|
|
9
9
|
import type { Context } from '@deepseek-ai/cordis';
|
|
@@ -21,9 +21,10 @@ export interface Config {
|
|
|
21
21
|
/** 记忆列表单页条数(分页端点缺省 limit)。 */
|
|
22
22
|
memoryListLimit: number;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* 激活期会话日志自愈(默认开):当前格式日志补 `"ignorable": true` 标记,
|
|
25
|
+
* 旧格式日志的 xingyuan/* 事件替换为官方便可迁移的惰性事件(详见
|
|
26
|
+
* session-log-repair.ts 头注;dsh 0.1.5 起历史迁移链拒绝未知事件)。
|
|
27
|
+
* 不含星愿事件的文件零写入。
|
|
27
28
|
*/
|
|
28
29
|
repairSessionLogs: boolean;
|
|
29
30
|
}
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 会话日志自愈(bundle 常驻层)。
|
|
3
3
|
*
|
|
4
|
-
* 背景:dsh
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* 背景:dsh 会话持久化读取端按「本仓库生成的官方事件类型白名单」拒绝未知事件;
|
|
5
|
+
* 仓库外插件事件按构造不在名单内,而写入端(Session.append)至今没有 ignorable
|
|
6
|
+
* 标记通道。0.1.5-rc.2 的规则分两档:
|
|
7
|
+
* - 当前格式(v3,工件名 session.v3.jsonl[.zstd]):读取端认 `ignorable: true`,
|
|
8
|
+
* 不认识的带标事件被安全跳过且数据原样保留;
|
|
9
|
+
* - 历史格式迁移(v0/v1/v2 → v3):迁移链**拒绝一切未知历史事件,ignorable
|
|
10
|
+
* 也不例外**(官方 alpha-historical-unknown-event-refusal 决策)——旧日志里的
|
|
11
|
+
* xingyuan/* 事件会让整个会话冷加载失败,且不产出 v3 后继、源文件原样保留。
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* 本模块在激活期扫描 $DSH_HOME/sessions,按工件版本分两种处理:
|
|
14
|
+
* - 当前版本工件:补标模式——xingyuan/* 未标记事件行补 `"ignorable": true`;
|
|
15
|
+
* - 历史版本工件:去毒模式——xingyuan/* 事件行替换为官方惰性事件 hook/invoked
|
|
16
|
+
* (seq/time 不变),迁移链即可安全穿过、会话照常打开;代价是该会话历史卡片
|
|
17
|
+
* 事件不再回放(业务数据在 sqlite,不丢失)。仅当目录内无当前版本工件时才动
|
|
18
|
+
* 历史工件(dsh 只按最新代读取,旧代已不被消费)。
|
|
14
19
|
*
|
|
15
20
|
* 安全约束(改前必读,勿删):
|
|
16
21
|
* - 不含星愿事件的会话文件零写入(字节与 mtime 均不变);
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
22
|
+
* - 只有星愿事件行被重序列化(补标仅追加字段;去毒仅换 type/data),其余行原样
|
|
23
|
+
* 保留字节;行序与 seq 由构造保证不变(本模块从不增删移动任何行);
|
|
19
24
|
* - 首次改写前把原件备份到 <home>/xingyuan/session-backups/<项目目录>/,
|
|
20
25
|
* 每会话保留最近 maxBackupPerSession 份;
|
|
21
26
|
* - 任何看不懂的情况一律整文件跳过不动:撕裂尾、坏帧、header 版本不符、
|
|
@@ -28,7 +33,8 @@
|
|
|
28
33
|
* (历史上一次实现曾把整个文件重压成单帧导致宿主启动崩溃,已由本模块的
|
|
29
34
|
* relayout-only 路径兜底自愈),即使无需补标也会重写为合法布局。
|
|
30
35
|
* - SESSION_FORMAT_VERSION 取自 @deepseek-ai/dsh-session。升级 dsh 时必须核对
|
|
31
|
-
*
|
|
36
|
+
* 本模块的前提与布局契约仍然成立(尤其迁移链是否改为接受 ignorable 历史事件——
|
|
37
|
+
* 若接受,去毒模式可回归补标)。
|
|
32
38
|
*/
|
|
33
39
|
/** $DSH_HOME 解析:环境变量优先,否则 ~/.dsh。 */
|
|
34
40
|
export declare function resolveDshHome(): string;
|
|
@@ -54,8 +60,10 @@ export interface RepairReport {
|
|
|
54
60
|
scanned: number;
|
|
55
61
|
/** 实际改写的会话文件数。 */
|
|
56
62
|
patched: number;
|
|
57
|
-
/** 补上 ignorable
|
|
63
|
+
/** 补上 ignorable 标记的事件总数(当前格式工件)。 */
|
|
58
64
|
eventsMarked: number;
|
|
65
|
+
/** 旧格式工件中替换为惰性事件的星愿事件总数(迁移链拒绝未知历史事件)。 */
|
|
66
|
+
neutralized: number;
|
|
59
67
|
skipped: Partial<Record<SkipReason, number>>;
|
|
60
68
|
warnings: string[];
|
|
61
69
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starwish-ai/xingyuan-dsh",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "星愿 XingYuan for DeepSeek Harness: 对话创建愿望/任务、机会日打卡、微行动、记忆与图表的 Agent Preset 组合包",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -93,14 +93,14 @@
|
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
95
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
96
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
97
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
98
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
99
|
-
"@deepseek-ai/dsh-storage": "^0.1.
|
|
100
|
-
"@deepseek-ai/dsh-storage-domain": "^0.1.
|
|
101
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.
|
|
102
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
103
|
-
"@deepseek-ai/dsh-user-questions": "^0.1.
|
|
96
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
|
|
97
|
+
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
98
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
|
|
99
|
+
"@deepseek-ai/dsh-storage": "^0.1.5-rc.2",
|
|
100
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
|
|
101
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
|
|
102
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
103
|
+
"@deepseek-ai/dsh-user-questions": "^0.1.5-rc.2",
|
|
104
104
|
"@deepseek-ai/schemastery": "*",
|
|
105
105
|
"react": "^18.2.0",
|
|
106
106
|
"zod": "^4.4.3"
|
|
@@ -113,27 +113,28 @@
|
|
|
113
113
|
"devDependencies": {
|
|
114
114
|
"@deepseek-ai/cordis": "4.0.2",
|
|
115
115
|
"@deepseek-ai/cordis-plugin-loader": "1.0.2",
|
|
116
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
117
|
-
"@deepseek-ai/dsh-api-remotes": "0.1.
|
|
118
|
-
"@deepseek-ai/dsh-api-session-controller": "0.1.
|
|
119
|
-
"@deepseek-ai/dsh-attachment": "0.1.
|
|
120
|
-
"@deepseek-ai/dsh-client-locale": "0.1.
|
|
121
|
-
"@deepseek-ai/dsh-client-store": "0.1.
|
|
122
|
-
"@deepseek-ai/dsh-client-ui-chat": "0.1.
|
|
123
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.1.
|
|
124
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.1.
|
|
125
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.1.
|
|
126
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.
|
|
127
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.1.
|
|
128
|
-
"@deepseek-ai/dsh-host-webserver": "0.1.
|
|
129
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
130
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
131
|
-
"@deepseek-ai/dsh-
|
|
132
|
-
"@deepseek-ai/dsh-
|
|
133
|
-
"@deepseek-ai/dsh-storage
|
|
134
|
-
"@deepseek-ai/dsh-
|
|
135
|
-
"@deepseek-ai/dsh-
|
|
136
|
-
"@deepseek-ai/dsh-
|
|
116
|
+
"@deepseek-ai/dsh-agent": "0.1.5-rc.2",
|
|
117
|
+
"@deepseek-ai/dsh-api-remotes": "0.1.5-rc.2",
|
|
118
|
+
"@deepseek-ai/dsh-api-session-controller": "0.1.5-rc.2",
|
|
119
|
+
"@deepseek-ai/dsh-attachment": "0.1.5-rc.2",
|
|
120
|
+
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
|
|
121
|
+
"@deepseek-ai/dsh-client-store": "0.1.5-rc.2",
|
|
122
|
+
"@deepseek-ai/dsh-client-ui-chat": "0.1.5-rc.2",
|
|
123
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2",
|
|
124
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.2",
|
|
125
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
|
|
126
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.5-rc.2",
|
|
127
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
|
|
128
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
|
|
129
|
+
"@deepseek-ai/dsh-llm": "0.1.5-rc.2",
|
|
130
|
+
"@deepseek-ai/dsh-session": "0.1.5-rc.2",
|
|
131
|
+
"@deepseek-ai/dsh-session-format-catalog": "0.1.5-rc.2",
|
|
132
|
+
"@deepseek-ai/dsh-settings": "0.1.5-rc.2",
|
|
133
|
+
"@deepseek-ai/dsh-storage": "0.1.5-rc.2",
|
|
134
|
+
"@deepseek-ai/dsh-storage-domain": "0.1.5-rc.2",
|
|
135
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.5-rc.2",
|
|
136
|
+
"@deepseek-ai/dsh-tools": "0.1.5-rc.2",
|
|
137
|
+
"@deepseek-ai/dsh-user-questions": "0.1.5-rc.2",
|
|
137
138
|
"@deepseek-ai/schemastery": "^3.18.0",
|
|
138
139
|
"@types/node": "^24.0.0",
|
|
139
140
|
"@types/react": "~18.3.1",
|