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 | */ |
| 14 | import { createHash } from 'node:crypto'; |
| 15 | import { UserError } from './errors.js'; |
| 16 | |
| 17 | export 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. */ |
| 20 | export 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 | |
| 26 | const ORIGIN = /^(?:owner|group:[\w.-]{1,64}|private:[\w.-]{1,64}|session:[a-f0-9]{32})$/; |
| 27 | const TEMPLATE = /^(?:owner|(?:group|private|\$\d):[\w.$-]{1,64}|\$\d:\$\d)$/; |
| 28 | |
| 29 | export 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 | |
| 35 | export 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 | |
| 45 | export 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 | */ |
| 53 | export 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. */ |
| 66 | export function viewOf(origin, { shareGroups, shareOwner }) { |
| 67 | const isolated = origin.startsWith('session:'); |
| 68 | return { scope: origin, groups: !isolated && shareGroups, owner: !isolated && shareOwner }; |
| 69 | } |
| 70 | |
| 71 | export 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 | } |

