@jiugeqqqqq/dsh-house 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/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # dsh-house
2
+
3
+ > 一份**常驻的资产清单** + 一个侧边栏页面。
4
+ > 清单每轮自动进系统提示(装了就生效),页面里能搜、能增、能改、能删;
5
+ > 被上下文压缩掉的旧原文还能按关键词翻回来。
6
+
7
+ "房子"是一个比喻:每个大能力/设备/工具算一座房子,只记**有没有、到哪一步、怎么用一句话**。
8
+ 细节不写在这里 —— 需要时用页面底部的「搜历史」去会话存档里翻原话。
9
+
10
+ ## 装
11
+
12
+ ```powershell
13
+ dsh plugin --profile desktop add @jiugeqqqqq/dsh-house
14
+ # 或 web profile
15
+ dsh plugin --profile web add @jiugeqqqqq/dsh-house
16
+ ```
17
+
18
+ 重启 DSH。
19
+
20
+ > 包名带 `@jiugeqqqqq/` 是因为 npm 上裸名 `dsh-house` 已被别人占用。
21
+
22
+ ## 装完什么样
23
+
24
+ 1. **第一次打开侧边栏「房子」** → 如果 `<DSH_HOME>/house.md` 不存在,会自动建一份模板,页面直接能看能改。
25
+ 2. **每一步请求** → 清单全文以 `【资产清单】` 注入系统提示。改了立刻生效,不用重启。
26
+ 3. **页面**:
27
+ - 顶部搜索框:按条目/分组名过滤
28
+ - `+ 新增`:选分组 + 写一行
29
+ - 每条右边 `改` / `删`(删除要二次确认)
30
+ - 底部「搜历史」:按关键词翻会话存档里被压缩掉的原文
31
+ 4. **写回有备份**:每次改动前把 `house.md` 复制成 `house.md.bak-<时间戳>`。
32
+
33
+ 确认装好:
34
+
35
+ ```
36
+ <DSH_HOME>\house.md # 清单本体
37
+ <DSH_HOME>\logs\... # 启动日志里会有 dsh-house: 房子清单 = ...
38
+ ```
39
+
40
+ ## 清单格式
41
+
42
+ 只有两种语法,够用就好:
43
+
44
+ ```markdown
45
+ # 标题(随便写)
46
+
47
+ > 引用块随便写,不进解析
48
+
49
+ ## 分组名
50
+
51
+ - 🏠 **条目名** ✅ 一句话说明
52
+ - 缩进的子项挂在上面那条下面(不算独立条目)
53
+ - 🚧 **在做的东西** 到哪一步了
54
+ ```
55
+
56
+ - `## ` 开一个分组
57
+ - `- ` 开一个条目
58
+ - **缩进的 `- `** 算上一条的子说明(比如「入口:…」「限制:…」),不单独占一条
59
+
60
+ ## 配置
61
+
62
+ ```yaml
63
+ - id: house
64
+ config:
65
+ assetsPath: '' # 留空 = <DSH_HOME>/house.md
66
+ knowledgeDir: '' # 留空 = <DSH_HOME>/knowledge(页面只显示路径,不读内容)
67
+ sessionsDir: '' # 留空 = <DSH_HOME>/sessions(「搜历史」去这里翻)
68
+ autoRotate: false # 默认关,见下
69
+ bridgeUrl: 'http://127.0.0.1:3100'
70
+ bridgeTokenPath: ''
71
+ rotateKeys: ''
72
+ pendingGroup: '正在长出来的'
73
+ rotateCooldownMs: 600000
74
+ ```
75
+
76
+ ## autoRotate(可选,默认关)
77
+
78
+ 如果你有一条能重置会话的桥接(本项目的 QQ 桥接有 `POST /api/session/reset`),
79
+ 可以打开它:**清单被改过、且 `pendingGroup` 那组清空了**,插件就调桥接换新会话,省上下文。
80
+
81
+ ```yaml
82
+ - id: house
83
+ config:
84
+ autoRotate: true
85
+ bridgeUrl: 'http://127.0.0.1:3100'
86
+ bridgeTokenPath: 'D:/path/to/console-token'
87
+ rotateKeys: 'private:123456789'
88
+ pendingGroup: '正在长出来的'
89
+ ```
90
+
91
+ 没有桥接就保持 `false` —— 它不会自己乱换会话。
92
+
93
+ ## 和 dsh-session-rotate 的关系
94
+
95
+ `dsh-house` 自己注入资产清单。如果你同时装了 `dsh-session-rotate`:
96
+
97
+ ```yaml
98
+ - id: session-rotate
99
+ config:
100
+ assetsPath: '' # ← 留空,否则清单在提示词里出现两遍
101
+ ```
102
+
103
+ 默认就是空的,正常不用动。
104
+
105
+ ## 更新
106
+
107
+ ```powershell
108
+ dsh plugin --profile desktop add @jiugeqqqqq/dsh-house@latest
109
+ ```
110
+
111
+ 重启 DSH 生效。
112
+
113
+ **自动检测**:插件每天查一次 npm(配置项 `updateCheck`,默认开)。有新版本时:
114
+
115
+ - `<DSH_HOME>/plugin-updates.json` 里记一条
116
+ - 日志里写一行
117
+ - 侧边栏「房子」页标题旁显示 `v0.2.1 → 0.3.0 可更新`
118
+
119
+ **它不会自动安装** —— DSH 没有热重载,装了也要重启;运行时替换自己的代码风险不值得。
120
+
121
+ ## 常见问题
122
+
123
+ **Q:页面空白 / 显示"解析不出条目"?**
124
+ 说明 `house.md` 被外部程序写坏了结构(比如少了 `## ` 或 `- `)。页面会直接把原文摊开显示,照着补回格式即可。原文件也有 `.bak-*` 备份。
125
+
126
+ **Q:改了清单,模型没反应?**
127
+ 注入是每一步重新读文件的 —— 检查 `assetsPath` 指对了没,以及文件是不是空的。
128
+
129
+ **Q:搜历史搜不到?**
130
+ 它只翻 `sessionsDir` 下最近 30 天、小于 80MB 的 `session.v4.jsonl.zstd`。刚聊的内容可能还没落盘,等一会儿再搜。
131
+
132
+ **Q:「搜历史」会不会很慢?**
133
+ 会扫文件,但只解压匹配的那些行,一般几秒内。对话特别多时可以把 `days` 限定小一点(页面暂固定 30 天)。
134
+
135
+ ## License
136
+
137
+ MIT
package/lib/client.js CHANGED
@@ -225,8 +225,15 @@ window.__ModuleLoader__.load({
225
225
  React.createElement(
226
226
  'div', { key: 'head', style: styles.head },
227
227
  React.createElement('h1', { style: styles.title }, '房子'),
228
- React.createElement('span', { style: styles.sub }, '她的资产与能力清单 · 常驻'),
228
+ React.createElement('span', { style: styles.sub }, '常驻的资产与能力清单'),
229
229
  React.createElement('span', { style: styles.note }, '共 ' + String(total) + ' 条'),
230
+ s.data && s.data.version
231
+ ? React.createElement(
232
+ 'span',
233
+ { style: Object.assign({}, styles.note, s.data.updateAvailable ? { color: '#e0a030' } : null) },
234
+ 'v' + s.data.version + (s.data.updateAvailable ? ' → ' + s.data.latest + ' 可更新' : ''),
235
+ )
236
+ : null,
230
237
  ),
231
238
  );
232
239
 
@@ -286,7 +293,24 @@ window.__ModuleLoader__.load({
286
293
  children.push(React.createElement('div', { key: 'loading', style: styles.note }, '读取中…'));
287
294
  }
288
295
  if (!s.loading && !s.error && shown.length === 0) {
289
- children.push(React.createElement('div', { key: 'empty', style: styles.note }, needle ? '没有匹配的房子。' : '还没有房子。'));
296
+ if (needle) {
297
+ children.push(React.createElement('div', { key: 'empty', style: styles.note }, '没有匹配的房子。'));
298
+ } else if (s.data && s.data.raw) {
299
+ // 解析不出任何条目,但文件非空 —— 多半是结构被写坏了。把原文摊开,
300
+ // 至少能看见内容,不至于以为整份清单丢了。
301
+ children.push(
302
+ React.createElement('div', { key: 'rawhead', style: styles.err }, '清单解析不出条目(格式可能被改坏了),下面是原文:'),
303
+ );
304
+ children.push(
305
+ React.createElement(
306
+ 'pre',
307
+ { key: 'raw', style: { whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: '12px', opacity: 0.8, margin: 0 } },
308
+ s.data.raw,
309
+ ),
310
+ );
311
+ } else {
312
+ children.push(React.createElement('div', { key: 'empty', style: styles.note }, '还没有房子。点上面的「+ 新增」写第一条。'));
313
+ }
290
314
  }
291
315
 
292
316
  shown.forEach(function (g, gi) {
@@ -378,7 +402,7 @@ window.__ModuleLoader__.load({
378
402
  return;
379
403
  }
380
404
  var when = new Date(h.time).toLocaleString('zh-CN', { hour12: false });
381
- var who = h.role === 'user' ? '粑粑' : '她';
405
+ var who = h.role === 'user' ? '你' : 'AI';
382
406
  histChildren.push(
383
407
  React.createElement(
384
408
  'div', { key: 'h' + String(i), style: styles.row },
package/lib/host/index.js CHANGED
@@ -17,10 +17,19 @@ import { readFile, writeFile, mkdir, copyFile } from 'node:fs/promises';
17
17
  import fs from 'node:fs';
18
18
  import zlib from 'node:zlib';
19
19
  import path from 'node:path';
20
+ import { createUpdateChecker } from '../update-check.js';
20
21
 
21
22
  export const name = 'house';
22
23
  export const inject = [];
23
24
 
25
+ const pkgVersion = (() => {
26
+ try {
27
+ return JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version || '0.0.0';
28
+ } catch {
29
+ return '0.0.0';
30
+ }
31
+ })();
32
+
24
33
  const ROUTE_PREFIX = '/dsh-house/api';
25
34
 
26
35
  function writeJson(response, status, payload) {
@@ -102,19 +111,36 @@ export function apply(ctx, config = {}) {
102
111
  ? config.sessionsDir
103
112
  : path.join(dshHome, 'sessions');
104
113
 
114
+ // 版本自检:只查 npm 有没有新版,不自动安装。
115
+ // 注意:本地开发目录的 package.json name 是 dsh-house,而 npm 上是 scoped 包,所以写死 npmName。
116
+ const updateChecker = createUpdateChecker({
117
+ npmName: '@jiugeqqqqq/dsh-house',
118
+ version: pkgVersion,
119
+ log: (m) => ctx.logger?.info('dsh-house: ' + m),
120
+ enabled: config.updateCheck !== false,
121
+ });
122
+ const updateTimer = setTimeout(() => {
123
+ updateChecker.check().catch(() => {});
124
+ }, 15000);
125
+ updateTimer.unref?.();
126
+
105
127
  // 房子清单不存在时自动建一份模板:别人装完第一次打开页面就有东西可看,
106
128
  // 不会像 2026-10-26 那样提示「读不到房子清单」。
107
129
  const TEMPLATE = [
108
130
  '# 房子(资产清单)',
109
131
  '',
110
- '> 每个大操作一行:任务名 + 到哪一步 + 一句话 + 能搜到的词(工具名/时间)。',
111
- '> 只记「有/没有、到哪一步」;细节留在别处,需要时按关键词搜历史。',
132
+ '> 一行一个:**名字** 状态 一句话。',
133
+ '> 只记「有/没有、到哪一步、怎么用一句话」;细节留在别处,需要时用页面底部的「搜历史」翻。',
134
+ '> 这份文件每一轮都会自动进系统提示 —— 改完立刻生效,不用重启。',
112
135
  '',
113
- '## 住着的地方',
114
- '- 🏠 **示例条目** ✅ 这是自动生成的模板,改成你自己的第一条房子',
136
+ '## 已经有的',
137
+ '- 🏠 **第一条** ✅ 把这条改成你真正能用的东西(工具、设备、账号、脚本都算)',
115
138
  '',
116
139
  '## 正在长出来的',
117
- '- 🚧 **把这条删掉** 这组空了,配合 autoRotate 才会在任务收尾后自动换会话',
140
+ '- 🚧 **还没做完的** 写在这组;这组清空后,配合 autoRotate 才会在任务收尾时换会话',
141
+ '',
142
+ '---',
143
+ '更新:' + new Date().toISOString().slice(0, 10),
118
144
  '',
119
145
  ].join('\n');
120
146
 
@@ -166,7 +192,7 @@ export function apply(ctx, config = {}) {
166
192
  const total = groups.reduce((n, g) => n + g.items.length, 0);
167
193
  return {
168
194
  ok: true,
169
- data: { path: assetsPath, missing: false, created: true, note: '已自动创建一份模板清单', groups, total },
195
+ data: { path: assetsPath, missing: false, created: true, note: '已自动创建一份模板清单', groups, total, raw: '' },
170
196
  };
171
197
  } catch {}
172
198
  }
@@ -182,7 +208,10 @@ export function apply(ctx, config = {}) {
182
208
  }
183
209
  const { groups } = parseWithLines(text);
184
210
  const total = groups.reduce((n, g) => n + g.items.length, 0);
185
- return { ok: true, data: { path: assetsPath, missing: false, groups, total } };
211
+ // 2026-10-27:解析出 0 条但文件非空 → 多半是被外部程序写坏了结构。
212
+ // 这时候把原文带回去,页面直接显示,总比"空白一片"让人以为文件丢了强。
213
+ const raw = total === 0 && text.trim() ? text.slice(0, 20000) : '';
214
+ return { ok: true, data: { path: assetsPath, missing: false, groups, total, raw } };
186
215
  }
187
216
 
188
217
  // ── 搜历史原话 ────────────────────────────────────────────────────────────
@@ -418,7 +447,19 @@ export function apply(ctx, config = {}) {
418
447
 
419
448
  try {
420
449
  if (method === 'list') {
421
- writeJson(response, 200, await loadAssets());
450
+ const result = await loadAssets();
451
+ if (result.ok && result.data) {
452
+ const u = updateChecker.get() || {};
453
+ result.data.version = pkgVersion;
454
+ result.data.latest = u.latest || '';
455
+ result.data.updateAvailable = u.updateAvailable === true;
456
+ }
457
+ writeJson(response, 200, result);
458
+ return;
459
+ }
460
+ if (method === 'checkUpdate') {
461
+ const rec = await updateChecker.check({ force: true });
462
+ writeJson(response, 200, { ok: true, data: { ...(rec || {}), version: pkgVersion } });
422
463
  return;
423
464
  }
424
465
  if (method === 'read') {
@@ -0,0 +1,116 @@
1
+ // dsh-affection —— 版本更新检查(自动检测 npm 上有没有新版本)
2
+ //
3
+ // 只做「检测 + 报告」,不擅自替换自己的代码:DSH 没有热重载,
4
+ // 插件在运行中改自己的文件也只会下次重启才生效,风险不值得。
5
+ //
6
+ // 结果写到 <DSH_HOME>/plugin-updates.json,页面/日志都能读:
7
+ // { "dsh-affection": { current, latest, updateAvailable, checkedAt, lastError? } }
8
+ //
9
+ // 频率:默认每 24 小时查一次;失败也记录时间,避免离线时每步重试。
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import os from 'node:os';
13
+
14
+ /** '0.10.2' vs '0.9.9' → 1;相等 0;更小 -1。忽略预发布后缀。 */
15
+ export function compareVersions(a, b) {
16
+ const parse = (v) =>
17
+ String(v || '')
18
+ .trim()
19
+ .replace(/^v/i, '')
20
+ .split('-')[0]
21
+ .split('.')
22
+ .map((x) => Number.parseInt(x, 10) || 0);
23
+ const pa = parse(a);
24
+ const pb = parse(b);
25
+ const len = Math.max(pa.length, pb.length);
26
+ for (let i = 0; i < len; i++) {
27
+ const x = pa[i] || 0;
28
+ const y = pb[i] || 0;
29
+ if (x > y) return 1;
30
+ if (x < y) return -1;
31
+ }
32
+ return 0;
33
+ }
34
+
35
+ export function createUpdateChecker({
36
+ npmName,
37
+ version,
38
+ log,
39
+ enabled = true,
40
+ intervalHours = 24,
41
+ registry = 'https://registry.npmjs.org',
42
+ }) {
43
+ const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
44
+ const statePath = path.join(dshHome, 'plugin-updates.json');
45
+
46
+ function readState() {
47
+ try {
48
+ const j = JSON.parse(fs.readFileSync(statePath, 'utf8'));
49
+ return j && typeof j === 'object' ? j : {};
50
+ } catch {
51
+ return {};
52
+ }
53
+ }
54
+
55
+ function writeState(state) {
56
+ try {
57
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
58
+ const tmp = `${statePath}.tmp-${process.pid}`;
59
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 1), 'utf8');
60
+ fs.renameSync(tmp, statePath);
61
+ } catch {}
62
+ }
63
+
64
+ function record(partial) {
65
+ const state = readState();
66
+ state[npmName] = { ...(state[npmName] || {}), ...partial };
67
+ writeState(state);
68
+ return state[npmName];
69
+ }
70
+
71
+ async function check({ force = false } = {}) {
72
+ if (!enabled) return null;
73
+ const prev = readState()[npmName] || {};
74
+ const now = Date.now();
75
+ const interval = Math.max(1, Number(intervalHours) || 24) * 3600 * 1000;
76
+ if (!force && prev.checkedAt && now - prev.checkedAt < interval) return prev;
77
+
78
+ try {
79
+ const res = await fetch(`${registry.replace(/\/+$/, '')}/${npmName.replace('/', '%2F')}/latest`, {
80
+ headers: { accept: 'application/json' },
81
+ signal: AbortSignal.timeout(8000),
82
+ });
83
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
84
+ const j = await res.json();
85
+ const latest = String(j?.version || '');
86
+ if (!latest) throw new Error('响应里没有 version');
87
+ const available = compareVersions(latest, version) > 0;
88
+ const rec = record({
89
+ current: version,
90
+ latest,
91
+ updateAvailable: available,
92
+ checkedAt: now,
93
+ lastError: '',
94
+ updateCommand: available ? `dsh plugin --profile <你的profile> add ${npmName}@latest` : '',
95
+ });
96
+ if (available) {
97
+ log?.(`发现新版本 ${latest}(当前 ${version}):dsh plugin --profile <你的profile> add ${npmName}@latest`);
98
+ }
99
+ return rec;
100
+ } catch (e) {
101
+ const rec = record({
102
+ current: version,
103
+ checkedAt: now,
104
+ lastError: String(e?.message ?? e),
105
+ });
106
+ log?.(`检查更新失败(不影响使用):${rec.lastError}`);
107
+ return rec;
108
+ }
109
+ }
110
+
111
+ function get() {
112
+ return readState()[npmName] || null;
113
+ }
114
+
115
+ return { check, get, npmName, version };
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
- {
1
+ {
2
2
  "name": "@jiugeqqqqq/dsh-house",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "房子 — 常驻的资产/能力清单,配一个侧边栏页面。清单会自动进系统提示(装了就生效);房子只记有没有、到哪一步,细节放 knowledge 和日志里。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,8 +27,18 @@
27
27
  },
28
28
  "manifestVersion": 1
29
29
  },
30
+ "keywords": [
31
+ "dsh",
32
+ "deepseek-harness",
33
+ "assets",
34
+ "capabilities",
35
+ "sidebar",
36
+ "memory"
37
+ ],
38
+ "author": "jiuge",
30
39
  "files": [
31
40
  "lib/**/*.js",
32
- "cordis.patch.yml"
41
+ "cordis.patch.yml",
42
+ "README.md"
33
43
  ]
34
44
  }