@dsh-libing/memory-manager 0.0.0-stage → 0.1.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/CHANGELOG.md +12 -0
- package/README.md +68 -2
- package/cordis.patch.yml +3 -0
- package/icon.svg +1 -0
- package/lib/client-types/client/Dialogs.d.ts +29 -0
- package/lib/client-types/client/Editor.d.ts +12 -0
- package/lib/client-types/client/MemoryPage.d.ts +31 -0
- package/lib/client-types/client/api.d.ts +9 -0
- package/lib/client-types/client/index.d.ts +9 -0
- package/lib/client-types/client/locales.d.ts +207 -0
- package/lib/client-types/client/navigation.d.ts +6 -0
- package/lib/client-types/client/turnView.d.ts +7 -0
- package/lib/client-types/client/useMemory.d.ts +123 -0
- package/lib/client-types/shared.d.ts +508 -0
- package/lib/client.js +221 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +134 -0
- package/lib/models.d.ts +21 -0
- package/lib/models.js +71 -0
- package/lib/recall.d.ts +55 -0
- package/lib/recall.js +47 -0
- package/lib/runtime.d.ts +351 -0
- package/lib/runtime.js +207 -0
- package/lib/scope.d.ts +48 -0
- package/lib/scope.js +100 -0
- package/lib/shared.d.ts +508 -0
- package/lib/shared.js +40 -0
- package/lib/storage-worker.d.ts +1 -0
- package/lib/storage-worker.js +276 -0
- package/lib/store.d.ts +15 -0
- package/lib/store.js +114 -0
- package/package.json +123 -3
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import type { activityItemSchema } from './shared.ts';
|
|
5
|
+
declare module '@deepseek-ai/dsh-llm' {
|
|
6
|
+
interface MessageSourceMap {
|
|
7
|
+
'libing-memory': {
|
|
8
|
+
kind: 'libing-memory';
|
|
9
|
+
form: 'recall';
|
|
10
|
+
turn: number;
|
|
11
|
+
receipt: string;
|
|
12
|
+
items: z.infer<typeof activityItemSchema>[];
|
|
13
|
+
reason: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export declare const name = "libing-memory-manager";
|
|
18
|
+
export declare const inject: string[];
|
|
19
|
+
export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
20
|
+
dataRoot: Schema<string, string, "plain">;
|
|
21
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
22
|
+
dataRoot: Schema<string, string, "plain">;
|
|
23
|
+
}>>, "plain">;
|
|
24
|
+
export declare function apply(ctx: Context, config?: {
|
|
25
|
+
dataRoot?: string;
|
|
26
|
+
}): Promise<void>;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
2
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
3
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
4
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
5
|
+
import { join, isAbsolute } from 'node:path';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { Runtime } from "./runtime.js";
|
|
8
|
+
import { agentScope } from "./scope.js";
|
|
9
|
+
import { modelDirectory } from "./models.js";
|
|
10
|
+
import { API, MemoryError, draftSchema, mutationSchema, selectionSchema } from "./shared.js";
|
|
11
|
+
export const name = 'libing-memory-manager';
|
|
12
|
+
export const inject = ['connection', 'tools', 'systemPrompt', 'llm', 'sessionQuery', 'workspaceRegistry', 'profileContext'];
|
|
13
|
+
export const Config = Schema.object({ dataRoot: Schema.string().description('Absolute data root; default is current profile/libing/memory-manager') });
|
|
14
|
+
const lifetimes = new Map();
|
|
15
|
+
export async function apply(ctx, config = {}) {
|
|
16
|
+
const root = config.dataRoot ?? join(ctx.profileContext.dir, 'libing/memory-manager');
|
|
17
|
+
if (!isAbsolute(root))
|
|
18
|
+
throw new MemoryError('absoluteRootRequired');
|
|
19
|
+
const owner = { disposed: false };
|
|
20
|
+
ctx.effect(() => () => { owner.disposed = true; const value = owner.lifetime; if (!value)
|
|
21
|
+
return; value.closing ??= value.runtime.close().finally(() => { if (lifetimes.get(root) === value)
|
|
22
|
+
lifetimes.delete(root); }); return value.closing; }, 'libing-memory: tasks and database');
|
|
23
|
+
const old = lifetimes.get(root);
|
|
24
|
+
if (old?.closing)
|
|
25
|
+
await old.closing;
|
|
26
|
+
if (owner.disposed)
|
|
27
|
+
return;
|
|
28
|
+
const runtime = new Runtime(ctx, root);
|
|
29
|
+
const value = { runtime };
|
|
30
|
+
owner.lifetime = value;
|
|
31
|
+
if (!lifetimes.has(root))
|
|
32
|
+
lifetimes.set(root, value);
|
|
33
|
+
await runtime.store.ready;
|
|
34
|
+
if (owner.disposed)
|
|
35
|
+
return;
|
|
36
|
+
class MemoryAccess extends Service {
|
|
37
|
+
version = 1;
|
|
38
|
+
constructor() { super(ctx, 'libingMemoryAccess'); }
|
|
39
|
+
}
|
|
40
|
+
new MemoryAccess();
|
|
41
|
+
const guard = async (request) => {
|
|
42
|
+
if (request.headers.get('content-type')?.split(';')[0]?.trim() !== 'application/json')
|
|
43
|
+
throw new MemoryError('invalidRequest', 415);
|
|
44
|
+
const body = await request.text();
|
|
45
|
+
if (Buffer.byteLength(body) > 20000)
|
|
46
|
+
throw new MemoryError('requestTooLarge', 413);
|
|
47
|
+
return JSON.parse(body);
|
|
48
|
+
};
|
|
49
|
+
const route = (path, methods, action) => ctx.effect(() => ctx.connection.fetch.register({ path: `${API}/${path}`, methods, requestBody: 'buffered', fetch: async (request) => {
|
|
50
|
+
try {
|
|
51
|
+
return Response.json(await runtime.run(signal => action(request, signal), request.signal), { headers: { 'cache-control': 'no-store' } });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return Response.json({ error: (() => {
|
|
55
|
+
if (error instanceof MemoryError) {
|
|
56
|
+
return error.code;
|
|
57
|
+
}
|
|
58
|
+
if (error instanceof z.ZodError || error instanceof SyntaxError) {
|
|
59
|
+
return 'invalidRequest';
|
|
60
|
+
}
|
|
61
|
+
return 'operationFailed';
|
|
62
|
+
})() }, { status: (() => {
|
|
63
|
+
if (error instanceof MemoryError) {
|
|
64
|
+
return error.status;
|
|
65
|
+
}
|
|
66
|
+
if (error instanceof z.ZodError || error instanceof SyntaxError) {
|
|
67
|
+
return 400;
|
|
68
|
+
}
|
|
69
|
+
return 503;
|
|
70
|
+
})() });
|
|
71
|
+
}
|
|
72
|
+
} }), `libing-memory: ${path}`);
|
|
73
|
+
route('state', ['GET'], async (request, signal) => { const pairs = [...new URL(request.url).searchParams]; if (new Set(pairs.map(([key]) => key)).size !== pairs.length)
|
|
74
|
+
throw new MemoryError('invalidRequest', 400); return await runtime.state(selectionSchema.parse(Object.fromEntries(pairs)), signal); });
|
|
75
|
+
route('models', ['GET'], async (request, signal) => { if (new URL(request.url).search)
|
|
76
|
+
throw new MemoryError('invalidRequest', 400); return await modelDirectory(ctx, signal); });
|
|
77
|
+
route('mutate', ['POST'], async (request, signal) => { const body = z.object({ selection: selectionSchema, mutation: mutationSchema }).strict().parse(await guard(request)); return await runtime.mutate(body.selection, body.mutation, signal); });
|
|
78
|
+
route('read', ['POST'], async (request, signal) => { const body = z.object({ selection: selectionSchema, id: z.string(), version: z.number().int().optional() }).strict().parse(await guard(request)); const state = await runtime.state(body.selection, signal); return await runtime.store.call('read', { scope: state.scope, id: body.id, history: true, ...body.version ? { version: body.version } : {} }, z.unknown()); });
|
|
79
|
+
route('history', ['POST'], async (request, signal) => { const body = z.object({ selection: selectionSchema, id: z.string() }).strict().parse(await guard(request)); const state = await runtime.state(body.selection, signal); return await runtime.store.call('history', { scope: state.scope, id: body.id }, z.unknown()); });
|
|
80
|
+
ctx.effect(() => ctx.systemPrompt.section({ name: 'libing-memory:usage', order: 600, interpolate: false, text: 'libing_memory_search/read retrieve confirmed historical references in this conversation scope. Current explicit instructions take priority. libing_memory_propose submits a candidate, including updates; human review is required before it becomes readable. Do not claim that a pending candidate is saved as active memory.' }), 'libing-memory: guidance');
|
|
81
|
+
const liveAgents = new Map();
|
|
82
|
+
ctx.on('agent/created', ({ agent }) => { liveAgents.set(agent.session.id, agent); });
|
|
83
|
+
ctx.on('agent/disposed', ({ agent }) => { if (liveAgents.get(agent.session.id) === agent)
|
|
84
|
+
liveAgents.delete(agent.session.id); });
|
|
85
|
+
ctx.on('agent/pre-step', async (payload, next) => {
|
|
86
|
+
liveAgents.set(payload.agent.session.id, payload.agent);
|
|
87
|
+
const decision = await next();
|
|
88
|
+
if (decision.kind === 'reject' || payload.signal.aborted || payload.step !== 1 || !decision.messages.length)
|
|
89
|
+
return decision;
|
|
90
|
+
const query = decision.messages.filter(message => message.source.kind === 'user').flatMap(message => message.content.filter(block => block.type === 'text').map(block => block.text)).join('\n').slice(0, 4000);
|
|
91
|
+
if (!query)
|
|
92
|
+
return decision;
|
|
93
|
+
try {
|
|
94
|
+
return await runtime.run(async (signal) => {
|
|
95
|
+
const snapshot = await runtime.prepare(payload.agent, query, payload.turn, signal);
|
|
96
|
+
if (!snapshot.text)
|
|
97
|
+
return decision;
|
|
98
|
+
const items = snapshot.selected.map(entry => ({ id: entry.id, version: entry.version, title: entry.title, summary: entry.content.slice(0, 500), status: 'included' }));
|
|
99
|
+
const message = createUserMessage({ content: [{ type: 'text', text: `Memory snapshot for turn ${payload.turn}.\n${snapshot.text}` }], source: { kind: 'libing-memory', form: 'recall', turn: payload.turn, receipt: snapshot.id, items, reason: snapshot.reason } });
|
|
100
|
+
signal.throwIfAborted();
|
|
101
|
+
return { ...decision, messages: [...decision.messages, message] };
|
|
102
|
+
}, payload.signal);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return decision;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
ctx.on('session/event', async (session, event) => {
|
|
109
|
+
if (event.type !== 'user/message' || event.data.source.kind !== 'libing-memory')
|
|
110
|
+
return;
|
|
111
|
+
const source = event.data.source;
|
|
112
|
+
await runtime.run(async () => { const agent = liveAgents.get(session.id); if (!agent)
|
|
113
|
+
return; const scope = await agentScope(agent, ctx); await runtime.activity(scope, source.turn, 'included', source.items, source.reason, source.receipt); }).catch(() => { });
|
|
114
|
+
});
|
|
115
|
+
// turn/end is the persisted successful close; turn-stopping occurs before that close.
|
|
116
|
+
ctx.on('session/event', async (session, event) => {
|
|
117
|
+
if (event.type !== 'turn/end')
|
|
118
|
+
return;
|
|
119
|
+
await runtime.run(async (signal) => {
|
|
120
|
+
const state = await runtime.state({ sessionId: session.id }, signal);
|
|
121
|
+
await runtime.store.call('settle', { scope: state.scope, turn: event.data.turn, reason: event.data.reason.kind }, z.null());
|
|
122
|
+
const agent = liveAgents.get(session.id);
|
|
123
|
+
if (agent && event.data.reason.kind === 'completed')
|
|
124
|
+
await runtime.automatic(agent, event.data.turn, signal);
|
|
125
|
+
}).catch(() => { });
|
|
126
|
+
});
|
|
127
|
+
const str = (description) => ({ type: 'string', description });
|
|
128
|
+
ctx.tools.register(defineTool({ name: 'libing_memory_search', description: '搜索本会话范围内已确认且允许召回的记忆;未命中如实返回。候选不参与搜索。', parameters: { query: { ...str('问题、标题或关键词'), required: true } }, output: { schema: { type: 'json' }, render: (_args, data) => [{ type: 'text', text: JSON.stringify(data) }] }, presentCall: args => ({ card: 'generic', title: '搜索记忆', kind: 'other', rawInput: args }), execute: async (args, exec) => { const agent = exec.agent; if (!agent)
|
|
129
|
+
throw new MemoryError('unknownScope', 403); const query = z.string().min(1).max(2000).parse(args.query); return z.json().parse(await runtime.run(signal => runtime.query(agent, query, signal), exec.signal)); } }));
|
|
130
|
+
ctx.tools.register(defineTool({ name: 'libing_memory_read', description: '读取搜索结果中已确认的记忆及固定版本;再次检查会话范围。', parameters: { id: { ...str('搜索返回的记忆身份'), required: true }, version: { type: 'integer' } }, output: { schema: { type: 'json' }, render: (_args, data) => [{ type: 'text', text: JSON.stringify(data) }] }, presentCall: args => ({ card: 'generic', title: '读取记忆', kind: 'other', rawInput: args }), execute: async (args, exec) => { const agent = exec.agent; if (!agent)
|
|
131
|
+
throw new MemoryError('unknownScope', 403); return z.json().parse(await runtime.run(signal => runtime.read(agent, args.id, args.version, signal), exec.signal)); } }));
|
|
132
|
+
ctx.tools.register(defineTool({ name: 'libing_memory_propose', description: '提交新记忆或已有记忆的候选修订,必须等用户确认。不要提交秘密、猜测或一次性内容。不能批准、直接覆盖或永久删除。', parameters: { title: { ...str('简短标题'), required: true }, content: { ...str('可复用的结论和适用条件'), required: true }, kind: { type: 'string', enum: ['agreement', 'decision', 'lesson', 'handoff'], required: true }, range: { type: 'string', enum: ['workspace', 'session'] }, target: str('修订时使用已读取记忆的身份'), expected: { type: 'integer', description: '修订所基于的版本' } }, output: { schema: { type: 'json' }, render: (_args, data) => [{ type: 'text', text: JSON.stringify(data) }] }, presentCall: args => ({ card: 'generic', title: '提交记忆候选', kind: 'other', rawInput: args }), execute: async (args, exec) => { const agent = exec.agent; if (!agent)
|
|
133
|
+
throw new MemoryError('unknownScope', 403); const draft = draftSchema.parse({ title: args.title, content: args.content, kind: args.kind, range: args.range ?? 'workspace' }); return z.json().parse(await runtime.run(signal => runtime.propose(agent, draft, args.target, args.expected, signal, String(exec.callId)), exec.signal)); } }));
|
|
134
|
+
}
|
package/lib/models.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Resolve only the selected session's route, with an explicit profile fallback. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
4
|
+
import type { CaptureModel, Settings } from './shared.ts';
|
|
5
|
+
export declare function latestSessionModel(events: readonly SessionEvent[]): {
|
|
6
|
+
provider: string;
|
|
7
|
+
model: string;
|
|
8
|
+
} | null;
|
|
9
|
+
export declare function captureModel(ctx: Context, settings: Settings, session: string | null, signal?: AbortSignal): Promise<CaptureModel | null>;
|
|
10
|
+
/** Directory lookups are explicit UI actions, bounded even for a slow adapter. */
|
|
11
|
+
export declare function modelDirectory(ctx: Context, signal: AbortSignal): Promise<{
|
|
12
|
+
providers: {
|
|
13
|
+
models: {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
}[];
|
|
17
|
+
unavailable: boolean;
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
}[];
|
|
21
|
+
}>;
|
package/lib/models.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
2
|
+
import { abort } from "./shared.js";
|
|
3
|
+
export function latestSessionModel(events) {
|
|
4
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
5
|
+
const event = events[index];
|
|
6
|
+
if (event?.type === 'request/context')
|
|
7
|
+
return { provider: event.data.provider, model: event.data.model };
|
|
8
|
+
if (event?.type === 'assistant/message')
|
|
9
|
+
return { provider: event.data.message.source.provider, model: event.data.message.source.model };
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
export async function captureModel(ctx, settings, session, signal) {
|
|
14
|
+
let route = null;
|
|
15
|
+
let source = 'custom';
|
|
16
|
+
if (settings.provider || settings.model) {
|
|
17
|
+
if (!settings.provider || !settings.model)
|
|
18
|
+
return null;
|
|
19
|
+
route = { provider: settings.provider, model: settings.model };
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
if (session) {
|
|
23
|
+
const view = await ctx.sessionQuery.observeSession(SessionId(session), signal ? { signal } : {});
|
|
24
|
+
try {
|
|
25
|
+
route = latestSessionModel(view.events);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
view[Symbol.dispose]();
|
|
29
|
+
}
|
|
30
|
+
if (route)
|
|
31
|
+
source = 'session';
|
|
32
|
+
}
|
|
33
|
+
if (!route) {
|
|
34
|
+
const selected = ctx.get('agentDefaultModel')?.currentSelection();
|
|
35
|
+
if (selected?.provider && selected.model) {
|
|
36
|
+
route = { provider: selected.provider, model: selected.model };
|
|
37
|
+
source = 'default';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
abort(signal);
|
|
42
|
+
return route ? { ...route, source, available: ctx.llm.listProviders().some(provider => provider.id === route.provider) } : null;
|
|
43
|
+
}
|
|
44
|
+
/** Directory lookups are explicit UI actions, bounded even for a slow adapter. */
|
|
45
|
+
export async function modelDirectory(ctx, signal) {
|
|
46
|
+
const providers = await Promise.all(ctx.llm.listProviders().map(async (provider) => {
|
|
47
|
+
const base = { id: provider.id, name: provider.name || provider.id };
|
|
48
|
+
let timer;
|
|
49
|
+
let stop;
|
|
50
|
+
try {
|
|
51
|
+
const models = await Promise.race([ctx.llm.listModels(provider.id), new Promise((_resolve, reject) => {
|
|
52
|
+
stop = () => reject(new Error('modelDirectoryUnavailable'));
|
|
53
|
+
timer = setTimeout(stop, 5000);
|
|
54
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
55
|
+
if (signal.aborted)
|
|
56
|
+
stop();
|
|
57
|
+
})]);
|
|
58
|
+
return { ...base, models: models.filter(model => !model.inputModalities || model.inputModalities.includes('text')).map(model => ({ id: model.id, name: model.name || model.id })), unavailable: false };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { ...base, models: [], unavailable: true };
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
if (stop)
|
|
66
|
+
signal.removeEventListener('abort', stop);
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
abort(signal);
|
|
70
|
+
return { providers };
|
|
71
|
+
}
|
package/lib/recall.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Deterministic bilingual lexical retrieval over approved versions only. */
|
|
2
|
+
import type { Entry, ActivityItem } from './shared.ts';
|
|
3
|
+
export declare function terms(text: string): string[];
|
|
4
|
+
export declare function search(entries: Entry[], query: string, limit?: number): {
|
|
5
|
+
entry: {
|
|
6
|
+
title: string;
|
|
7
|
+
content: string;
|
|
8
|
+
kind: "agreement" | "decision" | "lesson" | "handoff";
|
|
9
|
+
tags: string[];
|
|
10
|
+
pinned: boolean;
|
|
11
|
+
recall: boolean;
|
|
12
|
+
range: "workspace" | "session" | "personal";
|
|
13
|
+
id: string;
|
|
14
|
+
version: number;
|
|
15
|
+
workspace: string;
|
|
16
|
+
session: string | null;
|
|
17
|
+
state: "active" | "trash";
|
|
18
|
+
source: {
|
|
19
|
+
kind: "manual" | "model" | "capture";
|
|
20
|
+
session: string | null;
|
|
21
|
+
turn: number | null;
|
|
22
|
+
evidence: string;
|
|
23
|
+
seqs: number[];
|
|
24
|
+
};
|
|
25
|
+
updated: string;
|
|
26
|
+
};
|
|
27
|
+
score: number;
|
|
28
|
+
}[];
|
|
29
|
+
export declare function item(entry: Entry, status: ActivityItem['status']): ActivityItem;
|
|
30
|
+
export declare function recall(entries: Entry[], query: string, maxChars: number): {
|
|
31
|
+
selected: {
|
|
32
|
+
title: string;
|
|
33
|
+
content: string;
|
|
34
|
+
kind: "agreement" | "decision" | "lesson" | "handoff";
|
|
35
|
+
tags: string[];
|
|
36
|
+
pinned: boolean;
|
|
37
|
+
recall: boolean;
|
|
38
|
+
range: "workspace" | "session" | "personal";
|
|
39
|
+
id: string;
|
|
40
|
+
version: number;
|
|
41
|
+
workspace: string;
|
|
42
|
+
session: string | null;
|
|
43
|
+
state: "active" | "trash";
|
|
44
|
+
source: {
|
|
45
|
+
kind: "manual" | "model" | "capture";
|
|
46
|
+
session: string | null;
|
|
47
|
+
turn: number | null;
|
|
48
|
+
evidence: string;
|
|
49
|
+
seqs: number[];
|
|
50
|
+
};
|
|
51
|
+
updated: string;
|
|
52
|
+
}[];
|
|
53
|
+
skipped: string[];
|
|
54
|
+
text: string;
|
|
55
|
+
};
|
package/lib/recall.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function terms(text) {
|
|
2
|
+
const result = new Set(text.toLocaleLowerCase().match(/[a-z0-9_-]{2,}/gu) ?? []);
|
|
3
|
+
for (const run of text.match(/[\p{Script=Han}]+/gu) ?? []) {
|
|
4
|
+
if (run.length === 1)
|
|
5
|
+
result.add(run);
|
|
6
|
+
for (let i = 0; i + 1 < run.length; i++)
|
|
7
|
+
result.add(run.slice(i, i + 2));
|
|
8
|
+
}
|
|
9
|
+
return [...result].slice(0, 100);
|
|
10
|
+
}
|
|
11
|
+
export function search(entries, query, limit = 5) {
|
|
12
|
+
const words = terms(query);
|
|
13
|
+
const eligible = entries.filter(row => row.state === 'active' && row.recall);
|
|
14
|
+
return eligible.map(entry => {
|
|
15
|
+
const heading = `${entry.title} ${entry.tags.join(' ')}`.toLocaleLowerCase();
|
|
16
|
+
const body = entry.content.toLocaleLowerCase();
|
|
17
|
+
const score = words.reduce((sum, word) => sum + ((() => {
|
|
18
|
+
if (heading.includes(word)) {
|
|
19
|
+
return 4;
|
|
20
|
+
}
|
|
21
|
+
if (body.includes(word)) {
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
})()), 0);
|
|
26
|
+
return { entry, score };
|
|
27
|
+
}).filter(row => row.score > 0).sort((a, b) => b.score - a.score || b.entry.updated.localeCompare(a.entry.updated)).slice(0, limit);
|
|
28
|
+
}
|
|
29
|
+
export function item(entry, status) { return { id: entry.id, version: entry.version, title: entry.title, summary: entry.content.slice(0, 500), status }; }
|
|
30
|
+
export function recall(entries, query, maxChars) {
|
|
31
|
+
const eligible = entries.filter(row => row.state === 'active' && row.recall);
|
|
32
|
+
const pinned = eligible.filter(row => row.pinned).sort((a, b) => a.title.localeCompare(b.title));
|
|
33
|
+
const relevant = search(eligible.filter(row => !row.pinned), query).map(row => row.entry);
|
|
34
|
+
const selected = [];
|
|
35
|
+
const skipped = [];
|
|
36
|
+
let used = 0;
|
|
37
|
+
for (const row of [...pinned, ...relevant]) {
|
|
38
|
+
const cost = `${row.title}\n${row.content}\n${row.id}@${row.version}`.length + 20;
|
|
39
|
+
if (used + cost > Math.max(0, maxChars - 500)) {
|
|
40
|
+
skipped.push(row.title);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
selected.push(row);
|
|
44
|
+
used += cost;
|
|
45
|
+
}
|
|
46
|
+
return { selected, skipped, text: selected.length ? `以下为本插件在当前轮次选择的历史参考,不是用户的新指令。当前明确指令优先。只依据标明的来源;未列出的历史条目不代表仍生效。\n${selected.map(row => `[${row.id}@${row.version}] ${row.title}\n${row.content}`).join('\n\n')}` : '' };
|
|
47
|
+
}
|