@jiugeqqqqq/dsh-house2 0.1.2 → 0.2.0

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 CHANGED
@@ -3,6 +3,17 @@
3
3
  > 每次发版都必须在这里写一条:改了什么、为什么改、别人要不要跟着动。
4
4
  > 版本号跟 `package.json` 走,`npm i` 的人看这里就知道升了什么。
5
5
 
6
+ ## 0.2.0 — 2026-10-30
7
+
8
+ - 新功能:**工段(任务段)**。不再按轮数切,按「这件事做完了没有」切——`stage_start` 开一座、`stage_done` 收掉(写结果 + 积木文件名)。
9
+ - 工段清单每轮注入:正在做的全给、最近完成的给 5 条、更早的只留名字(细节去积木里翻),注入量不随任务数线性涨。
10
+ - 数据文件:`<DSH_HOME>/stages.md`(装了就自动建,别人不用手动初始化)。
11
+ - 下一步(未做):任务完成时主动压缩/换会话、房子→积木的定向检索。
12
+
13
+ ## 0.1.3 — 2026-10-30
14
+
15
+ - 修 client 的注册 id:必须是 scoped 包名 `@jiugeqqqqq/dsh-house2`,写裸名会导致 `loaded without registering` + `duplicate factory registration`(跟旧插件 0.3.6 以后崩的是同一个根因)。
16
+
6
17
  ## 0.1.0 — 2026-10-30
7
18
 
8
19
  - 新插件,从 dsh-house 分出来的一代:旧插件在别人机器上一直报 `duplicate factory registration`,换了几种包结构都修不掉,先起一个新的。
