dsh-diary-memory 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +150 -2
- package/cordis.patch.yml +40 -0
- package/icon.svg +1 -0
- package/lib/bailian-client.d.ts +38 -0
- package/lib/bailian-client.js +120 -0
- package/lib/bailian-client.js.map +1 -0
- package/lib/config.d.ts +46 -0
- package/lib/config.js +90 -0
- package/lib/config.js.map +1 -0
- package/lib/diary.d.ts +72 -0
- package/lib/diary.js +157 -0
- package/lib/diary.js.map +1 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +14 -0
- package/lib/index.js.map +1 -0
- package/lib/memory-store.d.ts +60 -0
- package/lib/memory-store.js +143 -0
- package/lib/memory-store.js.map +1 -0
- package/lib/orchestrator.d.ts +23 -0
- package/lib/orchestrator.js +118 -0
- package/lib/orchestrator.js.map +1 -0
- package/lib/projection.d.ts +15 -0
- package/lib/projection.js +23 -0
- package/lib/projection.js.map +1 -0
- package/lib/retrieval-service.d.ts +55 -0
- package/lib/retrieval-service.js +123 -0
- package/lib/retrieval-service.js.map +1 -0
- package/lib/tools.d.ts +5 -0
- package/lib/tools.js +18 -0
- package/lib/tools.js.map +1 -0
- package/locale/compaction/en.json +1 -0
- package/locale/compaction/zh.json +1 -0
- package/locale/en.json +1 -0
- package/locale/tools/en.json +1 -0
- package/locale/tools/zh.json +1 -0
- package/locale/zh.json +1 -0
- package/package.json +71 -4
- package/src/bailian-client.ts +121 -0
- package/src/config.ts +107 -0
- package/src/diary.ts +170 -0
- package/src/index.ts +21 -0
- package/src/memory-store.ts +184 -0
- package/src/orchestrator.ts +121 -0
- package/src/projection.ts +30 -0
- package/src/retrieval-service.ts +136 -0
- package/src/tools.ts +21 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
4
|
+
import { BailianClient } from './bailian-client.js';
|
|
5
|
+
import { MemoryStore } from './memory-store.js';
|
|
6
|
+
import type { Diary } from './diary.js';
|
|
7
|
+
import type { ResolvedConfig } from './config.js';
|
|
8
|
+
import type { DiaryProjectionState } from './projection.js';
|
|
9
|
+
declare module '@deepseek-ai/cordis' {
|
|
10
|
+
interface Context {
|
|
11
|
+
diaryMemory: RetrievalService;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
declare module '@deepseek-ai/dsh-llm' {
|
|
15
|
+
interface MessageSourceMap {
|
|
16
|
+
'diary-memory': {
|
|
17
|
+
kind: 'diary-memory';
|
|
18
|
+
form: 'recall';
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export interface RecallOptions {
|
|
23
|
+
limit?: number;
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}
|
|
27
|
+
export interface RecallHit {
|
|
28
|
+
id: string;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
recordedAt: string;
|
|
31
|
+
diary: Diary;
|
|
32
|
+
similarity: number;
|
|
33
|
+
score: number;
|
|
34
|
+
}
|
|
35
|
+
export declare class RetrievalService extends Service {
|
|
36
|
+
readonly config: ResolvedConfig;
|
|
37
|
+
readonly client: BailianClient;
|
|
38
|
+
readonly store: MemoryStore;
|
|
39
|
+
readonly lifetime: AbortController;
|
|
40
|
+
private readonly jobs;
|
|
41
|
+
private readonly recovery;
|
|
42
|
+
constructor(ctx: Context, config: ResolvedConfig, dependencies?: {
|
|
43
|
+
store?: MemoryStore;
|
|
44
|
+
client?: BailianClient;
|
|
45
|
+
});
|
|
46
|
+
initialize(): Promise<void>;
|
|
47
|
+
stateOf(session: Session): DiaryProjectionState;
|
|
48
|
+
track<T>(operation: () => Promise<T>): Promise<T>;
|
|
49
|
+
dispose(): Promise<void>;
|
|
50
|
+
recall(query: string, options?: RecallOptions): Promise<RecallHit[]>;
|
|
51
|
+
/** Session log is authoritative. Only a matching flushed replacement can publish memory. */
|
|
52
|
+
reconcile(session: Session): Promise<void>;
|
|
53
|
+
private reconcileOnce;
|
|
54
|
+
continuityContext(): string;
|
|
55
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { BailianClient } from './bailian-client.js';
|
|
4
|
+
import { MemoryStore } from './memory-store.js';
|
|
5
|
+
import { renderDiary } from './diary.js';
|
|
6
|
+
import { diaryProjection } from './projection.js';
|
|
7
|
+
export class RetrievalService extends Service {
|
|
8
|
+
config;
|
|
9
|
+
client;
|
|
10
|
+
store;
|
|
11
|
+
lifetime = new AbortController();
|
|
12
|
+
jobs = new Set();
|
|
13
|
+
recovery = new WeakMap();
|
|
14
|
+
constructor(ctx, config, dependencies = {}) {
|
|
15
|
+
super(ctx, 'diaryMemory');
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.store = dependencies.store ?? new MemoryStore(config.databasePath);
|
|
18
|
+
this.client = dependencies.client ?? new BailianClient(config, { signal: this.lifetime.signal });
|
|
19
|
+
ctx.effect(() => () => this.dispose(), 'diaryMemory.lifecycle()');
|
|
20
|
+
}
|
|
21
|
+
async initialize() {
|
|
22
|
+
this.ctx.sessionProjections.register(diaryProjection);
|
|
23
|
+
this.ctx.on('session/created', session => {
|
|
24
|
+
void this.track(() => this.reconcile(session)).catch(() => this.ctx.logger.warn('日记恢复未完成;保留待提交记录,下次使用时重试'));
|
|
25
|
+
});
|
|
26
|
+
for (const session of this.ctx.sessions.list())
|
|
27
|
+
await this.track(() => this.reconcile(session));
|
|
28
|
+
}
|
|
29
|
+
stateOf(session) {
|
|
30
|
+
const state = this.ctx.sessionProjections.stateOf(session, diaryProjection.key);
|
|
31
|
+
if (!state)
|
|
32
|
+
throw new Error('日记轮次 projection 未注册');
|
|
33
|
+
return state;
|
|
34
|
+
}
|
|
35
|
+
track(operation) {
|
|
36
|
+
this.lifetime.signal.throwIfAborted();
|
|
37
|
+
// Preserve native synchronous lock/maintenance admission before the first await.
|
|
38
|
+
let promise;
|
|
39
|
+
try {
|
|
40
|
+
promise = operation();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
promise = Promise.reject(error);
|
|
44
|
+
}
|
|
45
|
+
this.jobs.add(promise);
|
|
46
|
+
void promise.then(() => this.jobs.delete(promise), () => this.jobs.delete(promise));
|
|
47
|
+
return promise;
|
|
48
|
+
}
|
|
49
|
+
async dispose() {
|
|
50
|
+
this.lifetime.abort(new Error('日记记忆插件已卸载'));
|
|
51
|
+
await Promise.allSettled([...this.jobs]);
|
|
52
|
+
this.store.close();
|
|
53
|
+
}
|
|
54
|
+
recall(query, options = {}) {
|
|
55
|
+
return this.track(async () => {
|
|
56
|
+
const text = query.trim();
|
|
57
|
+
if (!text || text.length > 8000)
|
|
58
|
+
throw new TypeError('recall query 必须为 1–8000 字符');
|
|
59
|
+
const limit = options.limit ?? this.config.recallCount;
|
|
60
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > this.config.candidateCount)
|
|
61
|
+
throw new TypeError('recall limit 超出候选数量');
|
|
62
|
+
const signal = AbortSignal.any([this.lifetime.signal, ...options.signal ? [options.signal] : []]);
|
|
63
|
+
signal.throwIfAborted();
|
|
64
|
+
const namespace = this.config.namespace;
|
|
65
|
+
if (!this.store.count(namespace, this.client.embeddingKey)) {
|
|
66
|
+
if (this.store.count(namespace))
|
|
67
|
+
throw new Error('已有日记使用不同的 embedding 模型、维度或端点,请使用原向量配置或重新索引');
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const vector = await this.client.embed(text, { textType: 'query', signal });
|
|
71
|
+
const candidates = this.store.search(vector, { namespace, embeddingKey: this.client.embeddingKey, limit: this.config.candidateCount, sessionId: options.sessionId });
|
|
72
|
+
if (!candidates.length)
|
|
73
|
+
return [];
|
|
74
|
+
const ranking = await this.client.rerank(text, candidates.map(item => item.search_text), { topN: Math.min(limit, candidates.length), signal });
|
|
75
|
+
signal.throwIfAborted();
|
|
76
|
+
return ranking.map(item => {
|
|
77
|
+
const match = candidates[item.index];
|
|
78
|
+
return { id: match.id, sessionId: match.session_id, recordedAt: match.diary.recordedAt, diary: match.diary, similarity: match.similarity, score: item.relevance_score };
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Session log is authoritative. Only a matching flushed replacement can publish memory. */
|
|
83
|
+
reconcile(session) {
|
|
84
|
+
const existing = this.recovery.get(session);
|
|
85
|
+
if (existing)
|
|
86
|
+
return existing;
|
|
87
|
+
const promise = this.reconcileOnce(session).finally(() => this.recovery.delete(session));
|
|
88
|
+
this.recovery.set(session, promise);
|
|
89
|
+
return promise;
|
|
90
|
+
}
|
|
91
|
+
async reconcileOnce(session) {
|
|
92
|
+
const pending = this.store.pending(session.id);
|
|
93
|
+
if (!pending.length)
|
|
94
|
+
return;
|
|
95
|
+
const events = session.snapshotEvents();
|
|
96
|
+
const commits = [];
|
|
97
|
+
for (const record of pending) {
|
|
98
|
+
const checkpoint = events.find((event, index) => {
|
|
99
|
+
if (event.type !== 'user/message' || event.data.source.kind !== 'compact-checkpoint' || event.data.source.compactionId !== record.compaction_id || typeof event.surfaceOp !== 'object' || event.surfaceOp.op !== 'replace')
|
|
100
|
+
return false;
|
|
101
|
+
const summary = events[index - 1];
|
|
102
|
+
return summary?.type === 'compaction/summary'
|
|
103
|
+
&& summary.data.compactionId === record.compaction_id
|
|
104
|
+
&& isDeepStrictEqual(summary.data.shadowedSeqs, record.sourceSeqs)
|
|
105
|
+
&& isDeepStrictEqual(summary.data.summary, [{ type: 'text', text: renderDiary(record.diary) }]);
|
|
106
|
+
});
|
|
107
|
+
if (checkpoint)
|
|
108
|
+
commits.push({ id: record.id, seq: checkpoint.seq });
|
|
109
|
+
else if (events.some(event => event.type === 'compaction/end' && event.data.compactionId === record.compaction_id && event.data.error !== undefined))
|
|
110
|
+
this.store.abort(record.id);
|
|
111
|
+
}
|
|
112
|
+
if (!commits.length)
|
|
113
|
+
return;
|
|
114
|
+
if (!await this.ctx.sessions.flush(session))
|
|
115
|
+
throw new Error('没有会话持久化提供方;日记仍为 prepared,不对检索可见');
|
|
116
|
+
for (const item of commits)
|
|
117
|
+
this.store.commit(item.id, item.seq);
|
|
118
|
+
}
|
|
119
|
+
continuityContext() {
|
|
120
|
+
return this.store.recent(this.config.namespace, this.config.recentDiaryCount).reverse().map(record => renderDiary(record.diary)).join('\n\n---\n\n');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=retrieval-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retrieval-service.js","sourceRoot":"","sources":["../src/retrieval-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAG7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAGxC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAqBjD,MAAM,OAAO,gBAAiB,SAAQ,OAAO;IAClC,MAAM,CAAgB;IACtB,MAAM,CAAe;IACrB,KAAK,CAAa;IAClB,QAAQ,GAAG,IAAI,eAAe,EAAE,CAAA;IACxB,IAAI,GAAG,IAAI,GAAG,EAAoB,CAAA;IAClC,QAAQ,GAAG,IAAI,OAAO,EAA0B,CAAA;IAEjE,YAAY,GAAY,EAAE,MAAsB,EAAE,eAAgE,EAAE;QAClH,KAAK,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,KAAK,GAAG,YAAY,CAAC,KAAK,IAAI,IAAI,WAAW,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;QACvE,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,MAAM,IAAI,IAAI,aAAa,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;QAChG,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,yBAAyB,CAAC,CAAA;IACnE,CAAC;IAED,KAAK,CAAC,UAAU;QACd,IAAI,CAAC,GAAG,CAAC,kBAAkB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAA;QACrD,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,iBAAiB,EAAE,OAAO,CAAC,EAAE;YACvC,KAAK,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC,CAAA;QAC7G,CAAC,CAAC,CAAA;QACF,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE;YAAE,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;IACjG,CAAC;IAED,OAAO,CAAC,OAAgB;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,kBAAkB,CAAC,OAAO,CAAC,OAAO,EAAE,eAAe,CAAC,GAAG,CAAC,CAAA;QAC/E,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAA;QAClD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,KAAK,CAAI,SAA2B;QAClC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,CAAA;QACrC,iFAAiF;QACjF,IAAI,OAAmB,CAAA;QACvB,IAAI,CAAC;YAAC,OAAO,GAAG,SAAS,EAAE,CAAA;QAAC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAAC,CAAC;QAC/E,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QACtB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAA;QACnF,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,CAAA;QAC3C,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QACxC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;IACpB,CAAC;IAED,MAAM,CAAC,KAAa,EAAE,UAAyB,EAAE;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE;YAC3B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;YACzB,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,IAAI;gBAAE,MAAM,IAAI,SAAS,CAAC,4BAA4B,CAAC,CAAA;YAClF,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,WAAW,CAAA;YACtD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc;gBAAE,MAAM,IAAI,SAAS,CAAC,qBAAqB,CAAC,CAAA;YAC3H,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;YACjG,MAAM,CAAC,cAAc,EAAE,CAAA;YACvB,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAA;YACvC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC;gBAC3D,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC;oBAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;gBAC9F,OAAO,EAAE,CAAA;YACX,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAA;YAC3E,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAA;YACpK,IAAI,CAAC,UAAU,CAAC,MAAM;gBAAE,OAAO,EAAE,CAAA;YACjC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;YAC9I,MAAM,CAAC,cAAc,EAAE,CAAA;YACvB,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;gBACxB,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;gBACpC,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,CAAA;YACzK,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,4FAA4F;IAC5F,SAAS,CAAC,OAAgB;QACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAC3C,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAA;QAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAA;QACxF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACnC,OAAO,OAAO,CAAA;IAChB,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,OAAgB;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QAC9C,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,OAAM;QAC3B,MAAM,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAA;QACvC,MAAM,OAAO,GAAkC,EAAE,CAAA;QACjD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC9C,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,oBAAoB,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,KAAK,MAAM,CAAC,aAAa,IAAI,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,IAAI,KAAK,CAAC,SAAS,CAAC,EAAE,KAAK,SAAS;oBAAE,OAAO,KAAK,CAAA;gBACxO,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;gBACjC,OAAO,OAAO,EAAE,IAAI,KAAK,oBAAoB;uBACxC,OAAO,CAAC,IAAI,CAAC,YAAY,KAAK,MAAM,CAAC,aAAa;uBAClD,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,UAAU,CAAC;uBAC/D,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;YACnG,CAAC,CAAC,CAAA;YACF,IAAI,UAAU;gBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,UAAU,CAAC,GAAG,EAAE,CAAC,CAAA;iBAC/D,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,gBAAgB,IAAI,KAAK,CAAC,IAAI,CAAC,YAAY,KAAK,MAAM,CAAC,aAAa,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC;gBAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QACnL,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,OAAM;QAC3B,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;QAC/F,KAAK,MAAM,IAAI,IAAI,OAAO;YAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;IAClE,CAAC;IAED,iBAAiB;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IACtJ,CAAC;CACF"}
|
package/lib/tools.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
export declare const name = "diary-recall";
|
|
3
|
+
export declare const inject: string[];
|
|
4
|
+
/** Model-visible read-only recall, scoped by the companion preset and namespace. */
|
|
5
|
+
export declare function apply(ctx: Context): void;
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const name = 'diary-recall';
|
|
2
|
+
export const inject = ['tools', 'diaryMemory'];
|
|
3
|
+
/** Model-visible read-only recall, scoped by the companion preset and namespace. */
|
|
4
|
+
export function apply(ctx) {
|
|
5
|
+
ctx.tools.register({
|
|
6
|
+
name: 'diary_recall',
|
|
7
|
+
description: '想起我们之前的经历、称呼、偏好、情绪变化、约定或我自己的反思。输入自然语言,会检索历史日记并按相关性重排。日记中的主观感受和不确定解释不等于已确认事实。',
|
|
8
|
+
parameters: { type: 'object', properties: { query: { type: 'string', minLength: 1, maxLength: 8000 } }, required: ['query'], additionalProperties: false },
|
|
9
|
+
output: {
|
|
10
|
+
schema: { type: 'object', properties: { memories: { type: 'array', items: { type: 'object', additionalProperties: true } } }, required: ['memories'], additionalProperties: false },
|
|
11
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
|
|
12
|
+
},
|
|
13
|
+
async execute(args, execution) {
|
|
14
|
+
return { memories: await ctx.diaryMemory.recall(args.query, { signal: execution.signal }) };
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=tools.js.map
|
package/lib/tools.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,IAAI,GAAG,cAAc,CAAA;AAClC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,aAAa,CAAC,CAAA;AAE9C,oFAAoF;AACpF,MAAM,UAAU,KAAK,CAAC,GAAY;IAChC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC;QACjB,IAAI,EAAE,cAAc;QACpB,WAAW,EAAE,8EAA8E;QAC3F,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,oBAAoB,EAAE,KAAK,EAAE;QAC1J,MAAM,EAAE;YACN,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,UAAU,CAAC,EAAE,oBAAoB,EAAE,KAAK,EAAE;YACnL,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;SAC1E;QACD,KAAK,CAAC,OAAO,CAAC,IAAuB,EAAE,SAAS;YAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,CAAA;QAC7F,CAAC;KACF,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"meta":{"title":"Diary compaction","description":"Writes and indexes a diary before native DSH compaction, preserving tool call boundaries and recoverable commits."}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"meta":{"title":"日记压缩编排","description":"先写日记并持久化向量,再通过 DSH 原生压缩提交;保护工具调用边界。"}}
|
package/locale/en.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"meta":{"title":"Companion diary memory","description":"Bailian embedding and reranking for a personal companion's diary, continuity, reflection, and safe context compaction."}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"meta":{"title":"Recall diary","description":"Recall shared experiences, preferences, promises, and reflections through diary_recall."}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"meta":{"title":"回想日记","description":"通过 diary_recall 检索往日经历、偏好、约定和自我反思。"}}
|
package/locale/zh.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"meta":{"title":"陪伴日记记忆","description":"使用百炼向量与重排保存、回想共同经历;安全压缩会话,延续情感与自我反思。"}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-diary-memory",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "为 DSH 的陪伴与个人数字分身提供有温度、可演化的日记记忆、百炼检索和安全压缩。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"engines": { "node": ">=22.15", "dsh": "0.2.1-rc.3" },
|
|
10
|
+
"files": ["lib", "src", "locale", "icon.svg", "cordis.patch.yml", "README.md", "LICENSE"],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
|
|
13
|
+
"./compaction": "./lib/orchestrator.js",
|
|
14
|
+
"./compaction/locale/*.json": "./locale/compaction/*.json",
|
|
15
|
+
"./compaction/icon": "./icon.svg",
|
|
16
|
+
"./tools": "./lib/tools.js",
|
|
17
|
+
"./tools/locale/*.json": "./locale/tools/*.json",
|
|
18
|
+
"./tools/icon": "./icon.svg",
|
|
19
|
+
"./bailian-client": "./lib/bailian-client.js",
|
|
20
|
+
"./memory-store": "./lib/memory-store.js",
|
|
21
|
+
"./locale/*.json": "./locale/*.json",
|
|
22
|
+
"./icon": "./icon.svg",
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
28
|
+
"test": "tsc -p tsconfig.json && node --test test/*.test.mjs",
|
|
29
|
+
"prepack": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": { "better-sqlite3": "^12.4.1", "zod": "4.4.3" },
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@deepseek-ai/cordis": "~4.0.5",
|
|
34
|
+
"@deepseek-ai/schemastery": "~3.18.5",
|
|
35
|
+
"@deepseek-ai/dsh-agent": "0.2.1-rc.3",
|
|
36
|
+
"@deepseek-ai/dsh-compaction": "0.2.1-rc.3",
|
|
37
|
+
"@deepseek-ai/dsh-compaction-basic": "0.2.1-rc.3",
|
|
38
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.3",
|
|
39
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.3",
|
|
40
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.3",
|
|
41
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.3"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@deepseek-ai/cordis": { "optional": true },
|
|
45
|
+
"@deepseek-ai/schemastery": { "optional": true },
|
|
46
|
+
"@deepseek-ai/dsh-agent": { "optional": true },
|
|
47
|
+
"@deepseek-ai/dsh-compaction": { "optional": true },
|
|
48
|
+
"@deepseek-ai/dsh-compaction-basic": { "optional": true },
|
|
49
|
+
"@deepseek-ai/dsh-llm": { "optional": true },
|
|
50
|
+
"@deepseek-ai/dsh-session": { "optional": true },
|
|
51
|
+
"@deepseek-ai/dsh-session-projection": { "optional": true },
|
|
52
|
+
"@deepseek-ai/dsh-tools": { "optional": true }
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
56
|
+
"@deepseek-ai/schemastery": "3.18.5",
|
|
57
|
+
"@deepseek-ai/dsh-agent": "0.2.1-rc.3",
|
|
58
|
+
"@deepseek-ai/dsh-compaction": "0.2.1-rc.3",
|
|
59
|
+
"@deepseek-ai/dsh-compaction-basic": "0.2.1-rc.3",
|
|
60
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.3",
|
|
61
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.3",
|
|
62
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.3",
|
|
63
|
+
"@deepseek-ai/dsh-token-meter": "0.2.1-rc.3",
|
|
64
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.3",
|
|
65
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
66
|
+
"@types/node": "^24.0.0",
|
|
67
|
+
"typescript": "^5.9.3",
|
|
68
|
+
"yaml": "^2.8.1"
|
|
69
|
+
},
|
|
70
|
+
"dsh": {
|
|
71
|
+
"bundle": { "patch": "./cordis.patch.yml" }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
2
|
+
import type { ResolvedConfig } from './config.js'
|
|
3
|
+
|
|
4
|
+
interface BailianFailure {
|
|
5
|
+
status?: number
|
|
6
|
+
requestId?: string
|
|
7
|
+
retryable?: boolean
|
|
8
|
+
}
|
|
9
|
+
export interface RerankResult { index: number; relevance_score: number }
|
|
10
|
+
|
|
11
|
+
export class BailianError extends Error {
|
|
12
|
+
readonly status?: number
|
|
13
|
+
readonly requestId?: string
|
|
14
|
+
readonly retryable: boolean
|
|
15
|
+
constructor(message: string, { status, requestId, retryable = false }: BailianFailure = {}) {
|
|
16
|
+
super(message)
|
|
17
|
+
this.name = 'BailianError'
|
|
18
|
+
this.status = status
|
|
19
|
+
this.requestId = requestId
|
|
20
|
+
this.retryable = retryable
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function validateVector(vector: unknown, dimensions: number): number[] {
|
|
25
|
+
if (!Array.isArray(vector) || vector.length !== dimensions || !vector.every(n => typeof n === 'number' && Number.isFinite(n))) throw new BailianError('百炼返回的向量维度或数值无效')
|
|
26
|
+
if (!vector.some(n => n !== 0)) throw new BailianError('百炼返回了零向量')
|
|
27
|
+
return vector
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Native DashScope APIs; no OpenAI SDK or compatibility endpoint is involved. */
|
|
31
|
+
export class BailianClient {
|
|
32
|
+
readonly config: ResolvedConfig
|
|
33
|
+
readonly fetch: typeof globalThis.fetch
|
|
34
|
+
readonly signal?: AbortSignal
|
|
35
|
+
readonly embeddingKey: string
|
|
36
|
+
constructor(config: ResolvedConfig, { fetch: fetchImpl = globalThis.fetch, signal }: { fetch?: typeof globalThis.fetch; signal?: AbortSignal } = {}) {
|
|
37
|
+
this.config = config
|
|
38
|
+
this.fetch = fetchImpl
|
|
39
|
+
this.signal = signal
|
|
40
|
+
// Model names alone do not identify the vector space across endpoints/dimensions.
|
|
41
|
+
this.embeddingKey = JSON.stringify([config.baseUrl, config.embeddingModel, config.embeddingDimensions])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async request(path: string, body: unknown, callerSignal?: AbortSignal): Promise<unknown> {
|
|
45
|
+
const { config } = this
|
|
46
|
+
if (!config.apiKey) throw new BailianError('请先配置百炼 apiKey 或 DSH_DIARY_BAILIAN_API_KEY')
|
|
47
|
+
const signals = [this.signal, callerSignal].filter((item): item is AbortSignal => item !== undefined)
|
|
48
|
+
const parent = signals.length ? AbortSignal.any(signals) : undefined
|
|
49
|
+
for (let attempt = 0; ; attempt++) {
|
|
50
|
+
parent?.throwIfAborted()
|
|
51
|
+
const signal = AbortSignal.any([...(parent ? [parent] : []), AbortSignal.timeout(config.timeoutMs)])
|
|
52
|
+
let backoff = Math.min(500 * 2 ** attempt, 10000)
|
|
53
|
+
try {
|
|
54
|
+
const response = await this.fetch(new URL(path, config.baseUrl), {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { Authorization: `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' },
|
|
57
|
+
body: JSON.stringify(body),
|
|
58
|
+
signal,
|
|
59
|
+
redirect: 'error',
|
|
60
|
+
})
|
|
61
|
+
// Keep the timeout active while the body is being read, too.
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const retryAfter = response.headers.get('retry-after')
|
|
64
|
+
if (retryAfter) {
|
|
65
|
+
const ms = /^\d+(\.\d+)?$/.test(retryAfter) ? Number(retryAfter) * 1000 : Date.parse(retryAfter) - Date.now()
|
|
66
|
+
if (Number.isFinite(ms)) backoff = Math.min(30000, Math.max(backoff, ms))
|
|
67
|
+
}
|
|
68
|
+
await response.body?.cancel()
|
|
69
|
+
throw new BailianError(`百炼请求失败(HTTP ${response.status})`, {
|
|
70
|
+
status: response.status,
|
|
71
|
+
requestId: response.headers.get('x-request-id') ?? undefined,
|
|
72
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
const data: unknown = await response.json()
|
|
76
|
+
if (typeof data !== 'object' || data === null || !('output' in data) || !data.output || ('code' in data && data.code)) throw new BailianError('百炼返回了 API 错误或缺少 output')
|
|
77
|
+
parent?.throwIfAborted()
|
|
78
|
+
return data.output
|
|
79
|
+
} catch (error) {
|
|
80
|
+
parent?.throwIfAborted()
|
|
81
|
+
const failure = error instanceof BailianError ? error : new BailianError(
|
|
82
|
+
signal.aborted ? '百炼请求超时' : '百炼网络请求失败或响应不是有效 JSON',
|
|
83
|
+
{ retryable: error instanceof TypeError || signal.aborted },
|
|
84
|
+
)
|
|
85
|
+
if (!failure.retryable || attempt >= config.retries) throw failure
|
|
86
|
+
}
|
|
87
|
+
await delay(backoff, undefined, parent ? { signal: parent } : {})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async embed(text: string, { textType = 'document', signal }: { textType?: 'document' | 'query'; signal?: AbortSignal } = {}): Promise<number[]> {
|
|
92
|
+
if (typeof text !== 'string' || !text.trim()) throw new TypeError('embedding 文本不能为空')
|
|
93
|
+
if (!['document', 'query'].includes(textType)) throw new TypeError('无效的 embedding textType')
|
|
94
|
+
const { embeddingModel, embeddingDimensions } = this.config
|
|
95
|
+
const output = await this.request('/api/v1/services/embeddings/text-embedding/text-embedding', {
|
|
96
|
+
model: embeddingModel,
|
|
97
|
+
input: { texts: [text] },
|
|
98
|
+
parameters: { text_type: textType, dimension: embeddingDimensions },
|
|
99
|
+
}, signal) as { embeddings?: { text_index?: unknown; embedding?: unknown }[] }
|
|
100
|
+
if (output.embeddings?.length !== 1 || output.embeddings[0].text_index !== 0) throw new BailianError('百炼 embedding 的 text_index 无效')
|
|
101
|
+
return validateVector(output.embeddings[0].embedding, embeddingDimensions)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async rerank(query: string, documents: string[], { topN = documents.length, signal }: { topN?: number; signal?: AbortSignal } = {}): Promise<RerankResult[]> {
|
|
105
|
+
if (!documents.length) return []
|
|
106
|
+
if (typeof query !== 'string' || !query.trim() || documents.some(text => typeof text !== 'string' || !text.trim())) throw new TypeError('rerank 的查询和文档不能为空')
|
|
107
|
+
if (!Number.isInteger(topN) || topN < 1 || topN > documents.length) throw new TypeError('无效的 rerank topN')
|
|
108
|
+
const output = await this.request('/api/v1/services/rerank/text-rerank/text-rerank', {
|
|
109
|
+
model: this.config.rerankModel,
|
|
110
|
+
input: { query, documents },
|
|
111
|
+
parameters: { top_n: topN, return_documents: false },
|
|
112
|
+
}, signal) as { results?: RerankResult[] }
|
|
113
|
+
if (!Array.isArray(output.results) || output.results.length !== topN) throw new BailianError('百炼 rerank 结果数量无效')
|
|
114
|
+
const seen = new Set()
|
|
115
|
+
for (const result of output.results) {
|
|
116
|
+
if (!Number.isInteger(result.index) || result.index < 0 || result.index >= documents.length || seen.has(result.index) || !Number.isFinite(result.relevance_score) || result.relevance_score < 0 || result.relevance_score > 1) throw new BailianError('百炼 rerank 返回了无效或重复的文档索引/分数')
|
|
117
|
+
seen.add(result.index)
|
|
118
|
+
}
|
|
119
|
+
return [...output.results].sort((a, b) => b.relevance_score - a.relevance_score || a.index - b.index)
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export interface DiaryConfig {
|
|
6
|
+
apiKey: string
|
|
7
|
+
baseUrl: string
|
|
8
|
+
embeddingModel: string
|
|
9
|
+
embeddingDimensions: number
|
|
10
|
+
rerankModel: string
|
|
11
|
+
compressionRounds: number
|
|
12
|
+
retainRounds: number
|
|
13
|
+
namespace: string
|
|
14
|
+
databasePath: string
|
|
15
|
+
candidateCount: number
|
|
16
|
+
recallCount: number
|
|
17
|
+
timeoutMs: number
|
|
18
|
+
retries: number
|
|
19
|
+
diaryProvider: string
|
|
20
|
+
diaryModel: string
|
|
21
|
+
diaryMaxTokens: number
|
|
22
|
+
diaryInputMaxChars: number
|
|
23
|
+
recentDiaryCount: number
|
|
24
|
+
autoRecall: boolean
|
|
25
|
+
}
|
|
26
|
+
export type ResolvedConfig = Readonly<DiaryConfig>
|
|
27
|
+
|
|
28
|
+
export const defaults = Object.freeze({
|
|
29
|
+
apiKey: '',
|
|
30
|
+
baseUrl: 'https://dashscope.aliyuncs.com',
|
|
31
|
+
embeddingModel: 'text-embedding-v4',
|
|
32
|
+
embeddingDimensions: 1024,
|
|
33
|
+
rerankModel: 'gte-rerank-v2',
|
|
34
|
+
compressionRounds: 20,
|
|
35
|
+
retainRounds: 2,
|
|
36
|
+
namespace: 'companion',
|
|
37
|
+
databasePath: '',
|
|
38
|
+
candidateCount: 30,
|
|
39
|
+
recallCount: 6,
|
|
40
|
+
timeoutMs: 30000,
|
|
41
|
+
retries: 2,
|
|
42
|
+
diaryProvider: '',
|
|
43
|
+
diaryModel: '',
|
|
44
|
+
diaryMaxTokens: 3000,
|
|
45
|
+
diaryInputMaxChars: 200000,
|
|
46
|
+
recentDiaryCount: 3,
|
|
47
|
+
autoRecall: true,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// DSH reads this schema for its native plugin configuration form.
|
|
51
|
+
export const Config: z<DiaryConfig> = z.object({
|
|
52
|
+
apiKey: z.string().role('secret').default('').description('百炼 API Key;留空读取 DSH_DIARY_BAILIAN_API_KEY 或 DASHSCOPE_API_KEY。'),
|
|
53
|
+
baseUrl: z.string().default(defaults.baseUrl).description('百炼地域端点;须与 API Key 的地域一致。'),
|
|
54
|
+
embeddingModel: z.string().default(defaults.embeddingModel),
|
|
55
|
+
embeddingDimensions: z.number().step(1).min(1).max(4096).default(defaults.embeddingDimensions),
|
|
56
|
+
rerankModel: z.string().default(defaults.rerankModel),
|
|
57
|
+
compressionRounds: z.number().step(1).min(2).default(defaults.compressionRounds).description('累计完成多少个会话轮次后,在下一次安全的 pre-step 压缩。工具调用不额外计轮次。'),
|
|
58
|
+
retainRounds: z.number().step(1).min(1).default(defaults.retainRounds).description('至少保留最近多少个用户输入及其后续消息。'),
|
|
59
|
+
namespace: z.string().default(defaults.namespace).description('数字分身的记忆空间。只有使用同一空间的会话共享日记与自我演化。'),
|
|
60
|
+
databasePath: z.string().default('').description('SQLite 文件;默认位于当前 DSH profile 的 diary-memory/ 中。'),
|
|
61
|
+
candidateCount: z.number().step(1).min(1).max(100).default(defaults.candidateCount),
|
|
62
|
+
recallCount: z.number().step(1).min(1).max(100).default(defaults.recallCount),
|
|
63
|
+
timeoutMs: z.number().step(1).min(100).max(300000).default(defaults.timeoutMs),
|
|
64
|
+
retries: z.number().step(1).min(0).max(5).default(defaults.retries),
|
|
65
|
+
diaryProvider: z.string().default('').description('日记生成使用 DSH 的 LLM 路由;两个字段均留空则继承会话模型。'),
|
|
66
|
+
diaryModel: z.string().default(''),
|
|
67
|
+
diaryMaxTokens: z.number().step(1).min(256).max(16000).default(defaults.diaryMaxTokens),
|
|
68
|
+
diaryInputMaxChars: z.number().step(1).min(1000).default(defaults.diaryInputMaxChars),
|
|
69
|
+
recentDiaryCount: z.number().step(1).min(0).max(10).default(defaults.recentDiaryCount),
|
|
70
|
+
autoRecall: z.boolean().default(true).description('每个新用户输入自动检索日记,并通过 agent.inject 写入可重放的上下文。'),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export function resolveConfig(input: Partial<DiaryConfig> = {}, env: NodeJS.ProcessEnv = process.env): ResolvedConfig {
|
|
74
|
+
for (const key of Object.keys(input)) {
|
|
75
|
+
if (!(key in defaults)) throw new Error(`未知日记配置项:${key}`)
|
|
76
|
+
}
|
|
77
|
+
const config = Config({ ...defaults, ...input })
|
|
78
|
+
const ranges: [keyof Pick<DiaryConfig, 'embeddingDimensions' | 'compressionRounds' | 'retainRounds' | 'candidateCount' | 'recallCount' | 'timeoutMs' | 'retries' | 'diaryMaxTokens' | 'diaryInputMaxChars' | 'recentDiaryCount'>, number, number][] = [
|
|
79
|
+
['embeddingDimensions', 1, 4096], ['compressionRounds', 2, 1000000],
|
|
80
|
+
['retainRounds', 1, 1000000], ['candidateCount', 1, 100], ['recallCount', 1, 100],
|
|
81
|
+
['timeoutMs', 100, 300000], ['retries', 0, 5], ['diaryMaxTokens', 256, 16000],
|
|
82
|
+
['diaryInputMaxChars', 1000, 10000000], ['recentDiaryCount', 0, 10],
|
|
83
|
+
]
|
|
84
|
+
for (const [key, min, max] of ranges) {
|
|
85
|
+
if (!Number.isSafeInteger(config[key]) || config[key] < min || config[key] > max) {
|
|
86
|
+
throw new Error(`${key} 必须为 ${min}–${max} 的整数`)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (config.compressionRounds <= config.retainRounds) throw new Error('compressionRounds 必须大于 retainRounds')
|
|
90
|
+
if (config.recallCount > config.candidateCount) throw new Error('recallCount 不能大于 candidateCount')
|
|
91
|
+
config.diaryProvider = config.diaryProvider.trim()
|
|
92
|
+
config.diaryModel = config.diaryModel.trim()
|
|
93
|
+
if (Boolean(config.diaryProvider) !== Boolean(config.diaryModel)) throw new Error('diaryProvider 和 diaryModel 必须同时设置')
|
|
94
|
+
for (const key of ['namespace', 'embeddingModel', 'rerankModel'] as const) {
|
|
95
|
+
config[key] = config[key].trim()
|
|
96
|
+
if (!config[key] || config[key].length > 200) throw new Error(`${key} 不能为空且不能超过 200 字符`)
|
|
97
|
+
}
|
|
98
|
+
const endpoint = new URL(config.baseUrl)
|
|
99
|
+
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== '/') throw new Error('baseUrl 必须是无路径、凭据和查询参数的百炼 origin')
|
|
100
|
+
if (endpoint.protocol !== 'https:' && !(endpoint.protocol === 'http:' && ['127.0.0.1', '[::1]', 'localhost'].includes(endpoint.hostname))) throw new Error('baseUrl 需要 HTTPS;本地测试可以使用 loopback HTTP')
|
|
101
|
+
config.baseUrl = endpoint.origin
|
|
102
|
+
config.apiKey = config.apiKey.trim() || env.DSH_DIARY_BAILIAN_API_KEY?.trim() || env.DASHSCOPE_API_KEY?.trim() || ''
|
|
103
|
+
const profileDir = env.DSH_PROFILE_DIR?.trim() || join(env.DSH_HOME?.trim() || join(homedir(), '.dsh'), 'profiles', env.DSH_PROFILE?.trim() || 'default')
|
|
104
|
+
config.databasePath = config.databasePath || join(profileDir, 'diary-memory', 'memories.sqlite3')
|
|
105
|
+
if (config.databasePath !== ':memory:') config.databasePath = resolve(config.databasePath)
|
|
106
|
+
return Object.freeze(config)
|
|
107
|
+
}
|