@jiugeqqqqq/dsh-house 0.2.2 → 0.3.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/README.md CHANGED
@@ -37,6 +37,17 @@ dsh plugin --profile web add @jiugeqqqqq/dsh-house
37
37
  <DSH_HOME>\logs\... # 启动日志里会有 dsh-house: 房子清单 = ...
38
38
  ```
39
39
 
40
+ ## 给模型的两个工具(0.3.0 起)
41
+
42
+ 装完模型自己能维护清单,不用你手动改:
43
+
44
+ - `house_add`:做完一件大事往清单加一行(`text` + 可选 `group`,默认「已经有的」)
45
+ - `house_search`:在积木(`knowledge` 目录里的 .md)里按关键词翻原文
46
+
47
+ 系统提示里会带一句引导:「做完大事用 house_add 记一行;清单和最近对话里找不到的,用 house_search 去积木里翻,翻到再答,别猜。」
48
+
49
+ `knowledge` 目录不存在时会自动建一份带说明的 `README.md`,别人装完知道该往里写什么。
50
+
40
51
  ## 清单格式
41
52
 
42
53
  只有两种语法,够用就好:
package/cordis.patch.yml CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  - insert:
8
8
  - id: house
9
- name: dsh-house
9
+ name: "@jiugeqqqqq/dsh-house"
10
10
  config:
11
11
  # 房子清单:Markdown,按 "## 分组" 和 "- 条目" 解析。留空 = <DSH_HOME>/house.md
12
12
  assetsPath: ''
package/lib/client.js CHANGED
@@ -428,14 +428,18 @@ window.__ModuleLoader__.load({
428
428
  var inject = ['slots'];
429
429
 
430
430
  function apply(ctx) {
431
- ctx.slots.inject('sidebar.panellist', function () {
432
- return ctx.slots.register(
431
+ // 2026-10-27:DSH 0.2.1-rc.3 起,slots 要拿 ctx.get('slots'),直接读 ctx.slots 会是
432
+ // undefined(表现:host 正常、侧栏静默不出现、Console 也不报错)。
433
+ // 老版本没有 ctx.get,所以两种写法都兜一下。
434
+ var slots = (typeof ctx.get === 'function' && ctx.get('slots')) || ctx.slots;
435
+ slots.inject('sidebar.panellist', function () {
436
+ return slots.register(
433
437
  { name: 'sidebar.panellist', id: PANEL_ID, order: 21, label: function () { return '房子'; } },
434
438
  Icon,
435
439
  );
436
440
  });
437
- ctx.slots.inject('main', function () {
438
- return ctx.slots.register({ name: 'main', key: PANEL_ID }, HousePage);
441
+ slots.inject('main', function () {
442
+ return slots.register({ name: 'main', key: PANEL_ID }, HousePage);
439
443
  });
440
444
  }
441
445
 
package/lib/host/index.js CHANGED
@@ -144,6 +144,42 @@ export function apply(ctx, config = {}) {
144
144
  '',
145
145
  ].join('\n');
146
146
 
147
+ // 积木目录:房子只记结论,细节(怎么调通的、踩了什么坑、为什么这么改)写这里。
148
+ // 别人装完如果目录不存在,自动建一份带说明的 README —— 否则页面只显示一个空路径,
149
+ // 谁也不知道该往里写什么。
150
+ const KNOWLEDGE_TEMPLATE = [
151
+ '# 积木库',
152
+ '',
153
+ '> 房子清单(house.md)只记「有/没有、到哪一步、怎么用一句话」。',
154
+ '> 怎么调通的、踩了什么坑、为什么这么改 —— 写在这里,一个主题一个 .md。',
155
+ '> 新会话不会自动读这里;需要时用 house_search 按关键词翻,或者直接打开文件。',
156
+ '',
157
+ '## 怎么写',
158
+ '- 文件名用主题,比如 `手机-接入与操作.md`、`画图-Agnes.md`',
159
+ '- 只记「最后怎么定的 + 为什么」,不记纠结过程',
160
+ '- 原始输出留在日志里,不要整段粘进来',
161
+ '',
162
+ '## 索引',
163
+ '(把自己的文件一行一个写在这里,方便翻)',
164
+ '',
165
+ ].join('\n');
166
+
167
+ async function ensureKnowledge() {
168
+ try {
169
+ await mkdir(knowledgeDir, { recursive: true });
170
+ const readme = path.join(knowledgeDir, 'README.md');
171
+ if (!fs.existsSync(readme)) await writeFile(readme, KNOWLEDGE_TEMPLATE, 'utf8');
172
+ return { created: true };
173
+ } catch (error) {
174
+ return { created: false, error: error instanceof Error ? error.message : String(error) };
175
+ }
176
+ }
177
+ // 启动就建,不等页面打开(fire and forget,失败也不影响清单)。
178
+ ensureKnowledge().then((r) => {
179
+ if (r.created) ctx.logger?.info('dsh-house: 积木目录 = %s', knowledgeDir);
180
+ else ctx.logger?.warn('dsh-house: 建积木目录失败:%s', r.error);
181
+ });
182
+
147
183
  async function ensureAssets() {
148
184
  try {
149
185
  await readFile(assetsPath, 'utf8');
@@ -375,20 +411,101 @@ export function apply(ctx, config = {}) {
375
411
  }
376
412
  }
377
413
 
378
- // ------------------------------------------------------- 注入到系统提示
414
+ /** 往清单里加一行(工具 house_add 用,走和页面同一条写回链路)。 */
415
+ async function addHouseItem(group, text) {
416
+ const item = String(text || '').replace(/\s+/g, ' ').trim();
417
+ if (!item) return { ok: false, error: { code: 'invalid', message: '要写点什么。' } };
418
+ // 别人可能还没打开过页面,清单文件不存在 —— 先建模板再写,别让第一次 house_add 就失败。
419
+ if (!fs.existsSync(assetsPath)) await ensureAssets();
420
+ const g = String(group || '已经有的').trim() || '已经有的';
421
+ const r = await mutate((lines) => {
422
+ let gi = lines.findIndex((l) => l.trim() === '## ' + g);
423
+ if (gi < 0) {
424
+ lines.push('', '## ' + g);
425
+ gi = lines.length - 1;
426
+ }
427
+ let end = lines.length;
428
+ for (let i = gi + 1; i < lines.length; i++) {
429
+ if (/^##\s+/.test(lines[i])) {
430
+ end = i;
431
+ break;
432
+ }
433
+ }
434
+ let insertAt = end;
435
+ for (let i = end - 1; i > gi; i--) {
436
+ if (lines[i].trim()) {
437
+ insertAt = i + 1;
438
+ break;
439
+ }
440
+ }
441
+ lines.splice(insertAt, 0, '- ' + item);
442
+ return true;
443
+ });
444
+ return r.ok ? { ok: true, data: { group: g, text: item } } : r;
445
+ }
446
+
447
+ /** 在积木目录里按关键词翻(工具 house_search 用)。 */
448
+ function walkKnowledge(dir, out = [], depth = 0) {
449
+ if (depth > 4) return out;
450
+ let ents;
451
+ try {
452
+ ents = fs.readdirSync(dir, { withFileTypes: true });
453
+ } catch {
454
+ return out;
455
+ }
456
+ for (const e of ents) {
457
+ const p = path.join(dir, e.name);
458
+ if (e.isDirectory()) walkKnowledge(p, out, depth + 1);
459
+ else if (/\.md$/i.test(e.name) && e.name !== 'README.md') out.push(p);
460
+ }
461
+ return out;
462
+ }
463
+
464
+ function searchKnowledge(query, limit) {
465
+ const q = String(query ?? '').trim().toLowerCase();
466
+ if (!q) return { ok: false, error: { code: 'invalid', message: '要给个关键词。' } };
467
+ const max = Math.max(1, Math.min(30, Number(limit) || 8));
468
+ const files = walkKnowledge(knowledgeDir);
469
+ const hits = [];
470
+ for (const f of files) {
471
+ if (hits.length >= max) break;
472
+ let text;
473
+ try {
474
+ text = fs.readFileSync(f, 'utf8');
475
+ } catch {
476
+ continue;
477
+ }
478
+ const lines = text.split('\n');
479
+ for (let i = 0; i < lines.length; i++) {
480
+ if (hits.length >= max) break;
481
+ if (lines[i].toLowerCase().includes(q)) {
482
+ hits.push({
483
+ file: path.relative(knowledgeDir, f),
484
+ line: i + 1,
485
+ text: lines[i].trim().slice(0, 240),
486
+ });
487
+ }
488
+ }
489
+ }
490
+ return { ok: true, data: { query: q, hits, scanned: files.length } };
491
+ }
492
+
493
+
379
494
  // 2026-10-27:以前「房子清单能不能进模型上下文」要靠**另一个插件**
380
495
  // (dsh-session-rotate 的 assetsPath) 配一下才行 —— 等于装了这个插件的人也
381
496
  // 只得到页面、模型根本看不见清单。粑粑要求「装上就和我们一样」,所以这里自己注入。
382
497
  // 文件小(几 KB)且每轮重读,保证清单改了立刻生效;读不到就注入空串,不报错。
498
+ // 每轮都带一段用法提示:只给清单、不告诉它「找不到去哪翻」,模型就会自己猜。
499
+ const HOUSE_GUIDE =
500
+ '【房子怎么用】做完一件大事,用 house_add 往清单加一行(分组写「已经有的」或「正在长出来的」);' +
501
+ '清单和最近对话里找不到的东西,用 house_search 去积木(knowledge 目录)里按关键词翻,翻到再答,别猜。';
502
+
383
503
  function assetsAsPrompt() {
504
+ let text = '';
384
505
  try {
385
- if (!fs.existsSync(assetsPath)) return '';
386
- const text = fs.readFileSync(assetsPath, 'utf8').trim();
387
- if (!text) return '';
388
- return `【资产清单】\n${text}`;
389
- } catch {
390
- return '';
391
- }
506
+ if (fs.existsSync(assetsPath)) text = fs.readFileSync(assetsPath, 'utf8').trim();
507
+ } catch {}
508
+ return text ? `【资产清单】\n${text}\n\n${HOUSE_GUIDE}` : HOUSE_GUIDE;
392
509
  }
393
510
 
394
511
  ctx.inject(['systemPrompt'], (scoped) => {
@@ -409,6 +526,86 @@ export function apply(ctx, config = {}) {
409
526
  );
410
527
  });
411
528
 
529
+ // ----------------------------------------------------------- 两个工具
530
+ // 2026-10-28 加的:以前房子只能靠页面手改,模型自己没法维护;
531
+ // 积木目录也没人搜得到。给模型两个口子,它才能「做完事记一行、找不到就翻」。
532
+ ctx.inject(['tools'], (scoped) => {
533
+ const tools = scoped.get('tools');
534
+ if (tools === undefined || typeof tools.register !== 'function') {
535
+ ctx.logger?.warn('dsh-house: 宿主没有 tools,house_add / house_search 不可用(页面不受影响)。');
536
+ return;
537
+ }
538
+ const render = (args, value) => [{ type: 'text', text: String(value?.message ?? '') }];
539
+ const output = {
540
+ schema: {
541
+ type: 'object',
542
+ properties: {
543
+ ok: { type: 'boolean', description: '是否成功' },
544
+ message: { type: 'string', description: '给模型看的一句话结果' },
545
+ },
546
+ required: ['ok', 'message'],
547
+ additionalProperties: false,
548
+ },
549
+ render,
550
+ };
551
+
552
+ scoped.effect(
553
+ () =>
554
+ tools.register({
555
+ name: 'house_add',
556
+ description:
557
+ '往「房子」清单里加一行。做完一件大事(装好一个工具、接好一个设备、跑通一个流程)就调一次,' +
558
+ '只写「有/没有、到哪一步、怎么用一句话」,细节留在积木里。分组不写默认「已经有的」,' +
559
+ '没做完的写「正在长出来的」。',
560
+ parameters: {
561
+ type: 'object',
562
+ properties: {
563
+ text: { type: 'string', description: '一行话,比如「手机截图 ✅ phone.cmd shot」' },
564
+ group: { type: 'string', description: '分组名:已经有的 / 正在长出来的 / 或者你自己开的分组' },
565
+ },
566
+ required: ['text'],
567
+ additionalProperties: false,
568
+ },
569
+ output,
570
+ async execute(args) {
571
+ const r = await addHouseItem(args?.group, args?.text);
572
+ return r.ok
573
+ ? { ok: true, message: `已加到「${r.data.group}」:${r.data.text}` }
574
+ : { ok: false, message: r.error?.message || '没写成' };
575
+ },
576
+ }),
577
+ 'dsh-house: house_add',
578
+ );
579
+
580
+ scoped.effect(
581
+ () =>
582
+ tools.register({
583
+ name: 'house_search',
584
+ description:
585
+ '在「积木」(knowledge 目录里的 .md)里按关键词翻原文。房子清单和最近对话里找不到的细节,' +
586
+ '先在这里搜;搜到再回答,搜不到就说没找到,别猜。',
587
+ parameters: {
588
+ type: 'object',
589
+ properties: {
590
+ query: { type: 'string', description: '关键词,比如 抖音、画图、报错信息里的词' },
591
+ limit: { type: 'number', description: '最多返回几处,默认 8' },
592
+ },
593
+ required: ['query'],
594
+ additionalProperties: false,
595
+ },
596
+ output,
597
+ async execute(args) {
598
+ const r = searchKnowledge(args?.query, args?.limit);
599
+ if (!r.ok) return { ok: false, message: r.error?.message || '没搜成' };
600
+ if (!r.data.hits.length) return { ok: true, message: `积木里没搜到「${r.data.query}」(翻过 ${r.data.scanned} 个文件)` };
601
+ const body = r.data.hits.map((h) => `${h.file}:${h.line} ${h.text}`).join('\n');
602
+ return { ok: true, message: `在积木里找到 ${r.data.hits.length} 处:\n${body}` };
603
+ },
604
+ }),
605
+ 'dsh-house: house_search',
606
+ );
607
+ });
608
+
412
609
  // ---------------------------------------------------------------- web API
413
610
  ctx.inject(['webServer'], (scoped) => {
414
611
  const webServer = scoped.get('webServer');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiugeqqqqq/dsh-house",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "房子 — 常驻的资产/能力清单,配一个侧边栏页面。清单会自动进系统提示(装了就生效);房子只记有没有、到哪一步,细节放 knowledge 和日志里。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,7 +22,7 @@
22
22
  "platform": "web",
23
23
  "immediately": true,
24
24
  "inject": [
25
- "@deepseek-ai/dsh-client-ui-slots"
25
+ "slots"
26
26
  ]
27
27
  },
28
28
  "manifestVersion": 1