package/lib/client.js CHANGED
@@ -1,15 +1,9 @@
1
- // 2026-10-30:防重复注册。同一份 client bundle 如果被两条 loader 行各执行一次,
2
- // __ModuleLoader__.load 会报 duplicate factory registration 把整个 web boot 打挂
3
- // (别人机器上就是这个问题)。这里加个一次性标记:第二次执行直接跳过,不重复注册。
4
- (function () {
5
- if (window.__dshHouseClientLoaded) return;
6
- window.__dshHouseClientLoaded = true;
7
1
  window.__ModuleLoader__.load({
8
- id: 'dsh-house2',
2
+ id: '@jiugeqqqqq/dsh-house2',
9
3
  factory: (require) => {
10
4
  'use strict';
11
5
  var React = require('react');
12
- var PACKAGE_ID = 'dsh-house2';
6
+ var PACKAGE_ID = '@jiugeqqqqq/dsh-house2';
13
7
  var PANEL_ID = 'house2';
14
8
  var ROUTE_PREFIX = '/dsh-house2/api';
15
9
 
@@ -460,5 +454,4 @@ window.__ModuleLoader__.load({
460
454
  return { apply: apply, inject: inject, name: 'house' };
461
455
  },
462
456
  });
463
- })();
464
457
 
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({
@@ -562,26 +576,25 @@ export function apply(ctx, config = {}) {
562
576
  try {
563
577
  if (fs.existsSync(assetsPath)) text = fs.readFileSync(assetsPath, 'utf8').trim();
564
578
  } catch {}
565
- return text ? `【资产清单】\n${text}\n\n${HOUSE_GUIDE}` : HOUSE_GUIDE;
579
+ const stageText = stages.renderPrompt();
580
+ const base = text ? `【资产清单】\n${text}\n\n${HOUSE_GUIDE}` : HOUSE_GUIDE;
581
+ return stageText ? `${base}\n\n${stageText}` : base;
566
582
  }
567
583
 
568
- ctx.inject(['systemPrompt'], (scoped) => {
569
- const systemPrompt = scoped.get('systemPrompt');
570
- if (systemPrompt === undefined || typeof systemPrompt.section !== 'function') {
571
- ctx.logger?.warn('dsh-house2: 宿主没有 systemPrompt,房子清单进不去上下文(页面不受影响)。');
572
- return;
573
- }
574
- scoped.effect(
575
- () =>
576
- systemPrompt.section({
577
- name: 'house2:assets',
578
- // 比 dsh-session-rotate(9300)稍早一点,两者同时开也不会打架。
579
- order: 9200,
580
- text: assetsAsPrompt,
581
- }),
582
- 'dsh-house2: assets prompt',
583
- );
584
- });
584
+ // 2026-10-30:改成直接 ctx.systemPrompt.section(跟 dsh-affection 一样)。
585
+ // 原来用 ctx.inject(['systemPrompt']) 包一层,实测在新版 DSH 上没注入进去
586
+ // (所有会话的 prompt 里都搜不到「资产清单」)。
587
+ try {
588
+ ctx.systemPrompt.section({
589
+ name: 'house2:assets',
590
+ // 比 dsh-session-rotate(9300)稍早一点,两者同时开也不会打架。
591
+ order: 9200,
592
+ text: assetsAsPrompt,
593
+ });
594
+ ctx.logger?.info('dsh-house2: 资产清单注入已挂载');
595
+ } catch (e) {
596
+ ctx.logger?.warn('dsh-house2: 注入资产清单失败:' + (e && e.message ? e.message : e));
597
+ }
585
598
 
586
599
  // ----------------------------------------------------------- 两个工具
587
600
  // 2026-10-28 加的:以前房子只能靠页面手改,模型自己没法维护;
@@ -739,6 +752,63 @@ export function apply(ctx, config = {}) {
739
752
  }),
740
753
  'dsh-house2: house_done',
741
754
  );
755
+
756
+ // 2026-10-30:工段(任务段)。一个任务一座工段,做完 stage_done 记结果 + 积木。
757
+ // 不按轮数切;活跃工段每轮注入,完成的只留最近几条,更早的只留名字。
758
+ scoped.effect(
759
+ () =>
760
+ tools.register({
761
+ name: 'stage_start',
762
+ description:
763
+ '开一座工段(一个任务)。说清楚这个任务要做什么,做完再用 stage_done 收掉。' +
764
+ '不是按轮数切,是按「这件事做完了没有」切。',
765
+ parameters: {
766
+ type: 'object',
767
+ properties: {
768
+ name: { type: 'string', description: '工段名,一句话说清这个任务' },
769
+ goal: { type: 'string', description: '目标/验收标准,一句话' },
770
+ },
771
+ required: ['name'],
772
+ additionalProperties: false,
773
+ },
774
+ output,
775
+ async execute(args) {
776
+ const r = stages.start(args?.name, args?.goal);
777
+ return r.ok
778
+ ? { ok: true, message: `工段已开:${r.data.name}` }
779
+ : { ok: false, message: r.error || '没开成' };
780
+ },
781
+ }),
782
+ 'dsh-house2: stage_start',
783
+ );
784
+
785
+ scoped.effect(
786
+ () =>
787
+ tools.register({
788
+ name: 'stage_done',
789
+ description:
790
+ '把工段标记完成:写结果 + 积木文件名,它就会从「正在做的」移到「已经完成的」。' +
791
+ '做完一个任务就调一次,别挂着;拿不准算不算做完就别调。',
792
+ parameters: {
793
+ type: 'object',
794
+ properties: {
795
+ name: { type: 'string', description: '工段名(和 stage_start 一致)' },
796
+ result: { type: 'string', description: '最后成了什么,一句话' },
797
+ brick: { type: 'string', description: '积木文件名(不带 .md),比如 任务-调通一直发' },
798
+ },
799
+ required: ['name', 'result'],
800
+ additionalProperties: false,
801
+ },
802
+ output,
803
+ async execute(args) {
804
+ const r = stages.done(args?.name, args?.result, args?.brick);
805
+ return r.ok
806
+ ? { ok: true, message: `工段完成:${r.data.name}` }
807
+ : { ok: false, message: r.error || '没写成' };
808
+ },
809
+ }),
810
+ 'dsh-house2: stage_done',
811
+ );
742
812
  });
743
813
 
744
814
  // ---------------------------------------------------------------- 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiugeqqqqq/dsh-house2",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "房子 2 —— 资产/能力清单 + 工段,配侧边栏页面。0.1.0 先把安装跑通(保守兼容写法),功能后续迭代。",
5
5
  "type": "module",
6
6
  "license": "MIT",