@jiugeqqqqq/dsh-house2 0.1.3 → 0.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 +15 -0
- package/lib/host/index.js +101 -4
- package/lib/host/stages.js +152 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
> 每次发版都必须在这里写一条:改了什么、为什么改、别人要不要跟着动。
|
|
4
4
|
> 版本号跟 `package.json` 走,`npm i` 的人看这里就知道升了什么。
|
|
5
5
|
|
|
6
|
+
## 0.2.1 — 2026-10-30
|
|
7
|
+
|
|
8
|
+
- `house_search` 加 `stage` 参数:先按工段名找到它挂的积木文件(`stage_done` 时写的「积木:xxx」),只在那一个文件里搜,比全目录翻更快更准。不传就还是全目录搜。
|
|
9
|
+
|
|
10
|
+
## 0.2.0 — 2026-10-30
|
|
11
|
+
|
|
12
|
+
- 新功能:**工段(任务段)**。不再按轮数切,按「这件事做完了没有」切——`stage_start` 开一座、`stage_done` 收掉(写结果 + 积木文件名)。
|
|
13
|
+
- 工段清单每轮注入:正在做的全给、最近完成的给 5 条、更早的只留名字(细节去积木里翻),注入量不随任务数线性涨。
|
|
14
|
+
- 数据文件:`<DSH_HOME>/stages.md`(装了就自动建,别人不用手动初始化)。
|
|
15
|
+
- 下一步(未做):任务完成时主动压缩/换会话、房子→积木的定向检索。
|
|
16
|
+
|
|
17
|
+
## 0.1.3 — 2026-10-30
|
|
18
|
+
|
|
19
|
+
- 修 client 的注册 id:必须是 scoped 包名 `@jiugeqqqqq/dsh-house2`,写裸名会导致 `loaded without registering` + `duplicate factory registration`(跟旧插件 0.3.6 以后崩的是同一个根因)。
|
|
20
|
+
|
|
6
21
|
## 0.1.0 — 2026-10-30
|
|
7
22
|
|
|
8
23
|
- 新插件,从 dsh-house 分出来的一代:旧插件在别人机器上一直报 `duplicate factory registration`,换了几种包结构都修不掉,先起一个新的。
|
package/lib/host/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import fs from 'node:fs';
|
|
|
18
18
|
import zlib from 'node:zlib';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { createUpdateChecker, compareVersions } from '../update-check.js';
|
|
21
|
+
import { createStages } from './stages.js';
|
|
21
22
|
|
|
22
23
|
export const name = 'house';
|
|
23
24
|
export const inject = [];
|
|
@@ -111,6 +112,19 @@ export function apply(ctx, config = {}) {
|
|
|
111
112
|
? config.sessionsDir
|
|
112
113
|
: path.join(dshHome, 'sessions');
|
|
113
114
|
|
|
115
|
+
// 工段(任务段):每做完一个任务记一座,不按轮数切。
|
|
116
|
+
// 活跃的工段每轮注入;完成的只留最近几条,更早的只有名字,细节去积木里翻。
|
|
117
|
+
const stagesPath =
|
|
118
|
+
typeof config.stagesPath === 'string' && config.stagesPath.length > 0
|
|
119
|
+
? config.stagesPath
|
|
120
|
+
: path.join(dshHome, 'stages.md');
|
|
121
|
+
const stages = createStages({
|
|
122
|
+
filePath: stagesPath,
|
|
123
|
+
log: (m) => ctx.logger?.info('dsh-house2: ' + m),
|
|
124
|
+
});
|
|
125
|
+
stages.ensure();
|
|
126
|
+
ctx.logger?.info('dsh-house2: 工段文件 = %s', stagesPath);
|
|
127
|
+
|
|
114
128
|
// 版本自检:只查 npm 有没有新版,不自动安装。
|
|
115
129
|
// 注意:本地开发目录的 package.json name 是 dsh-house2,而 npm 上是 scoped 包,所以写死 npmName。
|
|
116
130
|
const updateChecker = createUpdateChecker({
|
|
@@ -514,11 +528,12 @@ export function apply(ctx, config = {}) {
|
|
|
514
528
|
return out;
|
|
515
529
|
}
|
|
516
530
|
|
|
517
|
-
function searchKnowledge(query, limit) {
|
|
531
|
+
function searchKnowledge(query, limit, onlyFiles) {
|
|
518
532
|
const q = String(query ?? '').trim().toLowerCase();
|
|
519
533
|
if (!q) return { ok: false, error: { code: 'invalid', message: '要给个关键词。' } };
|
|
520
534
|
const max = Math.max(1, Math.min(30, Number(limit) || 8));
|
|
521
|
-
|
|
535
|
+
// 2026-10-30:给了 onlyFiles 就只在这些积木里搜(工段定向检索)。
|
|
536
|
+
const files = Array.isArray(onlyFiles) && onlyFiles.length ? onlyFiles : walkKnowledge(knowledgeDir);
|
|
522
537
|
const hits = [];
|
|
523
538
|
for (const f of files) {
|
|
524
539
|
if (hits.length >= max) break;
|
|
@@ -543,6 +558,22 @@ export function apply(ctx, config = {}) {
|
|
|
543
558
|
return { ok: true, data: { query: q, hits, scanned: files.length } };
|
|
544
559
|
}
|
|
545
560
|
|
|
561
|
+
/** 按工段名找它挂的积木文件(stage_done 时写的「积木:xxx」)。 */
|
|
562
|
+
function stageBrickFiles(stageName) {
|
|
563
|
+
const name = String(stageName ?? '').trim();
|
|
564
|
+
if (!name) return null;
|
|
565
|
+
const { doing, done } = stages.parse();
|
|
566
|
+
const all = [...doing, ...done];
|
|
567
|
+
const hit = all.find((x) => x.name === name) || all.find((x) => x.name.includes(name) || name.includes(x.name));
|
|
568
|
+
if (!hit) return { error: `没有叫「${name}」的工段` };
|
|
569
|
+
const m = hit.line.match(/积木[::]\s*([^\s|]+)/);
|
|
570
|
+
if (!m) return { error: `工段「${hit.name}」没有记积木文件名` };
|
|
571
|
+
const base = m[1].replace(/\.md$/i, '');
|
|
572
|
+
const file = path.join(knowledgeDir, `${base}.md`);
|
|
573
|
+
if (!fs.existsSync(file)) return { error: `积木文件不存在:${base}.md` };
|
|
574
|
+
return { files: [file], brick: base };
|
|
575
|
+
}
|
|
576
|
+
|
|
546
577
|
|
|
547
578
|
// 2026-10-27:以前「房子清单能不能进模型上下文」要靠**另一个插件**
|
|
548
579
|
// (dsh-session-rotate 的 assetsPath) 配一下才行 —— 等于装了这个插件的人也
|
|
@@ -562,7 +593,9 @@ export function apply(ctx, config = {}) {
|
|
|
562
593
|
try {
|
|
563
594
|
if (fs.existsSync(assetsPath)) text = fs.readFileSync(assetsPath, 'utf8').trim();
|
|
564
595
|
} catch {}
|
|
565
|
-
|
|
596
|
+
const stageText = stages.renderPrompt();
|
|
597
|
+
const base = text ? `【资产清单】\n${text}\n\n${HOUSE_GUIDE}` : HOUSE_GUIDE;
|
|
598
|
+
return stageText ? `${base}\n\n${stageText}` : base;
|
|
566
599
|
}
|
|
567
600
|
|
|
568
601
|
// 2026-10-30:改成直接 ctx.systemPrompt.section(跟 dsh-affection 一样)。
|
|
@@ -643,13 +676,20 @@ export function apply(ctx, config = {}) {
|
|
|
643
676
|
properties: {
|
|
644
677
|
query: { type: 'string', description: '关键词,比如 抖音、画图、报错信息里的词' },
|
|
645
678
|
limit: { type: 'number', description: '最多返回几处,默认 8' },
|
|
679
|
+
stage: { type: 'string', description: '可选:先定位到这个工段,只搜它挂的积木文件(更快更准)' },
|
|
646
680
|
},
|
|
647
681
|
required: ['query'],
|
|
648
682
|
additionalProperties: false,
|
|
649
683
|
},
|
|
650
684
|
output,
|
|
651
685
|
async execute(args) {
|
|
652
|
-
|
|
686
|
+
let onlyFiles = null;
|
|
687
|
+
if (args?.stage) {
|
|
688
|
+
const sb = stageBrickFiles(args.stage);
|
|
689
|
+
if (!sb || sb.error) return { ok: false, message: sb?.error || '找不到这个工段' };
|
|
690
|
+
onlyFiles = sb.files;
|
|
691
|
+
}
|
|
692
|
+
const r = searchKnowledge(args?.query, args?.limit, onlyFiles);
|
|
653
693
|
if (!r.ok) return { ok: false, message: r.error?.message || '没搜成' };
|
|
654
694
|
if (!r.data.hits.length) return { ok: true, message: `积木里没搜到「${r.data.query}」(翻过 ${r.data.scanned} 个文件)` };
|
|
655
695
|
const body = r.data.hits.map((h) => `${h.file}:${h.line} ${h.text}`).join('\n');
|
|
@@ -736,6 +776,63 @@ export function apply(ctx, config = {}) {
|
|
|
736
776
|
}),
|
|
737
777
|
'dsh-house2: house_done',
|
|
738
778
|
);
|
|
779
|
+
|
|
780
|
+
// 2026-10-30:工段(任务段)。一个任务一座工段,做完 stage_done 记结果 + 积木。
|
|
781
|
+
// 不按轮数切;活跃工段每轮注入,完成的只留最近几条,更早的只留名字。
|
|
782
|
+
scoped.effect(
|
|
783
|
+
() =>
|
|
784
|
+
tools.register({
|
|
785
|
+
name: 'stage_start',
|
|
786
|
+
description:
|
|
787
|
+
'开一座工段(一个任务)。说清楚这个任务要做什么,做完再用 stage_done 收掉。' +
|
|
788
|
+
'不是按轮数切,是按「这件事做完了没有」切。',
|
|
789
|
+
parameters: {
|
|
790
|
+
type: 'object',
|
|
791
|
+
properties: {
|
|
792
|
+
name: { type: 'string', description: '工段名,一句话说清这个任务' },
|
|
793
|
+
goal: { type: 'string', description: '目标/验收标准,一句话' },
|
|
794
|
+
},
|
|
795
|
+
required: ['name'],
|
|
796
|
+
additionalProperties: false,
|
|
797
|
+
},
|
|
798
|
+
output,
|
|
799
|
+
async execute(args) {
|
|
800
|
+
const r = stages.start(args?.name, args?.goal);
|
|
801
|
+
return r.ok
|
|
802
|
+
? { ok: true, message: `工段已开:${r.data.name}` }
|
|
803
|
+
: { ok: false, message: r.error || '没开成' };
|
|
804
|
+
},
|
|
805
|
+
}),
|
|
806
|
+
'dsh-house2: stage_start',
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
scoped.effect(
|
|
810
|
+
() =>
|
|
811
|
+
tools.register({
|
|
812
|
+
name: 'stage_done',
|
|
813
|
+
description:
|
|
814
|
+
'把工段标记完成:写结果 + 积木文件名,它就会从「正在做的」移到「已经完成的」。' +
|
|
815
|
+
'做完一个任务就调一次,别挂着;拿不准算不算做完就别调。',
|
|
816
|
+
parameters: {
|
|
817
|
+
type: 'object',
|
|
818
|
+
properties: {
|
|
819
|
+
name: { type: 'string', description: '工段名(和 stage_start 一致)' },
|
|
820
|
+
result: { type: 'string', description: '最后成了什么,一句话' },
|
|
821
|
+
brick: { type: 'string', description: '积木文件名(不带 .md),比如 任务-调通一直发' },
|
|
822
|
+
},
|
|
823
|
+
required: ['name', 'result'],
|
|
824
|
+
additionalProperties: false,
|
|
825
|
+
},
|
|
826
|
+
output,
|
|
827
|
+
async execute(args) {
|
|
828
|
+
const r = stages.done(args?.name, args?.result, args?.brick);
|
|
829
|
+
return r.ok
|
|
830
|
+
? { ok: true, message: `工段完成:${r.data.name}` }
|
|
831
|
+
: { ok: false, message: r.error || '没写成' };
|
|
832
|
+
},
|
|
833
|
+
}),
|
|
834
|
+
'dsh-house2: stage_done',
|
|
835
|
+
);
|
|
739
836
|
});
|
|
740
837
|
|
|
741
838
|
// ---------------------------------------------------------------- web API
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// dsh-house2 工段(任务段)—— 2026-10-30 新功能
|
|
2
|
+
//
|
|
3
|
+
// 房子 = 能力清单(一直留着);工段 = 一次任务(做完就归档)。
|
|
4
|
+
// 不按轮数切,按「这件事做完了没有」切:做完了 stage_done,写结果和积木指针。
|
|
5
|
+
// 工段清单每轮注入(活跃的 + 最近完成的),老的不注入、只留名字,细节去积木里翻。
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
const TEMPLATE = `# 工段(任务段)
|
|
11
|
+
|
|
12
|
+
> 每做一个任务就是一座工段。做完写一行到「已经完成的」,细节进积木(knowledge 目录)。
|
|
13
|
+
> 不要按轮数切,按「这件事做完了没有」切。
|
|
14
|
+
|
|
15
|
+
## 正在做的
|
|
16
|
+
|
|
17
|
+
## 已经完成的
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
export function createStages({ filePath, log = () => {} }) {
|
|
21
|
+
function ensure() {
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(filePath)) return { created: false };
|
|
24
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
25
|
+
fs.writeFileSync(filePath, TEMPLATE, 'utf8');
|
|
26
|
+
return { created: true };
|
|
27
|
+
} catch (e) {
|
|
28
|
+
log('建工段文件失败:' + (e && e.message ? e.message : e));
|
|
29
|
+
return { created: false, error: e };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readText() {
|
|
34
|
+
try {
|
|
35
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
36
|
+
} catch {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 按分组解析成 {doing:[], done:[]},每条保留原文和名字。 */
|
|
42
|
+
function parse() {
|
|
43
|
+
const text = readText();
|
|
44
|
+
const out = { doing: [], done: [] };
|
|
45
|
+
let group = '';
|
|
46
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
47
|
+
const t = raw.trim();
|
|
48
|
+
if (t.startsWith('## ')) {
|
|
49
|
+
group = t.slice(3).trim();
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!t.startsWith('- ')) continue;
|
|
53
|
+
const line = t.slice(2).trim();
|
|
54
|
+
if (group === '正在做的') out.doing.push({ line, name: nameOf(line) });
|
|
55
|
+
else if (group === '已经完成的') out.done.push({ line, name: nameOf(line) });
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function nameOf(line) {
|
|
61
|
+
// 条目格式:🚧 **名字** | 目标:... 或 ✅ **名字** | 结果:...
|
|
62
|
+
const m = line.match(/\*\*([^*]+)\*\*/);
|
|
63
|
+
return m ? m[1].trim() : line.split('|')[0].replace(/^[^\w\u4e00-\u9fa5]+/, '').trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function today() {
|
|
67
|
+
const d = new Date();
|
|
68
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
69
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function start(name, goal) {
|
|
73
|
+
ensure();
|
|
74
|
+
const n = String(name || '').trim();
|
|
75
|
+
if (!n) return { ok: false, error: '工段名不能为空' };
|
|
76
|
+
const { doing } = parse();
|
|
77
|
+
if (doing.some((x) => x.name === n)) return { ok: false, error: `工段「${n}」已经开始过了` };
|
|
78
|
+
const line = `- 🚧 **${n}** | 目标:${String(goal || '(没写)').trim()} | 开始:${today()}`;
|
|
79
|
+
const text = readText();
|
|
80
|
+
const marker = '## 正在做的';
|
|
81
|
+
const idx = text.indexOf(marker);
|
|
82
|
+
if (idx < 0) {
|
|
83
|
+
fs.appendFileSync(filePath, `\n${line}\n`, 'utf8');
|
|
84
|
+
} else {
|
|
85
|
+
const insertAt = text.indexOf('\n', idx) + 1;
|
|
86
|
+
fs.writeFileSync(filePath, text.slice(0, insertAt) + line + '\n' + text.slice(insertAt), 'utf8');
|
|
87
|
+
}
|
|
88
|
+
log(`开工段:${n}`);
|
|
89
|
+
return { ok: true, data: { name: n, line } };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function done(name, result, brick) {
|
|
93
|
+
ensure();
|
|
94
|
+
const n = String(name || '').trim();
|
|
95
|
+
if (!n) return { ok: false, error: '工段名不能为空' };
|
|
96
|
+
const text = readText();
|
|
97
|
+
const lines = text.split(/\r?\n/);
|
|
98
|
+
const out = [];
|
|
99
|
+
let group = '';
|
|
100
|
+
let removed = false;
|
|
101
|
+
for (const raw of lines) {
|
|
102
|
+
const t = raw.trim();
|
|
103
|
+
if (t.startsWith('## ')) {
|
|
104
|
+
group = t.slice(3).trim();
|
|
105
|
+
out.push(raw);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (group === '正在做的' && t.startsWith('- ') && nameOf(t.slice(2).trim()) === n) {
|
|
109
|
+
removed = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
out.push(raw);
|
|
113
|
+
}
|
|
114
|
+
const line = `- ✅ **${n}** | 结果:${String(result || '(没写)').trim()} | 完成:${today()}${brick ? ` | 积木:${String(brick).trim()}` : ''}`;
|
|
115
|
+
let next = out.join('\n');
|
|
116
|
+
const marker = '## 已经完成的';
|
|
117
|
+
const idx = next.indexOf(marker);
|
|
118
|
+
if (idx < 0) {
|
|
119
|
+
next += `\n${marker}\n\n${line}\n`;
|
|
120
|
+
} else {
|
|
121
|
+
const end = next.indexOf('\n## ', idx + marker.length);
|
|
122
|
+
const insertAt = end < 0 ? next.length : end;
|
|
123
|
+
const before = next.slice(0, insertAt).replace(/\s*$/, '\n');
|
|
124
|
+
next = before + line + '\n' + next.slice(insertAt);
|
|
125
|
+
}
|
|
126
|
+
fs.writeFileSync(filePath, next.replace(/\n{3,}/g, '\n\n'), 'utf8');
|
|
127
|
+
log(`完工段:${n}${removed ? '' : '(原来不在「正在做的」里)'}`);
|
|
128
|
+
return { ok: true, data: { name: n, removed, line } };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 注入用:活跃工段全给,最近完成的给 5 条,更老的只给名字。 */
|
|
132
|
+
function renderPrompt(maxDone = 5) {
|
|
133
|
+
const { doing, done } = parse();
|
|
134
|
+
if (!doing.length && !done.length) return '';
|
|
135
|
+
const lines = ['【工段】'];
|
|
136
|
+
if (doing.length) {
|
|
137
|
+
lines.push('正在做的:');
|
|
138
|
+
for (const x of doing) lines.push(` ${x.line}`);
|
|
139
|
+
}
|
|
140
|
+
if (done.length) {
|
|
141
|
+
const recent = done.slice(-maxDone);
|
|
142
|
+
const older = done.length - recent.length;
|
|
143
|
+
lines.push('最近完成的:');
|
|
144
|
+
for (const x of recent) lines.push(` ${x.line}`);
|
|
145
|
+
if (older > 0) lines.push(` (更早的 ${older} 座工段只留了名字,细节翻积木)`);
|
|
146
|
+
}
|
|
147
|
+
lines.push('做完一个任务就 stage_done 记一笔(结果 + 积木文件名),别挂着。');
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { ensure, parse, start, done, renderPrompt, path: filePath };
|
|
152
|
+
}
|