dsh-plugin-lrc 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.
@@ -0,0 +1,210 @@
1
+ /**
2
+ * LRC HTTP 客户端(纯 TypeScript,不依赖 DSH)。
3
+ *
4
+ * 为什么单独成文件:DSH 插件 API 处于 Developer Preview,签名会变;
5
+ * 而 LRC 的 HTTP 接口是稳定的。插件 API 变了只需改 index.ts。
6
+ */
7
+
8
+ export interface DistilledItem {
9
+ content: string;
10
+ memory_type: string;
11
+ tags: string[];
12
+ importance: number;
13
+ source_turns: [number, number];
14
+ }
15
+
16
+ export interface DistillResult {
17
+ items: DistilledItem[];
18
+ count: number;
19
+ index_line: string;
20
+ }
21
+
22
+ export interface MigrateResult {
23
+ written: number;
24
+ merged: number;
25
+ failed: number;
26
+ remaining?: number;
27
+ truncated?: boolean;
28
+ }
29
+
30
+ export interface IndexResult {
31
+ index_line: string;
32
+ count: number;
33
+ total: number;
34
+ items: Array<{
35
+ id: string;
36
+ content_preview: string;
37
+ memory_type: string;
38
+ created_at_ms: number;
39
+ }>;
40
+ }
41
+
42
+ /** 一条召回结果(`/v1/memories/enrich` 的子集)。 */
43
+ export interface RecallItem {
44
+ id: string;
45
+ content: string;
46
+ memory_type: string;
47
+ /** 展示用相关度(0–1,相对 top1 归一化) */
48
+ score_norm: number;
49
+ importance: number;
50
+ }
51
+
52
+ export interface RecallResult {
53
+ memories: RecallItem[];
54
+ /** 候选总数(折叠前) */
55
+ total: number;
56
+ }
57
+
58
+ export interface LrcClientOptions {
59
+ baseUrl?: string;
60
+ token?: string;
61
+ timeoutMs?: number;
62
+ }
63
+
64
+ export class LrcClient {
65
+ readonly baseUrl: string;
66
+ private readonly token?: string;
67
+ private readonly timeoutMs: number;
68
+
69
+ constructor(options: LrcClientOptions = {}) {
70
+ const raw = options.baseUrl ?? process.env.LRC_BASE ?? 'http://127.0.0.1:3099';
71
+ this.baseUrl = raw.replace(/\/+$/, '');
72
+ this.token = options.token ?? process.env.LRC_TOKEN;
73
+ this.timeoutMs = options.timeoutMs ?? 20_000;
74
+ }
75
+
76
+ private headers(): Record<string, string> {
77
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
78
+ if (this.token) headers.Authorization = `Bearer ${this.token}`;
79
+ return headers;
80
+ }
81
+
82
+ /**
83
+ * 底层请求封装。
84
+ *
85
+ * 非 2xx 一律抛错并带上状态码与响应体前 300 字符 —— 调用方必须能区分
86
+ * "服务没起"(fetch 抛错)与"服务起了但拒绝"(带状态码的 Error)。
87
+ */
88
+ async request<T>(path: string, init: RequestInit): Promise<T> {
89
+ const response = await fetch(`${this.baseUrl}${path}`, {
90
+ ...init,
91
+ headers: this.headers(),
92
+ signal: AbortSignal.timeout(this.timeoutMs),
93
+ });
94
+ const text = await response.text();
95
+ if (!response.ok) {
96
+ throw new Error(`LRC ${path} 返回 ${response.status}: ${text.slice(0, 300)}`);
97
+ }
98
+ return JSON.parse(text) as T;
99
+ }
100
+
101
+ /** 健康检查:服务是否可用。 */
102
+ async health(): Promise<boolean> {
103
+ try {
104
+ const response = await fetch(`${this.baseUrl}/health`, {
105
+ signal: AbortSignal.timeout(3000),
106
+ });
107
+ return response.ok;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ /** 提炼对话文本(不写库)。 */
114
+ async distill(text: string, project?: string, maxItems = 40): Promise<DistillResult> {
115
+ return this.request<DistillResult>('/v1/context/distill', {
116
+ method: 'POST',
117
+ body: JSON.stringify({ text, project, max_items: maxItems }),
118
+ });
119
+ }
120
+
121
+ /** 批量写入(幂等:同内容走相似合并)。 */
122
+ async migrate(items: DistilledItem[], project?: string): Promise<MigrateResult> {
123
+ return this.request<MigrateResult>('/v1/context/migrate', {
124
+ method: 'POST',
125
+ body: JSON.stringify({ items, project }),
126
+ });
127
+ }
128
+
129
+ /** 取归档索引。 */
130
+ async index(project?: string, limit = 20): Promise<IndexResult> {
131
+ const query = new URLSearchParams();
132
+ if (project) query.set('project', project);
133
+ query.set('limit', String(limit));
134
+ return this.request<IndexResult>(`/v1/context/index?${query.toString()}`, {
135
+ method: 'GET',
136
+ });
137
+ }
138
+
139
+ /** 记忆总数(用于验收"不膨胀")。 */
140
+ async totalMemories(): Promise<number> {
141
+ const stats = await this.request<{ total_memories?: number }>('/v1/memories/stats', {
142
+ method: 'GET',
143
+ });
144
+ return stats.total_memories ?? 0;
145
+ }
146
+
147
+ /**
148
+ * 召回相关记忆(三路融合 + 近重复折叠)。
149
+ *
150
+ * 这是插件唯一的**读**路径:DSH 没有 MCP 客户端,AI 看不到 LRC 的
151
+ * `recall` 工具,只能通过插件注册的原生工具读回记忆。
152
+ */
153
+ async recall(
154
+ query: string,
155
+ options: { topK?: number; project?: string } = {},
156
+ ): Promise<RecallResult> {
157
+ const body: Record<string, unknown> = {
158
+ query,
159
+ top_k: options.topK ?? 5,
160
+ };
161
+ if (options.project !== undefined && options.project.length > 0) {
162
+ body.project = options.project;
163
+ }
164
+ const data = await this.request<{ memories?: RecallItem[]; total?: number }>(
165
+ '/v1/memories/enrich',
166
+ { method: 'POST', body: JSON.stringify(body) },
167
+ );
168
+ return { memories: data.memories ?? [], total: data.total ?? 0 };
169
+ }
170
+
171
+ /**
172
+ * 删除一条记忆(幂等)。
173
+ *
174
+ * 用于**纠错**:AI 发现某条记忆是错的(例如把「没找到」误写成「不存在」),
175
+ * 必须能撤回,否则错误结论会被持续召回、污染后续所有对话。
176
+ */
177
+ async forget(memoryId: string): Promise<{ deleted: boolean; alreadyDeleted: boolean }> {
178
+ const data = await this.request<{
179
+ success?: boolean;
180
+ deleted?: boolean;
181
+ already_deleted?: boolean;
182
+ }>('/v1/memories/forget', {
183
+ method: 'POST',
184
+ body: JSON.stringify({ memory_id: memoryId }),
185
+ });
186
+ return {
187
+ deleted: data.deleted === true,
188
+ alreadyDeleted: data.already_deleted === true,
189
+ };
190
+ }
191
+
192
+ /** 一步完成"提炼 + 迁移"。 */
193
+ async migrateText(
194
+ text: string,
195
+ project?: string,
196
+ maxItems = 40,
197
+ ): Promise<{ distilled: number; written: number; merged: number; indexLine: string }> {
198
+ const distilled = await this.distill(text, project, maxItems);
199
+ if (distilled.items.length === 0) {
200
+ return { distilled: 0, written: 0, merged: 0, indexLine: distilled.index_line };
201
+ }
202
+ const migrated = await this.migrate(distilled.items, project);
203
+ return {
204
+ distilled: distilled.count,
205
+ written: migrated.written,
206
+ merged: migrated.merged,
207
+ indexLine: distilled.index_line,
208
+ };
209
+ }
210
+ }