dsh-daughter 0.1.0 → 0.6.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.
Files changed (44) hide show
  1. package/README.md +151 -12
  2. package/bin/connector-token.mjs +78 -0
  3. package/config.yaml +141 -1
  4. package/dist/config.js +234 -4
  5. package/dist/host.d.ts +2 -0
  6. package/dist/host.js +241 -19
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +223 -19
  9. package/dist/runtime/connector-auth.d.ts +56 -0
  10. package/dist/runtime/connector-auth.js +148 -0
  11. package/dist/runtime/content.d.ts +11 -2
  12. package/dist/runtime/content.js +36 -6
  13. package/dist/runtime/desktop-input.d.ts +2 -0
  14. package/dist/runtime/desktop-input.js +3 -1
  15. package/dist/runtime/desktop-observe.d.ts +16 -0
  16. package/dist/runtime/desktop-observe.js +30 -8
  17. package/dist/runtime/douyin-link.d.ts +49 -0
  18. package/dist/runtime/douyin-link.js +102 -0
  19. package/dist/runtime/expression.d.ts +60 -0
  20. package/dist/runtime/expression.js +113 -0
  21. package/dist/runtime/filter.js +23 -0
  22. package/dist/runtime/maintenance.d.ts +0 -5
  23. package/dist/runtime/maintenance.js +59 -8
  24. package/dist/runtime/memory.d.ts +34 -2
  25. package/dist/runtime/memory.js +130 -18
  26. package/dist/runtime/motivation.d.ts +65 -0
  27. package/dist/runtime/motivation.js +282 -0
  28. package/dist/runtime/panels.js +34 -1
  29. package/dist/runtime/personality.js +3 -11
  30. package/dist/runtime/pet-link.d.ts +87 -0
  31. package/dist/runtime/pet-link.js +129 -0
  32. package/dist/runtime/reset.d.ts +40 -0
  33. package/dist/runtime/reset.js +253 -0
  34. package/dist/runtime/speak.d.ts +79 -0
  35. package/dist/runtime/speak.js +277 -0
  36. package/dist/runtime/state.js +66 -9
  37. package/dist/runtime/vpet-control.d.ts +2 -0
  38. package/dist/runtime/vpet-control.js +5 -3
  39. package/dist/store.d.ts +63 -1
  40. package/dist/store.js +233 -14
  41. package/dist/types.d.ts +253 -0
  42. package/dist/util.d.ts +10 -0
  43. package/dist/util.js +37 -0
  44. package/package.json +9 -2
package/dist/config.js CHANGED
@@ -21,6 +21,9 @@ export const defaultConfig = {
21
21
  link_proximity: 0.1,
22
22
  },
23
23
  min_score: 0.12,
24
+ min_semantic: 0.01,
25
+ reconstruct_distance: 0.08,
26
+ reconstruct_cooldown_minutes: 360,
24
27
  expand_fulltext: true,
25
28
  fulltext_excerpt_chars: 120,
26
29
  },
@@ -28,6 +31,7 @@ export const defaultConfig = {
28
31
  max_depth: 2,
29
32
  max_injected: 2,
30
33
  hop_decay: 0.6,
34
+ fallback_when_no_match: true,
31
35
  },
