@jiugeqqqqq/dsh-house2 0.2.0 → 0.2.2
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 +9 -0
- package/lib/host/index.js +88 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
> 每次发版都必须在这里写一条:改了什么、为什么改、别人要不要跟着动。
|
|
4
4
|
> 版本号跟 `package.json` 走,`npm i` 的人看这里就知道升了什么。
|
|
5
5
|
|
|
6
|
+
## 0.2.2 — 2026-10-30
|
|
7
|
+
|
|
8
|
+
- 压缩改成**按 token 触发**(不再挂在「任务完成」上):house2 自己统计累计 token(口径同 dsh-session-rotate,监听 `assistant/message` 的 `usage`),到阈值 80% 就在注入里提醒「该准备交接/换会话」。阈值 `thresholdTokens`,默认 1 亿。
|
|
9
|
+
- 不自己调压缩(DSH 没给插件公开的手动压缩接口),只按 token 提醒;工段只负责记任务。
|
|
10
|
+
|
|
11
|
+
## 0.2.1 — 2026-10-30
|
|
12
|
+
|
|
13
|
+
- `house_search` 加 `stage` 参数:先按工段名找到它挂的积木文件(`stage_done` 时写的「积木:xxx」),只在那一个文件里搜,比全目录翻更快更准。不传就还是全目录搜。
|
|
14
|
+
|
|
6
15
|
## 0.2.0 — 2026-10-30
|
|
7
16
|
|
|
8
17
|
- 新功能:**工段(任务段)**。不再按轮数切,按「这件事做完了没有」切——`stage_start` 开一座、`stage_done` 收掉(写结果 + 积木文件名)。
|
package/lib/host/index.js
CHANGED
|
@@ -125,6 +125,56 @@ export function apply(ctx, config = {}) {
|
|
|
125
125
|
stages.ensure();
|
|
126
126
|
ctx.logger?.info('dsh-house2: 工段文件 = %s', stagesPath);
|
|
127
127
|
|
|
128
|
+
// 2026-10-30:压缩改成按 token 触发(不再挂在「任务完成」上)。
|
|
129
|
+
// 口径跟 dsh-session-rotate 一样:监听 session/event 里 assistant/message 的 data.usage。
|
|
130
|
+
// 到阈值就在注入里提醒「该交接/换会话」,不自己调压缩(DSH 没给插件公开的手动压缩接口)。
|
|
131
|
+
const tokenStatePath = path.join(dshHome, 'house2-token.json');
|
|
132
|
+
const tokenState = { sessionId: '', usedTokens: 0 };
|
|
133
|
+
try {
|
|
134
|
+
Object.assign(tokenState, JSON.parse(fs.readFileSync(tokenStatePath, 'utf8')));
|
|
135
|
+
} catch {}
|
|
136
|
+
let tokenSaveTimer = null;
|
|
137
|
+
const saveTokenState = () => {
|
|
138
|
+
if (tokenSaveTimer) return;
|
|
139
|
+
tokenSaveTimer = setTimeout(() => {
|
|
140
|
+
tokenSaveTimer = null;
|
|
141
|
+
try {
|
|
142
|
+
fs.writeFileSync(tokenStatePath, JSON.stringify(tokenState), 'utf8');
|
|
143
|
+
} catch {}
|
|
144
|
+
}, 1000);
|
|
145
|
+
tokenSaveTimer.unref?.();
|
|
146
|
+
};
|
|
147
|
+
const tokenThreshold = Math.max(0, Number(config.thresholdTokens) || 100000000);
|
|
148
|
+
try {
|
|
149
|
+
ctx.on('session/event', (session, event) => {
|
|
150
|
+
try {
|
|
151
|
+
const sid = String(session?.id ?? session?.sessionId ?? '');
|
|
152
|
+
if (sid && sid !== tokenState.sessionId) {
|
|
153
|
+
tokenState.sessionId = sid;
|
|
154
|
+
tokenState.usedTokens = 0;
|
|
155
|
+
saveTokenState();
|
|
156
|
+
}
|
|
157
|
+
if (event?.type !== 'assistant/message') return;
|
|
158
|
+
const u = event?.data?.usage;
|
|
159
|
+
if (!u) return;
|
|
160
|
+
const total = Number(u.totalTokens);
|
|
161
|
+
const add =
|
|
162
|
+
Number.isFinite(total) && total > 0
|
|
163
|
+
? total
|
|
164
|
+
: (Number(u.inputTokens) || 0) +
|
|
165
|
+
(Number(u.outputTokens) || 0) +
|
|
166
|
+
(Number(u.cacheReadTokens) || 0) +
|
|
167
|
+
(Number(u.cacheWriteTokens) || 0);
|
|
168
|
+
if (add > 0) {
|
|
169
|
+
tokenState.usedTokens += add;
|
|
170
|
+
saveTokenState();
|
|
171
|
+
}
|
|
172
|
+
} catch {}
|
|
173
|
+
});
|
|
174
|
+
} catch (e) {
|
|
175
|
+
ctx.logger?.warn('dsh-house2: 挂 token 统计失败:' + (e && e.message ? e.message : e));
|
|
176
|
+
}
|
|
177
|
+
|
|
128
178
|
// 版本自检:只查 npm 有没有新版,不自动安装。
|
|
129
179
|
// 注意:本地开发目录的 package.json name 是 dsh-house2,而 npm 上是 scoped 包,所以写死 npmName。
|
|
130
180
|
const updateChecker = createUpdateChecker({
|
|
@@ -528,11 +578,12 @@ export function apply(ctx, config = {}) {
|
|
|
528
578
|
return out;
|
|
529
579
|
}
|
|
530
580
|
|
|
531
|
-
function searchKnowledge(query, limit) {
|
|
581
|
+
function searchKnowledge(query, limit, onlyFiles) {
|
|
532
582
|
const q = String(query ?? '').trim().toLowerCase();
|
|
533
583
|
if (!q) return { ok: false, error: { code: 'invalid', message: '要给个关键词。' } };
|
|
534
584
|
const max = Math.max(1, Math.min(30, Number(limit) || 8));
|
|
535
|
-
|
|
585
|
+
// 2026-10-30:给了 onlyFiles 就只在这些积木里搜(工段定向检索)。
|
|
586
|
+
const files = Array.isArray(onlyFiles) && onlyFiles.length ? onlyFiles : walkKnowledge(knowledgeDir);
|
|
536
587
|
const hits = [];
|
|
537
588
|
for (const f of files) {
|
|
538
589
|
if (hits.length >= max) break;
|
|
@@ -557,6 +608,22 @@ export function apply(ctx, config = {}) {
|
|
|
557
608
|
return { ok: true, data: { query: q, hits, scanned: files.length } };
|
|
558
609
|
}
|
|
559
610
|
|
|
611
|
+
/** 按工段名找它挂的积木文件(stage_done 时写的「积木:xxx」)。 */
|
|
612
|
+
function stageBrickFiles(stageName) {
|
|
613
|
+
const name = String(stageName ?? '').trim();
|
|
614
|
+
if (!name) return null;
|
|
615
|
+
const { doing, done } = stages.parse();
|
|
616
|
+
const all = [...doing, ...done];
|
|
617
|
+
const hit = all.find((x) => x.name === name) || all.find((x) => x.name.includes(name) || name.includes(x.name));
|
|
618
|
+
if (!hit) return { error: `没有叫「${name}」的工段` };
|
|
619
|
+
const m = hit.line.match(/积木[::]\s*([^\s|]+)/);
|
|
620
|
+
if (!m) return { error: `工段「${hit.name}」没有记积木文件名` };
|
|
621
|
+
const base = m[1].replace(/\.md$/i, '');
|
|
622
|
+
const file = path.join(knowledgeDir, `${base}.md`);
|
|
623
|
+
if (!fs.existsSync(file)) return { error: `积木文件不存在:${base}.md` };
|
|
624
|
+
return { files: [file], brick: base };
|
|
625
|
+
}
|
|
626
|
+
|
|
560
627
|
|
|
561
628
|
// 2026-10-27:以前「房子清单能不能进模型上下文」要靠**另一个插件**
|
|
562
629
|
// (dsh-session-rotate 的 assetsPath) 配一下才行 —— 等于装了这个插件的人也
|
|
@@ -569,7 +636,8 @@ export function apply(ctx, config = {}) {
|
|
|
569
636
|
'写积木分两种:按能力写(`手机-接入与操作`、`画图-Agnes`,回答「怎么做」)、按任务写(`任务-调通一直发`,回答「那段对话干了什么」,一段任务一篇,按时间轴切,切完要覆盖全程、不重不漏);' +
|
|
570
637
|
'细节、命令、踩过的坑,用 house_write 写进积木(knowledge 目录,默认追加,别一个坑建一个文件);' +
|
|
571
638
|
'一件事真做完了(跑通、验证过),用 house_done 把它从「正在长出来的」搬到「已经完工的」——别让旧条目一直挂着;' +
|
|
572
|
-
'清单和最近对话里找不到的东西,用 house_search
|
|
639
|
+
'清单和最近对话里找不到的东西,用 house_search 去积木里按关键词翻,翻到再答,别猜(先按工段定位,传 stage 参数只搜它挂的那一篇);' +
|
|
640
|
+
'压缩和换会话不归这里管——那是按累计 token 阈值来的(session-rotate),工段只负责把任务记清楚。';
|
|
573
641
|
|
|
574
642
|
function assetsAsPrompt() {
|
|
575
643
|
let text = '';
|
|
@@ -578,7 +646,15 @@ export function apply(ctx, config = {}) {
|
|
|
578
646
|
} catch {}
|
|
579
647
|
const stageText = stages.renderPrompt();
|
|
580
648
|
const base = text ? `【资产清单】\n${text}\n\n${HOUSE_GUIDE}` : HOUSE_GUIDE;
|
|
581
|
-
|
|
649
|
+
// 2026-10-30:压缩按 token 走,到 80% 就在这里提醒一句。
|
|
650
|
+
const used = tokenState.usedTokens;
|
|
651
|
+
let tokenLine = '';
|
|
652
|
+
if (used > 0 && tokenThreshold > 0) {
|
|
653
|
+
const pct = Math.round((used / tokenThreshold) * 100);
|
|
654
|
+
tokenLine = `【token】已用 ${(used / 10000).toFixed(0)} 万 / 阈值 ${(tokenThreshold / 10000).toFixed(0)} 万(${pct}%)`;
|
|
655
|
+
if (pct >= 80) tokenLine += ' —— 该准备交接、收尾换会话了(压缩按 token 触发,不归工段管)';
|
|
656
|
+
}
|
|
657
|
+
return [base, stageText, tokenLine].filter(Boolean).join('\n\n');
|
|
582
658
|
}
|
|
583
659
|
|
|
584
660
|
// 2026-10-30:改成直接 ctx.systemPrompt.section(跟 dsh-affection 一样)。
|
|
@@ -659,13 +735,20 @@ export function apply(ctx, config = {}) {
|
|
|
659
735
|
properties: {
|
|
660
736
|
query: { type: 'string', description: '关键词,比如 抖音、画图、报错信息里的词' },
|
|
661
737
|
limit: { type: 'number', description: '最多返回几处,默认 8' },
|
|
738
|
+
stage: { type: 'string', description: '可选:先定位到这个工段,只搜它挂的积木文件(更快更准)' },
|
|
662
739
|
},
|
|
663
740
|
required: ['query'],
|
|
664
741
|
additionalProperties: false,
|
|
665
742
|
},
|
|
666
743
|
output,
|
|
667
744
|
async execute(args) {
|
|
668
|
-
|
|
745
|
+
let onlyFiles = null;
|
|
746
|
+
if (args?.stage) {
|
|
747
|
+
const sb = stageBrickFiles(args.stage);
|
|
748
|
+
if (!sb || sb.error) return { ok: false, message: sb?.error || '找不到这个工段' };
|
|
749
|
+
onlyFiles = sb.files;
|
|
750
|
+
}
|
|
751
|
+
const r = searchKnowledge(args?.query, args?.limit, onlyFiles);
|
|
669
752
|
if (!r.ok) return { ok: false, message: r.error?.message || '没搜成' };
|
|
670
753
|
if (!r.data.hits.length) return { ok: true, message: `积木里没搜到「${r.data.query}」(翻过 ${r.data.scanned} 个文件)` };
|
|
671
754
|
const body = r.data.hits.map((h) => `${h.file}:${h.line} ${h.text}`).join('\n');
|