dsh-compact-relay 0.5.2 → 0.5.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 +55 -0
- package/README.en.md +3 -0
- package/README.md +28 -3
- package/lib/actions/better-compact.js +66 -6
- package/lib/actions/compact-run.js +54 -5
- package/lib/actions/favorite.js +435 -0
- package/lib/actions/force-archive.js +60 -12
- package/lib/actions/handoff.js +3 -1
- package/lib/actions/recover.js +17 -4
- package/lib/actions/state.js +6 -0
- package/lib/adapters/commands.js +36 -0
- package/lib/adapters/http.js +90 -12
- package/lib/client.js +654 -101
- package/lib/config.js +8 -0
- package/lib/core/config-schema.js +23 -0
- package/lib/core/errors.js +9 -3
- package/lib/core/favorite-score.js +220 -0
- package/lib/core/favorites.js +167 -0
- package/lib/core/relay-record.js +3 -0
- package/lib/dsh/favorite-scorer.js +91 -0
- package/lib/dsh/favorite-watch.js +244 -0
- package/lib/dsh/favorites-file.js +349 -0
- package/lib/dsh/relay.js +3 -0
- package/lib/dsh/select-range.js +2 -2
- package/lib/index.js +13 -2
- package/package.json +2 -2
package/lib/adapters/commands.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { betterCompact, previewBetterCompact, takePreviewPlan } from '../actions/better-compact.js';
|
|
8
8
|
import { handoff } from '../actions/handoff.js';
|
|
9
9
|
import { listPins, pinByIndex, pinLastTurn, unpinAll } from '../actions/pin.js';
|
|
10
|
+
import { favoriteLast, listFavoriteSuggestions, listFavorites, markFavoriteInSession, readFavoriteFull, readFavoriteInfo, unfavoriteByIndex } from '../actions/favorite.js';
|
|
10
11
|
import { testCompact } from '../actions/test-compact.js';
|
|
11
12
|
import { errorText } from '../core/errors.js';
|
|
12
13
|
|
|
@@ -104,6 +105,41 @@ export function registerCommands(ctx, deps) {
|
|
|
104
105
|
),
|
|
105
106
|
});
|
|
106
107
|
|
|
108
|
+
register({
|
|
109
|
+
name: 'favorite',
|
|
110
|
+
description: '收藏消息:/favorite 列表|/favorite mark [序号] 收藏(不带序号=最后一条回复)|/favorite last 收藏我刚发的|/favorite <序号> 看体积|/favorite <序号> read 读原文|/favorite rm <序号> 取消',
|
|
111
|
+
input: { hint: 'list | mark [序号] | last | suggest | 序号 | 序号 read | rm 序号' },
|
|
112
|
+
handler: (invocation) =>
|
|
113
|
+
track(
|
|
114
|
+
(async () => {
|
|
115
|
+
try {
|
|
116
|
+
const raw = invocation.rawInput.trim();
|
|
117
|
+
const lower = raw.toLowerCase();
|
|
118
|
+
const agent = invocation.agent;
|
|
119
|
+
let outcome = null;
|
|
120
|
+
if (raw === '' || lower === 'list') outcome = listFavorites(deps, { agent });
|
|
121
|
+
else if (lower === 'last') outcome = favoriteLast(deps, { agent });
|
|
122
|
+
else if (lower === 'suggest') outcome = listFavoriteSuggestions(deps, { agent });
|
|
123
|
+
else if (/^mark$/i.test(raw)) outcome = markFavoriteInSession(deps, { agent });
|
|
124
|
+
else if (/^mark\s+\d+$/i.test(raw)) outcome = markFavoriteInSession(deps, { agent, seq: Number(raw.slice(4).trim()) });
|
|
125
|
+
else if (/^\d+$/.test(raw)) outcome = readFavoriteInfo(deps, { agent, index: Number(raw) });
|
|
126
|
+
else {
|
|
127
|
+
const readMatch = /^(\d+)\s+read$/i.exec(raw);
|
|
128
|
+
const rmMatch = /^rm\s+(\d+)$/i.exec(raw);
|
|
129
|
+
if (readMatch !== null) outcome = readFavoriteFull(deps, { agent, index: Number(readMatch[1]) });
|
|
130
|
+
else if (rmMatch !== null) outcome = unfavoriteByIndex(deps, { agent, index: Number(rmMatch[1]) });
|
|
131
|
+
}
|
|
132
|
+
if (outcome === null) {
|
|
133
|
+
return { kind: 'error', text: '用法:/favorite(列表)|/favorite mark [序号](收藏,不带序号=最后一条回复)|/favorite last|/favorite <序号>(看体积)|/favorite <序号> read(读原文)|/favorite rm <序号>(取消)' };
|
|
134
|
+
}
|
|
135
|
+
return success(outcome);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return failure(error, invocation.signal);
|
|
138
|
+
}
|
|
139
|
+
})(),
|
|
140
|
+
),
|
|
141
|
+
});
|
|
142
|
+
|
|
107
143
|
register({
|
|
108
144
|
name: 'pin',
|
|
109
145
|
description: '锁定重要消息:/pin 锁定最近一轮|/pin list 按序号挑|/pin clear 全部解锁',
|
package/lib/adapters/http.js
CHANGED
|
@@ -12,6 +12,7 @@ import { listHandoffDocs, readHandoffDoc } from '../actions/handoff-docs.js';
|
|
|
12
12
|
import { recoverArchivedSession } from '../actions/recover.js';
|
|
13
13
|
import { readCompactRelayChain } from '../actions/relay-chain.js';
|
|
14
14
|
import { togglePin } from '../actions/pin.js';
|
|
15
|
+
import { markFavoriteBySeq, ownerAgentOfMessage, toggleFavorite, trialFavoriteScore } from '../actions/favorite.js';
|
|
15
16
|
import { setNativeAutoCompactionEnabled } from '../actions/native-compaction.js';
|
|
16
17
|
import { readState, resolveTarget } from '../actions/state.js';
|
|
17
18
|
import { errorText } from '../core/errors.js';
|
|
@@ -443,12 +444,27 @@ export function registerHttp(webCtx, deps) {
|
|
|
443
444
|
// - force-archive 同样作用在**旧会话**(交接前的那个)上,通常没有活 agent 可解;
|
|
444
445
|
// 它自己的动作层会校验 sessionId 与 confirmStop,不走 resolveTarget。
|
|
445
446
|
let agent = null;
|
|
446
|
-
|
|
447
|
+
// favorite-toggle 例外:messageId 是宿主 UUID,客户端没带对 sessionId 也能由
|
|
448
|
+
// ownerAgentOfMessage 反查所属会话(2026-10-29 真机 BUG:多会话时点星静默失败)。
|
|
449
|
+
const sessionOptionalAction =
|
|
450
|
+
parsed.action === 'native-auto' ||
|
|
451
|
+
parsed.action === 'recover' ||
|
|
452
|
+
parsed.action === 'force-archive' ||
|
|
453
|
+
parsed.action === 'favorite-toggle' ||
|
|
454
|
+
parsed.action === 'favorite-mark';
|
|
455
|
+
if (!sessionOptionalAction) {
|
|
447
456
|
if (typeof parsed.sessionId !== 'string' || parsed.sessionId.length === 0) {
|
|
448
457
|
return send(res, 400, { ok: false, message: '缺少 sessionId' });
|
|
449
458
|
}
|
|
450
459
|
agent = resolveTarget(deps, { sessionId: parsed.sessionId });
|
|
451
460
|
if (agent === null) return send(res, 409, { ok: false, message: '没有活动会话' });
|
|
461
|
+
} else if (
|
|
462
|
+
parsed.action === 'favorite-toggle' &&
|
|
463
|
+
typeof parsed.sessionId === 'string' &&
|
|
464
|
+
parsed.sessionId.length > 0
|
|
465
|
+
) {
|
|
466
|
+
// 带了 sessionId 就按它取 agent;取不到(会话已关)也继续,后面按 messageId 反查。
|
|
467
|
+
agent = resolveTarget(deps, { sessionId: parsed.sessionId });
|
|
452
468
|
}
|
|
453
469
|
|
|
454
470
|
if (parsed.action === 'recover') {
|
|
@@ -462,19 +478,14 @@ export function registerHttp(webCtx, deps) {
|
|
|
462
478
|
|
|
463
479
|
if (parsed.action === 'force-archive') {
|
|
464
480
|
// 0.5.2 L 项:用户确认「会停止旧会话上正在跑的任务」后强制归档(唯一 stopActivity 调用点)。
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
return send(res, 400, {
|
|
470
|
-
ok: false,
|
|
471
|
-
code: 'force-archive',
|
|
472
|
-
message: '强制归档会停止旧会话上正在跑的任务,需要显式确认(confirmStop:true)',
|
|
473
|
-
});
|
|
481
|
+
// 0.5.3 P2-1:这里不再提前 400 —— 已归档 + 没带确认应回 already(动作层先判幂等),
|
|
482
|
+
// 只有「真要归档」的路径才要求 confirmStop:true(动作层给 statusCode 400)。
|
|
483
|
+
if (parsed.confirmStop !== undefined && typeof parsed.confirmStop !== 'boolean') {
|
|
484
|
+
return send(res, 400, { ok: false, message: 'confirmStop 必须是 true/false(true = 确认停止旧会话上的任务后归档)' });
|
|
474
485
|
}
|
|
475
486
|
const outcome = await forceArchiveSession(deps, {
|
|
476
487
|
sessionId: parsed.sessionId,
|
|
477
|
-
confirmStop:
|
|
488
|
+
confirmStop: parsed.confirmStop,
|
|
478
489
|
confirmUnpin: parsed.confirmUnpin,
|
|
479
490
|
});
|
|
480
491
|
return send(res, 200, outcome);
|
|
@@ -509,6 +520,62 @@ export function registerHttp(webCtx, deps) {
|
|
|
509
520
|
const outcome = await betterCompact(deps, { agent, expected });
|
|
510
521
|
return send(res, 200, { ok: true, message: outcome.message });
|
|
511
522
|
}
|
|
523
|
+
if (parsed.action === 'favorite-score-trial') {
|
|
524
|
+
// 0.5.3 P1-7:设置页「试算」——对一段文本按当前(可覆盖)配置跑一次打分+判定。
|
|
525
|
+
// 只读:不写收藏、不影响任何状态;没有活会话拿不到模型路由时明确 409。
|
|
526
|
+
if (agent === null) return send(res, 409, { ok: false, message: '没有活动会话,无法试算(先发一条消息唤醒 agent)' });
|
|
527
|
+
const text = typeof parsed.text === 'string' ? parsed.text : '';
|
|
528
|
+
if ([...text.trim()].length === 0) return send(res, 400, { ok: false, message: '缺少要试算的文本' });
|
|
529
|
+
const outcome = await trialFavoriteScore(deps, {
|
|
530
|
+
agent,
|
|
531
|
+
text,
|
|
532
|
+
level: parsed.level,
|
|
533
|
+
mustWords: parsed.mustWords,
|
|
534
|
+
muteWords: parsed.muteWords,
|
|
535
|
+
});
|
|
536
|
+
return send(res, 200, { ok: true, ...outcome });
|
|
537
|
+
}
|
|
538
|
+
if (parsed.action === 'favorite-mark') {
|
|
539
|
+
// 0.5.3:标记层解耦——按 sessionId+seq 收藏,**不需要活会话**(归档/关闭的会话也能标)。
|
|
540
|
+
// 只写一行 JSONL;原文等压缩时落档,不读会话日志、不碰压缩。
|
|
541
|
+
if (parsed.active !== undefined && typeof parsed.active !== 'boolean') {
|
|
542
|
+
return send(res, 400, { ok: false, message: 'active 必须是 true/false(省略=切换)' });
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
const outcome = await markFavoriteBySeq(deps, {
|
|
546
|
+
sessionId: parsed.sessionId,
|
|
547
|
+
seq: Number(parsed.seq),
|
|
548
|
+
active: parsed.active,
|
|
549
|
+
});
|
|
550
|
+
return send(res, 200, { ok: true, ...outcome });
|
|
551
|
+
} catch (error) {
|
|
552
|
+
const code = error !== null && typeof error === 'object' ? error.code : undefined;
|
|
553
|
+
return send(res, code === 'favorite' ? 400 : 500, {
|
|
554
|
+
ok: false,
|
|
555
|
+
...(code === undefined ? {} : { code }),
|
|
556
|
+
message: errorText(error),
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (parsed.action === 'favorite-toggle') {
|
|
561
|
+
// 0.6:助手消息底部星标——按宿主 messageId 收藏/取消。
|
|
562
|
+
// 客户端 sessionId 可能缺失或过期:messageId 唯一,按它反查所属活会话。
|
|
563
|
+
const messageId =
|
|
564
|
+
typeof parsed.messageId === 'string' && parsed.messageId.length > 0 ? parsed.messageId : null;
|
|
565
|
+
if (messageId === null) return send(res, 400, { ok: false, message: '缺少 messageId' });
|
|
566
|
+
try {
|
|
567
|
+
const actor = agent ?? ownerAgentOfMessage(deps, messageId);
|
|
568
|
+
const outcome = toggleFavorite(deps, { agent: actor, messageId });
|
|
569
|
+
return send(res, 200, { ok: true, ...outcome });
|
|
570
|
+
} catch (error) {
|
|
571
|
+
const code = error !== null && typeof error === 'object' ? error.code : undefined;
|
|
572
|
+
return send(res, code === 'favorite' ? 400 : 500, {
|
|
573
|
+
ok: false,
|
|
574
|
+
...(code === undefined ? {} : { code }),
|
|
575
|
+
message: errorText(error),
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
512
579
|
if (parsed.action === 'pin-toggle') {
|
|
513
580
|
// M6:菜单「锁定重要消息 / 解锁消息」——有锁定就清空,没有就锁最近一轮。
|
|
514
581
|
const outcome = togglePin(deps, { agent });
|
|
@@ -573,13 +640,24 @@ export function registerHttp(webCtx, deps) {
|
|
|
573
640
|
return send(res, 409, { ok: false, code, message: errorText(error) });
|
|
574
641
|
}
|
|
575
642
|
// 0.5.2 L 项:强制归档失败要把**具体原因**带回界面(不静默、不吞成通用文案)。
|
|
643
|
+
// 0.5.3 P2-2:状态码由动作层标注(未知会话 404 / 宿主缺接缝 503 / 内部错 500),
|
|
644
|
+
// 不再一律 400。
|
|
576
645
|
if (code === 'force-archive') {
|
|
577
|
-
|
|
646
|
+
const status = Number.isInteger(error?.statusCode) ? error.statusCode : 400;
|
|
647
|
+
return send(res, status, {
|
|
578
648
|
ok: false,
|
|
579
649
|
code,
|
|
580
650
|
message: error instanceof Error ? error.message : String(error),
|
|
581
651
|
});
|
|
582
652
|
}
|
|
653
|
+
// 0.5.3 P2-2:recover 等动作层显式标注的状态码(404/503/500)直接映射。
|
|
654
|
+
if (Number.isInteger(error?.statusCode) && error.statusCode >= 400) {
|
|
655
|
+
return send(res, error.statusCode, {
|
|
656
|
+
ok: false,
|
|
657
|
+
...(code === undefined ? {} : { code }),
|
|
658
|
+
message: error instanceof Error ? error.message : String(error),
|
|
659
|
+
});
|
|
660
|
+
}
|
|
583
661
|
if (error?.statusCode === 413) {
|
|
584
662
|
return send(res, 413, { ok: false, message: '请求体太大' });
|
|
585
663
|
}
|