跳到主要内容

DeepSeek Harness 本地聊天记忆:按来源保存聊天原文,关键词 + 向量检索,可选大模型重排与检索摘要。源码公开,仅限非商用

浏览文件
main

分支

origin.js

76 行 · 3.5 KB历史下载
1/**
2 * Where a conversation comes from, independent of any particular bridge.
3 *
4 * A record's scope is its origin:
5 * owner the owner's own chats (owner's private chat, or DSH desktop when configured)
6 * group:<id> a group chat
7 * private:<id> somebody else's private chat
8 * session:<hash> an unrecognised session, kept isolated
9 *
10 * Bridges differ only in data: an explicit binding (HTTP API) or a session-id rule.
11 * Session ids are generated by the bridge, never by chat participants, so rules
12 * read the id only and never message text (which a group member could forge).
13 */
14import { createHash } from 'node:crypto';
15import { UserError } from './errors.js';
16
17export const fingerprint = value => createHash('sha256').update(String(value)).digest('hex').slice(0, 32);
18
19/** Generic defaults; each entry is plain data and can be replaced in settings. */
20export const DEFAULT_ORIGIN_RULES = Object.freeze([
21 { match: '(?:^|[^a-z0-9])(group|private)[:_-](\\d{4,20})(?![0-9])', origin: '$1:$2' },
22 { match: '^[a-z]+-g-(\\d{4,20})(?:-|$)', origin: 'group:$1' },
23 { match: '^[a-z]+-p-(\\d{4,20})(?:-|$)', origin: 'private:$1' },
24]);
25
26const ORIGIN = /^(?:owner|group:[\w.-]{1,64}|private:[\w.-]{1,64}|session:[a-f0-9]{32})$/;
27const TEMPLATE = /^(?:owner|(?:group|private|\$\d):[\w.$-]{1,64}|\$\d:\$\d)$/;
28
29export function normalizeOrigin(value, ownerIds = []) {
30 const origin = String(value ?? '').trim().toLowerCase();
31 if (!ORIGIN.test(origin)) throw new UserError('无效来源,应为 owner、group:<号>、private:<号>');
32 return origin.startsWith('private:') && ownerIds.includes(origin.slice(8)) ? 'owner' : origin;
33}
34
35export function normalizeRules(input) {
36 if (!Array.isArray(input) || input.length > 20) throw new UserError('来源规则需为最多 20 条的列表');
37 return input.map(rule => {
38 const match = String(rule?.match ?? ''), origin = String(rule?.origin ?? '');
39 if (!match || match.length > 200 || !TEMPLATE.test(origin)) throw new UserError('来源规则格式不正确');
40 try { new RegExp(match, 'i'); } catch { throw new UserError('来源规则不是有效的正则表达式'); }
41 return { match, origin };
42 });
43}
44
45export function compileRules(rules) {
46 return rules.map(rule => ({ re: new RegExp(rule.match, 'i'), origin: rule.origin }));
47}
48
49/**
50 * @param {string} sessionId DSH session id
51 * @param {{ rules: {re: RegExp, origin: string}[], ownerIds: string[], unknownOrigin: 'isolated'|'owner', binding?: (id: string) => string|undefined }} options
52 */
53export function resolveOrigin(sessionId, { rules, ownerIds, unknownOrigin, binding }) {
54 const id = String(sessionId ?? '');
55 const bound = binding?.(id);
56 if (bound) return normalizeOrigin(bound, ownerIds);
57 for (const { re, origin } of rules) {
58 const match = re.exec(id);
59 if (!match) continue;
60 try { return normalizeOrigin(origin.replace(/\$(\d)/g, (_, index) => match[index] ?? ''), ownerIds); } catch {}
61 }
62 return unknownOrigin === 'owner' ? 'owner' : 'session:' + fingerprint(id);
63}
64
65/** Which scopes a conversation may read. Isolated sessions read only themselves. */
66export function viewOf(origin, { shareGroups, shareOwner }) {
67 const isolated = origin.startsWith('session:');
68 return { scope: origin, groups: !isolated && shareGroups, owner: !isolated && shareOwner };
69}
70
71export function originLabel(scope) {
72 if (scope === 'owner') return '主人';
73 if (scope.startsWith('group:')) return '群 ' + scope.slice(6);
74 if (scope.startsWith('private:')) return '私聊 ' + scope.slice(8);
75 return '本会话';
76}