dsh-anima 0.0.0-stage → 2.2.2

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/lib/audit.mjs ADDED
@@ -0,0 +1,126 @@
1
+ // lib/audit.mjs — 人设变更审计日志
2
+ //
3
+ // 为什么需要它:本插件有三个入口能改人设 —— 设置页、`/anima` 命令、
4
+ // 以及模型调用的 `set_assistant_persona` 工具。前两个是用户自己动手,
5
+ // 第三个是**模型主动改**。而「工具确认」默认是关的,也就是说模型可以直接
6
+ // 改写「你是谁、我叫你什么、用什么语气」。用户第二天发现语气变了、
7
+ // 名字换了,无从知道是谁、什么时候、把哪个字段从什么改成了什么。
8
+ //
9
+ // 这份日志就是回答那个问题的:**每一次人设变更都留痕**。
10
+ // 它只记「改了什么」,不做任何判断,也不参与提示词 —— 纯粹给人看。
11
+ //
12
+ // 设计约束:
13
+ // ① 只记人设与记忆相关的配置键,不记凭据类内容(本插件的配置里本就没有);
14
+ // ② 值超长时截断 —— 审计日志是给人扫一眼的,不是备份;
15
+ // ③ 有上限并环形淘汰,绝不无限增长(无界日志最终会拖垮存储)。
16
+
17
+ /** 谁能改人设。区分来源是这份日志的全部意义所在。 */
18
+ export const AUDIT_ACTORS = ['user', 'agent', 'system']
19
+
20
+ /** 每个值的记录上限:够认出「改了哪一段」,不够还原全文 —— 日志不是备份。 */
21
+ export const AUDIT_VALUE_MAX = 200
22
+
23
+ /** 环形上限:超出后丢弃最早的记录。 */
24
+ export const AUDIT_MAX_RECORDS = 200
25
+
26
+ /** 来源码 → 人话(设置页与命令共用,避免两处各写一份翻译)。 */
27
+ export const AUDIT_SOURCE_TEXT = {
28
+ settings: '设置页',
29
+ command: '命令',
30
+ tool: '模型工具',
31
+ proposal: '待确认提议',
32
+ preset: '人设预设',
33
+ reset: '重置',
34
+ memory: '记忆裁决',
35
+ other: '其他'
36
+ }
37
+
38
+ /** 归一化记录人(未知值一律按 system 处理,不扩大信任面)。 */
39
+ export function normalizeActor(actor) {
40
+ return AUDIT_ACTORS.includes(actor) ? actor : 'system'
41
+ }
42
+
43
+ /** 截断单个值:非字符串走 JSON 摘要,超长加省略号标注。 */
44
+ export function auditValue(value) {
45
+ if (value === undefined) return null
46
+ if (value === null) return null
47
+ if (typeof value === 'string') {
48
+ return value.length > AUDIT_VALUE_MAX ? `${value.slice(0, AUDIT_VALUE_MAX)}…(共 ${value.length} 字)` : value
49
+ }
50
+ if (typeof value === 'boolean' || typeof value === 'number') return value
51
+ if (Array.isArray(value)) return `[${value.length} 项]`
52
+ if (typeof value === 'object') {
53
+ const keys = Object.keys(value)
54
+ return `{${keys.length} 个字段}`
55
+ }
56
+ return String(value)
57
+ }
58
+
59
+ /**
60
+ * 把 diffConfig 产出的字段名清单翻译成逐字段的「前 → 后」。
61
+ *
62
+ * `memory.x` 这种复合键要拆开取 `before.memory.x`,否则审计里会看到
63
+ * `from: undefined` —— 那等于没记。
64
+ */
65
+ export function buildAuditChanges(before, after, changedKeys) {
66
+ const changes = []
67
+ for (const key of changedKeys ?? []) {
68
+ const read = (source) => {
69
+ if (!key.startsWith('memory.')) return source?.[key]
70
+ return source?.memory?.[key.startsWith('memory.') ? key.slice('memory.'.length) : key]
71
+ }
72
+ changes.push({
73
+ key,
74
+ from: auditValue(read(before)),
75
+ to: auditValue(read(after))
76
+ })
77
+ }
78
+ return changes
79
+ }
80
+
81
+ /**
82
+ * 构造一条审计记录。
83
+ *
84
+ * @param {object} input {changes, actor, source, note, sessionId}
85
+ * @param {Date} [at]
86
+ * @returns {object|null} changes 为空时返回 null(没有变更就没有日志)
87
+ */
88
+ export function makeAuditEntry({ changes, actor, source, note, sessionId }, at = new Date()) {
89
+ const list = Array.isArray(changes) ? changes.filter((c) => c && c.key) : []
90
+ if (list.length === 0) return null
91
+ const entry = {
92
+ id: `a-${at.getTime().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
93
+ at: at.toISOString(),
94
+ actor: normalizeActor(actor),
95
+ source: AUDIT_SOURCE_TEXT[source] ? source : 'other',
96
+ changes: list
97
+ }
98
+ const trimmedNote = typeof note === 'string' ? note.trim() : ''
99
+ if (trimmedNote) entry.note = trimmedNote.slice(0, AUDIT_VALUE_MAX)
100
+ if (sessionId) entry.sessionId = sessionId
101
+ return entry
102
+ }
103
+
104
+ /** 环形淘汰:保留最新的 max 条,返回 {kept, dropped} 的条数。 */
105
+ export function trimAudit(records, max = AUDIT_MAX_RECORDS) {
106
+ const list = Array.isArray(records) ? records : []
107
+ if (list.length <= max) return { kept: list, dropped: 0 }
108
+ const sorted = [...list].sort((a, b) => String(a?.at ?? '').localeCompare(String(b?.at ?? '')))
109
+ return { kept: sorted.slice(sorted.length - max), dropped: sorted.length - max }
110
+ }
111
+
112
+ /** 一行摘要,供命令与设置页列表使用。 */
113
+ export function summarizeAudit(entry) {
114
+ if (!entry) return ''
115
+ const who = AUDIT_SOURCE_TEXT[entry.source] ?? entry.source ?? '其他'
116
+ const fields = (entry.changes ?? []).map((c) => c.key).join(', ')
117
+ return `[${shortTime(entry.at)}] ${who}:${fields}`
118
+ }
119
+
120
+ /** 时间戳 → `MM-DD HH:mm`(列表里年份是噪声)。 */
121
+ export function shortTime(iso) {
122
+ const d = new Date(iso)
123
+ if (Number.isNaN(d.getTime())) return String(iso ?? '')
124
+ const pad = (n) => String(n).padStart(2, '0')
125
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
126
+ }