32
36
  skills: {
33
37
  max_main: 1,
@@ -38,6 +42,66 @@ export const defaultConfig = {
38
42
  min_associated_score: 0.15,
39
43
  min_steps_to_learn: 2,
40
44
  },
45
+ motivation: {
46
+ enabled: true,
47
+ trait_threshold_gain: 0.25,
48
+ min_threshold: 0.05,
49
+ trait_signal_gain: 0.2,
50
+ familiarity: { k: 3, peak: 0.45, sigma: 0.35 },
51
+ memory: {
52
+ outcome_weight: 0.4,
53
+ emotion_weight: 0.3,
54
+ efficacy_weight: 0.3,
55
+ half_life_days: 30,
56
+ lookback_days: 30,
57
+ },
58
+ breaker: { fail_streak: 3, base_cooldown_min: 10, max_cooldown_min: 1440 },
59
+ emotion_override: {
60
+ safety_low: 0.35,
61
+ safety_low_offset: 0.1,
62
+ stress_high: 0.7,
63
+ stress_high_offset: 0.08,
64
+ pleasure_low: -0.25,
65
+ pleasure_low_offset: 0.06,
66
+ arousal_high: 0.4,
67
+ arousal_high_offset: -0.05,
68
+ fatigue_high: 0.8,
69
+ fatigue_high_offset: 0.08,
70
+ clamp_min: -0.15,
71
+ clamp_max: 0.2,
72
+ },
73
+ situation: { min_multiplier: 0.85, max_multiplier: 1.15 },
74
+ hold_ratio: 0.7,
75
+ },
76
+ speak: {
77
+ // 默认关闭:主动开口是自主行为,必须显式开启,不能装完就自己说话
78
+ enabled: false,
79
+ check_interval_min: 3,
80
+ // 活跃档:安静 25 分钟就进入考虑区间,思念 2 小时就到半饱和。
81
+ // 早期 45/180/0.62 的组合在写代码的场景下要沉默四五个小时才开口,等于装了个哑巴。
82
+ min_idle_min: 25,
83
+ idle_half_min: 120,
84
+ trait_urge_gain: 1,
85
+ threshold: 0.52,
86
+ threshold_min: 0.4,
87
+ refractory_min: 60,
88
+ max_per_day: 8,
89
+ quiet_hours: [0, 7],
90
+ quiet_penalty: 0.35,
91
+ busy_penalty: 0.25,
92
+ hold_ratio: 0.75,
93
+ carry_step: 0.05,
94
+ carry_decay: 0.02,
95
+ carry_max: 0.25,
96
+ jitter: 0.04,
97
+ },
98
+ expression: {
99
+ enabled: true,
100
+ // 默认只发桌宠;要接别的应用,在 config.yaml 里加 pet / http / file 出口
101
+ sinks: [{ type: 'vpet' }],
102
+ // 出厂配置下若 dsh-pet 在跑,就让它接管,避免两套桌宠各弹一遍
103
+ prefer_pet: true,
104
+ },
41
105
  reflex: {
42
106
  enabled: true,
43
107
  max_match_ms: 5,
@@ -74,6 +138,10 @@ export const defaultConfig = {
74
138
  backend: 'sqlite',
75
139
  single_writer: true,
76
140
  },
141
+ security: {
142
+ // 留空 = 使用 brain/brainstem/connector_token;显式填写则固定用它(§12.2)
143
+ connector_token: '',
144
+ },
77
145
  decay: {
78
146
  weight_decay_rate: 0.02,
79
147
  recall_boost: 0.05,
@@ -107,9 +175,18 @@ export const defaultConfig = {
107
175
  evidence_cap: 200,
108
176
  },
109
177
  desktop: {
110
- enabled: true,
178
+ // 默认关闭(§12.2):桌面连接器能给的是屏幕画面与键鼠注入,
179
+ // 属于"开了就有风险"的能力,必须由用户显式打开。
180
+ enabled: false,
111
181
  bridge_url: 'http://127.0.0.1:47614',
112
182
  },
183
+ douyin: {
184
+ enabled: true,
185
+ // 抖音插件的共享记忆库(QQ/抖音共用的那份),只读
186
+ memory_db: '~/.dsh/douyin-browser/memory/shared.sqlite',
187
+ poll_interval_min: 10,
188
+ max_items: 5,
189
+ },
113
190
  filter: {
114
191
  enabled: true,
115
192
  source_weights: {
@@ -124,6 +201,7 @@ export const defaultConfig = {
124
201
  },
125
202
  shield: { sources: [], keywords: [], patterns: [] },
126
203
  habituation: { window_minutes: 30, max_repeat: 3 },
204
+ burst: { window_minutes: 60, max_keep: 3, exempt_sources: ['chat', 'qq', 'douyin_dm'] },
127
205
  salience: { drop_below: 0.2 },
128
206
  forget: { enabled: true, idle_days: 30, min_salience: 0.2 },
129
207
  },
@@ -189,9 +267,6 @@ function warnUnsupported(config, logger) {
189
267
  backend: config.storage.backend,
190
268
  });
191
269
  }
192
- if (config.desktop.enabled) {
193
- logger.warn('dsh-daughter: desktop.enabled=true,但桌面端能力尚未实现(§14 里程碑 P9),配置值已忽略');
194
- }
195
270
  if (!config.safety.diagnosis_labels_forbidden) {
196
271
  logger.warn('dsh-daughter: diagnosis_labels_forbidden=false,输出侧诊断词检查已关闭(不建议)');
197
272
  }
@@ -217,6 +292,10 @@ function mergeDeep(base, override) {
217
292
  }
218
293
  function normalizeConfig(config) {
219
294
  config.retrieval.top_k = Math.max(1, Math.round(num(config.retrieval.top_k, 5)));
295
+ config.retrieval.min_semantic = clampUnit(num(config.retrieval.min_semantic, 0.01), 0.01);
296
+ // 回看感受的触发距离:0 会让每次召回都写一条,1 等于永不写,两端都无意义
297
+ config.retrieval.reconstruct_distance = Math.min(0.5, clampUnit(num(config.retrieval.reconstruct_distance, 0.08), 0.08));
298
+ config.retrieval.reconstruct_cooldown_minutes = Math.min(1440, Math.max(0, Math.round(num(config.retrieval.reconstruct_cooldown_minutes, 360))));
220
299
  // 打分权重归一化到和为 1:min_score 是绝对门限,权重整体放大 3 倍会让召回条数
221
300
  // 静默变化,不归一化等于把两个无关参数耦合在一起
222
301
  const weights = config.retrieval.score_weights;
@@ -236,7 +315,140 @@ function normalizeConfig(config) {
236
315
  config.retrieval.expand_fulltext = config.retrieval.expand_fulltext !== false;
237
316
  config.retrieval.fulltext_excerpt_chars = Math.min(400, Math.max(40, Math.round(num(config.retrieval.fulltext_excerpt_chars, 120))));
238
317
  config.skills.min_steps_to_learn = Math.min(8, Math.max(2, Math.round(num(config.skills.min_steps_to_learn, 2))));
318
+ if (!config.motivation || typeof config.motivation !== 'object') {
319
+ config.motivation = structuredClone(defaultConfig.motivation);
320
+ }
321
+ const motivation = config.motivation;
322
+ const defaults = defaultConfig.motivation;
323
+ motivation.familiarity ??= { ...defaults.familiarity };
324
+ motivation.memory ??= { ...defaults.memory };
325
+ motivation.breaker ??= { ...defaults.breaker };
326
+ motivation.emotion_override ??= { ...defaults.emotion_override };
327
+ motivation.situation ??= { ...defaults.situation };
328
+ motivation.enabled = motivation.enabled !== false;
329
+ motivation.trait_threshold_gain = clampUnit(motivation.trait_threshold_gain, 0.25);
330
+ // 下限不能超过基础阈值,否则 skill 永远不可能 act
331
+ motivation.min_threshold = clampRange(motivation.min_threshold, 0, Math.max(0, Math.min(0.5, config.skills.min_main_score)), defaults.min_threshold);
332
+ // 上限 0.9:乘子最低留 0.1,避免人格把某个通道直接归零
333
+ motivation.trait_signal_gain = clampRange(motivation.trait_signal_gain, 0, 0.9, defaults.trait_signal_gain);
334
+ motivation.familiarity.k = Math.max(1, num(motivation.familiarity.k, 3));
335
+ motivation.familiarity.peak = clampUnit(motivation.familiarity.peak, 0.45);
336
+ motivation.familiarity.sigma = clampRange(motivation.familiarity.sigma, 0.05, 1, 0.35);
337
+ const memory = motivation.memory;
338
+ const memoryKeys = ['outcome_weight', 'emotion_weight', 'efficacy_weight'];
339
+ let memorySum = 0;
340
+ for (const key of memoryKeys) {
341
+ memory[key] = Math.max(0, num(memory[key], defaultConfig.motivation.memory[key]));
342
+ memorySum += memory[key];
343
+ }
344
+ if (!(memorySum > 0)) {
345
+ Object.assign(memory, defaultConfig.motivation.memory);
346
+ }
347
+ else {
348
+ for (const key of memoryKeys)
349
+ memory[key] = Number((memory[key] / memorySum).toFixed(4));
350
+ }
351
+ memory.half_life_days = clampRange(memory.half_life_days, 0.1, 3650, 30);
352
+ memory.lookback_days = Math.min(3650, Math.max(1, Math.round(num(memory.lookback_days, 30))));
353
+ const breaker = motivation.breaker;
354
+ breaker.fail_streak = Math.min(20, Math.max(1, Math.round(num(breaker.fail_streak, 3))));
355
+ breaker.base_cooldown_min = clampRange(breaker.base_cooldown_min, 0, 1440, 10);
356
+ breaker.max_cooldown_min = clampRange(breaker.max_cooldown_min, breaker.base_cooldown_min, 10080, 1440);
357
+ const override = motivation.emotion_override;
358
+ override.safety_low = clampUnit(override.safety_low, 0.35);
359
+ override.safety_low_offset = clampRange(override.safety_low_offset, -0.5, 0.5, 0.1);
360
+ override.stress_high = clampUnit(override.stress_high, 0.7);
361
+ override.stress_high_offset = clampRange(override.stress_high_offset, -0.5, 0.5, 0.08);
362
+ override.pleasure_low = clampRange(override.pleasure_low, -1, 1, -0.25);
363
+ override.pleasure_low_offset = clampRange(override.pleasure_low_offset, -0.5, 0.5, 0.06);
364
+ override.arousal_high = clampRange(override.arousal_high, -1, 1, 0.4);
365
+ override.arousal_high_offset = clampRange(override.arousal_high_offset, -0.5, 0.5, -0.05);
366
+ override.fatigue_high = clampUnit(override.fatigue_high, 0.8);
367
+ override.fatigue_high_offset = clampRange(override.fatigue_high_offset, -0.5, 0.5, 0.08);
368
+ override.clamp_min = clampRange(override.clamp_min, -0.5, 0.5, -0.15);
369
+ override.clamp_max = clampRange(override.clamp_max, override.clamp_min, 0.5, 0.2);
370
+ motivation.situation.min_multiplier = clampRange(motivation.situation.min_multiplier, 0.5, 2, 0.85);
371
+ motivation.situation.max_multiplier = clampRange(motivation.situation.max_multiplier, motivation.situation.min_multiplier, 2, 1.15);
372
+ motivation.hold_ratio = clampUnit(motivation.hold_ratio, 0.7);
373
+ if (!config.speak || typeof config.speak !== 'object') {
374
+ config.speak = structuredClone(defaultConfig.speak);
375
+ }
376
+ const speak = config.speak;
377
+ speak.enabled = speak.enabled !== false;
378
+ speak.check_interval_min = clampRange(speak.check_interval_min, 1, 120, 3);
379
+ speak.min_idle_min = clampRange(speak.min_idle_min, 1, 2880, 25);
380
+ speak.idle_half_min = clampRange(speak.idle_half_min, 5, 2880, 120);
381
+ speak.trait_urge_gain = clampRange(speak.trait_urge_gain, 0, 2, 1);
382
+ speak.threshold = clampRange(speak.threshold, 0.1, 1, 0.52);
383
+ // 下限不能高于基础阈值,否则等于把门槛整体抬高
384
+ speak.threshold_min = clampRange(speak.threshold_min, 0, speak.threshold, 0.4);
385
+ speak.refractory_min = clampRange(speak.refractory_min, 0, 10080, 60);
386
+ speak.max_per_day = Math.min(48, Math.max(0, Math.round(num(speak.max_per_day, 8))));
387
+ speak.quiet_penalty = clampRange(speak.quiet_penalty, 0, 1, 0.35);
388
+ // 会议/游戏仍然要能拦住:这个系数调小,长时间沉默会连开会都敢插话
389
+ speak.busy_penalty = clampRange(speak.busy_penalty, 0, 1, 0.25);
390
+ speak.hold_ratio = clampRange(speak.hold_ratio, 0.1, 1, 0.75);
391
+ speak.carry_step = clampRange(speak.carry_step, 0, 0.5, 0.05);
392
+ speak.carry_decay = clampRange(speak.carry_decay, 0, 0.5, 0.02);
393
+ speak.carry_max = clampRange(speak.carry_max, 0, 1, 0.25);
394
+ speak.jitter = clampRange(speak.jitter, 0, 0.3, 0.04);
395
+ // 安静时段:YAML 可能写成字符串或只有一个值,统一收敛成 [起, 止)
396
+ const quietRaw = Array.isArray(speak.quiet_hours) ? speak.quiet_hours : defaultConfig.speak.quiet_hours;
397
+ const quietStart = Math.min(23, Math.max(0, Math.round(num(quietRaw[0], 0))));
398
+ const quietEnd = Math.min(24, Math.max(0, Math.round(num(quietRaw[1], 7))));
399
+ speak.quiet_hours = [quietStart, quietEnd];
400
+ if (!config.expression || typeof config.expression !== 'object') {
401
+ config.expression = structuredClone(defaultConfig.expression);
402
+ }
403
+ const expression = config.expression;
404
+ expression.enabled = expression.enabled !== false;
405
+ expression.prefer_pet = expression.prefer_pet !== false;
406
+ const rawSinks = Array.isArray(expression.sinks) ? expression.sinks : [];
407
+ const sinks = [];
408
+ for (const item of rawSinks) {
409
+ if (!item || typeof item !== 'object')
410
+ continue;
411
+ const type = String(item.type ?? '');
412
+ if (type === 'vpet') {
413
+ sinks.push({ type: 'vpet' });
414
+ continue;
415
+ }
416
+ if (type === 'http') {
417
+ const url = typeof item.url === 'string' ? item.url.trim() : '';
418
+ if (!url)
419
+ continue;
420
+ sinks.push({ type: 'http', url, timeout_ms: clampRange(item.timeout_ms, 200, 30000, 3000) });
421
+ continue;
422
+ }
423
+ if (type === 'pet') {
424
+ // url 可省略:缺省由宿主在运行时用自己监听的端口拼出来,省得用户抄端口
425
+ const url = typeof item.url === 'string' ? item.url.trim() : '';
426
+ const petId = typeof item.pet === 'string' ? item.pet.trim() : '';
427
+ const route = typeof item.route === 'string' ? item.route.trim() : '';
428
+ sinks.push({
429
+ type: 'pet',
430
+ ...(url ? { url } : {}),
431
+ ...(petId ? { pet: petId } : {}),
432
+ ...(route ? { route } : {}),
433
+ timeout_ms: clampRange(item.timeout_ms, 200, 30000, 3000),
434
+ // 0 是合法值(只要动作不要文字),所以下限放 0
435
+ duration_ms: clampRange(item.duration_ms, 0, 60000, 10000),
436
+ });
437
+ continue;
438
+ }
439
+ if (type === 'file') {
440
+ const path = typeof item.path === 'string' ? item.path.trim() : '';
441
+ if (!path)
442
+ continue;
443
+ sinks.push({ type: 'file', path });
444
+ continue;
445
+ }
446
+ // 未知 type 静默丢弃,配置手滑不该拖垮整个插件
447
+ }
448
+ // 一个都没配成时退回桌宠:宁可只到气泡,也不要静默丢消息
449
+ expression.sinks = sinks.length > 0 ? sinks : [{ type: 'vpet' }];
239
450
  config.association.max_depth = Math.min(3, Math.max(1, Math.round(num(config.association.max_depth, 2))));
451
+ config.association.fallback_when_no_match = config.association.fallback_when_no_match !== false;
240
452
  config.budget.max_total_tokens = Math.max(200, Math.round(num(config.budget.max_total_tokens, 3000)));
241
453
  config.safety.thresholds.stress_level1 = num(config.safety.thresholds.stress_level1, 0.7);
242
454
  config.safety.thresholds.safety_level2 = num(config.safety.thresholds.safety_level2, 0.35);
@@ -247,6 +459,10 @@ function normalizeConfig(config) {
247
459
  const filter = config.filter;
248
460
  filter.habituation.window_minutes = Math.min(1440, Math.max(1, Math.round(num(filter.habituation.window_minutes, 30))));
249
461
  filter.habituation.max_repeat = Math.min(20, Math.max(1, Math.round(num(filter.habituation.max_repeat, 3))));
462
+ filter.burst.window_minutes = Math.min(1440, Math.max(1, Math.round(num(filter.burst.window_minutes, 60))));
463
+ filter.burst.max_keep = Math.min(20, Math.max(1, Math.round(num(filter.burst.max_keep, 3))));
464
+ filter.burst.exempt_sources = (Array.isArray(filter.burst.exempt_sources) ? filter.burst.exempt_sources : [])
465
+ .filter((source) => typeof source === 'string' && source.length > 0);
250
466
  filter.salience.drop_below = clampUnit(filter.salience.drop_below, 0.2);
251
467
  filter.forget.idle_days = Math.min(3650, Math.max(1, Math.round(num(filter.forget.idle_days, 30))));
252
468
  filter.forget.min_salience = clampUnit(filter.forget.min_salience, 0.2);
@@ -265,6 +481,17 @@ function normalizeConfig(config) {
265
481
  bodyLink.fatigue_weight = clampUnit(bodyLink.fatigue_weight, 0.5);
266
482
  bodyLink.stress_weight = clampUnit(bodyLink.stress_weight, 0.3);
267
483
  bodyLink.max_influence = clampUnit(bodyLink.max_influence, 0.6);
484
+ const douyin = config.douyin;
485
+ douyin.enabled = douyin.enabled !== false;
486
+ douyin.memory_db = typeof douyin.memory_db === 'string' ? douyin.memory_db.trim() : '';
487
+ // 1 分钟一次已经比自己刷得还勤,再快只是空读;上限一天一次的量级足够追平
488
+ douyin.poll_interval_min = Math.min(1440, Math.max(1, Math.round(num(douyin.poll_interval_min, 10))));
489
+ douyin.max_items = Math.min(20, Math.max(1, Math.round(num(douyin.max_items, 5))));
490
+ // 连接器认证(§12.2):空字符串表示"用文件令牌",过短的显式令牌在运行时会被忽略并告警
491
+ config.security ??= { connector_token: '' };
492
+ config.security.connector_token = String(config.security.connector_token ?? '').trim();
493
+ config.desktop.enabled = config.desktop.enabled === true;
494
+ config.desktop.bridge_url = String(config.desktop.bridge_url ?? '').trim() || 'http://127.0.0.1:47614';
268
495
  return config;
269
496
  }
270
497
  function num(value, fallback) {
@@ -274,3 +501,6 @@ function clampUnit(value, fallback) {
274
501
  const number = num(value, fallback);
275
502
  return Math.min(1, Math.max(0, number));
276
503
  }
504
+ function clampRange(value, min, max, fallback) {
505
+ return Math.min(max, Math.max(min, num(value, fallback)));
506
+ }
package/dist/host.d.ts CHANGED
@@ -11,6 +11,8 @@ interface HostCtx {
11
11
  effect(fn: () => (() => void) | void, label?: string): () => void;
12
12
  emit?(name: string, payload: unknown): void;
13
13
  webServer?: {
14
+ /** 宿主实际监听的端口(配置写 0 时由系统分配),dsh-pet 出口靠它拼本机地址 */
15
+ port?: number;
14
16
  register(route: {
15
17
  kind: 'exact' | 'prefix';
16
18
  path: string;