dsh-hams 0.1.2 → 0.1.4
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/README.md +4 -0
- package/dist/index.js +11335 -0
- package/package.json +9 -5
- package/src/domain/check.ts +0 -193
- package/src/domain/errors.ts +0 -59
- package/src/domain/evidence.ts +0 -134
- package/src/domain/ids.ts +0 -31
- package/src/host/chat-runtime.ts +0 -489
- package/src/host/dsh-adapter.ts +0 -867
- package/src/host/session-adapter.ts +0 -276
- package/src/http/handler.ts +0 -1132
- package/src/index.js +0 -53
- package/src/service/audit.ts +0 -92
- package/src/service/chat.ts +0 -1100
- package/src/service/contracts.ts +0 -193
- package/src/service/defaults.ts +0 -182
- package/src/service/deliverables.ts +0 -240
- package/src/service/hams.ts +0 -3103
- package/src/service/knowledge.ts +0 -940
- package/src/service/member-config.ts +0 -39
- package/src/service/members.ts +0 -506
- package/src/service/memory.ts +0 -375
- package/src/service/permissions.ts +0 -118
- package/src/service/project-lifecycle.ts +0 -233
- package/src/service/projects.ts +0 -840
- package/src/service/receipts.ts +0 -63
- package/src/service/templates.ts +0 -140
- package/src/service/tick.ts +0 -751
- package/src/service/tools.ts +0 -711
- package/src/service/types.ts +0 -219
- package/src/service/work-item-create.ts +0 -159
- package/src/service/workspace-profile.ts +0 -443
- package/src/store/backup.ts +0 -239
- package/src/store/bootstrap.ts +0 -225
- package/src/store/db.ts +0 -179
- package/src/store/lock.ts +0 -194
- package/src/store/migrations.ts +0 -211
- package/src/store/schema/001_p1.sql +0 -846
- package/src/store/schema/001_p1.sql.ts +0 -865
- package/src/store/schema/002_knowledge.sql +0 -54
- package/src/store/schema/002_knowledge.sql.ts +0 -68
- package/src/store/schema/003_projects.sql.ts +0 -3
- package/src/store/schema/004_team.sql.ts +0 -50
- package/src/store/schema/005_target_check.sql.ts +0 -22
- package/src/store/schema/006_target_flow.sql.ts +0 -19
- package/src/store/schema/007_project_delete.sql.ts +0 -17
- package/src/store/schema/008_memory.sql.ts +0 -47
- package/src/store/schema/generate-projects.mjs +0 -16
- package/src/store/seed.ts +0 -114
package/src/service/memory.ts
DELETED
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 项目记忆:把推进过程中值得留下来的短句收起来,并在需要时喂回给 bot。
|
|
3
|
-
*
|
|
4
|
-
* 三类来源:
|
|
5
|
-
* - `auto`:群聊消息里被规则识别出来的决策 / 偏好 / 教训 / 能力 / 事实;
|
|
6
|
-
* - `manual`:人在项目「记忆」页签手写;
|
|
7
|
-
* - `tool`:bot 通过 hams_remember 主动记录。
|
|
8
|
-
*
|
|
9
|
-
* 能力类(kind='capability')记忆会镜像一份到 `bot_memory`,并进入该 bot
|
|
10
|
-
* 的会话上下文;其它类型只属于项目,避免把“项目口径”误当成 bot 的永久能力。
|
|
11
|
-
*
|
|
12
|
-
* 纪律:
|
|
13
|
-
* - 自动提取只做保守的关键词规则,宁可少收,不把整段聊天搬进记忆;
|
|
14
|
-
* - 同一项目内用 dedupe_key 去重,重复轮询/重复发送不会产生重复记忆;
|
|
15
|
-
* - 归档只改 status,不删行;项目软删时记忆随项目一起隐藏。
|
|
16
|
-
*/
|
|
17
|
-
import type { DatabaseSync } from 'node:sqlite'
|
|
18
|
-
import { createHash } from 'node:crypto'
|
|
19
|
-
import { stateInvalid } from '../domain/errors.ts'
|
|
20
|
-
import type { IdFactory } from '../domain/ids.ts'
|
|
21
|
-
|
|
22
|
-
export const MEMORY_KINDS = ['fact', 'decision', 'preference', 'lesson', 'capability'] as const
|
|
23
|
-
export const MEMORY_ORIGINS = ['auto', 'manual', 'tool'] as const
|
|
24
|
-
export const MEMORY_STATUSES = ['active', 'archived'] as const
|
|
25
|
-
|
|
26
|
-
export type MemoryKind = (typeof MEMORY_KINDS)[number]
|
|
27
|
-
export type MemoryOrigin = (typeof MEMORY_ORIGINS)[number]
|
|
28
|
-
export type MemoryStatus = (typeof MEMORY_STATUSES)[number]
|
|
29
|
-
|
|
30
|
-
export const MEMORY_KIND_LABEL: Record<MemoryKind, string> = {
|
|
31
|
-
fact: '事实',
|
|
32
|
-
decision: '决策',
|
|
33
|
-
preference: '偏好',
|
|
34
|
-
lesson: '教训',
|
|
35
|
-
capability: '能力',
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const MAX_TITLE = 120
|
|
39
|
-
const MAX_BODY = 2000
|
|
40
|
-
const MAX_RETURN = 200
|
|
41
|
-
|
|
42
|
-
export interface ProjectMemoryRow {
|
|
43
|
-
id: string
|
|
44
|
-
project_id: string
|
|
45
|
-
kind: MemoryKind
|
|
46
|
-
title: string
|
|
47
|
-
body: string
|
|
48
|
-
origin: MemoryOrigin
|
|
49
|
-
status: MemoryStatus
|
|
50
|
-
member_id: string | null
|
|
51
|
-
source_ref: string | null
|
|
52
|
-
dedupe_key: string
|
|
53
|
-
created_by: string
|
|
54
|
-
created_at: number
|
|
55
|
-
updated_at: number
|
|
56
|
-
revision: number
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface ProjectMemoryView extends ProjectMemoryRow {
|
|
60
|
-
member_name: string | null
|
|
61
|
-
created_by_name: string | null
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface MemberMemoryRow {
|
|
65
|
-
id: string
|
|
66
|
-
member_id: string
|
|
67
|
-
project_id: string
|
|
68
|
-
memory_id: string
|
|
69
|
-
kind: string
|
|
70
|
-
title: string
|
|
71
|
-
body: string
|
|
72
|
-
created_at: number
|
|
73
|
-
project_name?: string
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface CreateMemoryInput {
|
|
77
|
-
projectId: string
|
|
78
|
-
kind: string
|
|
79
|
-
title: string
|
|
80
|
-
body: string
|
|
81
|
-
origin?: string
|
|
82
|
-
memberId?: string | null
|
|
83
|
-
sourceRef?: string | null
|
|
84
|
-
dedupeKey?: string | null
|
|
85
|
-
status?: string
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface CreateMemoryResult {
|
|
89
|
-
memory: ProjectMemoryView
|
|
90
|
-
created: boolean
|
|
91
|
-
syncedTo: string | null
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function text(value: unknown, field: string, max: number): string {
|
|
95
|
-
if (typeof value !== 'string') throw stateInvalid(`${field} 必须是字符串`)
|
|
96
|
-
const trimmed = value.trim()
|
|
97
|
-
if (!trimmed) throw stateInvalid(`${field} 不能为空`)
|
|
98
|
-
if (trimmed.length > max) throw stateInvalid(`${field} 超过 ${max} 字`)
|
|
99
|
-
return trimmed
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function optionalText(value: unknown, field: string, max: number): string | null {
|
|
103
|
-
if (value === undefined || value === null) return null
|
|
104
|
-
if (typeof value !== 'string') throw stateInvalid(`${field} 必须是字符串`)
|
|
105
|
-
const trimmed = value.trim()
|
|
106
|
-
if (!trimmed) return null
|
|
107
|
-
if (trimmed.length > max) throw stateInvalid(`${field} 超过 ${max} 字`)
|
|
108
|
-
return trimmed
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function oneOf<T extends string>(value: unknown, field: string, allowed: readonly T[], fallback?: T): T {
|
|
112
|
-
if ((value === undefined || value === null || value === '') && fallback !== undefined) return fallback
|
|
113
|
-
if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {
|
|
114
|
-
throw stateInvalid(`${field} 只能是 ${allowed.join(' / ')}`)
|
|
115
|
-
}
|
|
116
|
-
return value as T
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function normalize(value: string): string {
|
|
120
|
-
return value.replace(/\s+/g, ' ').trim()
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function memoryDedupeKey(kind: string, title: string, body: string): string {
|
|
124
|
-
return createHash('sha256').update(`${kind}\n${normalize(title)}\n${normalize(body)}`).digest('hex').slice(0, 32)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function projectOrThrow(db: DatabaseSync, projectId: string): { id: string; status: string } {
|
|
128
|
-
const row = db
|
|
129
|
-
.prepare('SELECT id, status FROM project WHERE id = ? AND deleted_at IS NULL')
|
|
130
|
-
.get(projectId) as { id: string; status: string } | undefined
|
|
131
|
-
if (!row) throw stateInvalid(`项目不存在:${projectId}`)
|
|
132
|
-
return row
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function memoryView(db: DatabaseSync, projectId: string, dedupeKey: string): ProjectMemoryView | null {
|
|
136
|
-
return (
|
|
137
|
-
(db
|
|
138
|
-
.prepare(
|
|
139
|
-
`SELECT m.*, mb.name AS member_name, cb.name AS created_by_name
|
|
140
|
-
FROM project_memory m
|
|
141
|
-
LEFT JOIN member mb ON mb.id = m.member_id
|
|
142
|
-
LEFT JOIN member cb ON cb.id = m.created_by
|
|
143
|
-
WHERE m.project_id = ? AND m.dedupe_key = ?`,
|
|
144
|
-
)
|
|
145
|
-
.get(projectId, dedupeKey) as ProjectMemoryView | undefined) ?? null
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** 写一条记忆;能力类且指定了 bot 时同步一份到 bot_memory。返回是否真的新建。 */
|
|
150
|
-
export function createMemory(
|
|
151
|
-
db: DatabaseSync,
|
|
152
|
-
ids: IdFactory,
|
|
153
|
-
now: number,
|
|
154
|
-
actorId: string,
|
|
155
|
-
input: CreateMemoryInput,
|
|
156
|
-
): CreateMemoryResult {
|
|
157
|
-
const project = projectOrThrow(db, input.projectId)
|
|
158
|
-
if (project.status === 'archived') throw stateInvalid('归档项目只读,不能再写记忆')
|
|
159
|
-
const kind = oneOf(input.kind, '记忆类型', MEMORY_KINDS)
|
|
160
|
-
const origin = oneOf(input.origin, '记忆来源', MEMORY_ORIGINS, 'manual')
|
|
161
|
-
const status = oneOf(input.status, '记忆状态', MEMORY_STATUSES, 'active')
|
|
162
|
-
const title = text(input.title, '记忆标题', MAX_TITLE)
|
|
163
|
-
const body = text(input.body, '记忆正文', MAX_BODY)
|
|
164
|
-
const sourceRef = optionalText(input.sourceRef, '来源', 200)
|
|
165
|
-
const dedupeKey = optionalText(input.dedupeKey, '去重键', 160) || memoryDedupeKey(kind, title, body)
|
|
166
|
-
|
|
167
|
-
let memberId = optionalText(input.memberId, '归属 bot', 64)
|
|
168
|
-
if (kind !== 'capability') memberId = null
|
|
169
|
-
if (memberId) {
|
|
170
|
-
const member = db.prepare('SELECT id, kind, status FROM member WHERE id = ?').get(memberId) as
|
|
171
|
-
| { id: string; kind: string; status: string }
|
|
172
|
-
| undefined
|
|
173
|
-
if (!member || member.kind !== 'bot') throw stateInvalid('能力类记忆只能同步给 bot')
|
|
174
|
-
if (member.status !== 'active') throw stateInvalid(`${memberId} 不在岗,暂不同步能力记忆`)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const insert = db
|
|
178
|
-
.prepare(
|
|
179
|
-
`INSERT OR IGNORE INTO project_memory
|
|
180
|
-
(id, project_id, kind, title, body, origin, status, member_id, source_ref,
|
|
181
|
-
dedupe_key, created_by, created_at, updated_at, revision)
|
|
182
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
|
183
|
-
)
|
|
184
|
-
.run(ids(), input.projectId, kind, title, body, origin, status, memberId, sourceRef, dedupeKey, actorId, now, now)
|
|
185
|
-
const created = Number(insert.changes ?? 0) > 0
|
|
186
|
-
const memory = memoryView(db, input.projectId, dedupeKey)
|
|
187
|
-
if (!memory) throw stateInvalid('记忆写入失败')
|
|
188
|
-
let syncedTo: string | null = null
|
|
189
|
-
if (kind === 'capability' && memberId) {
|
|
190
|
-
db.prepare(
|
|
191
|
-
`INSERT OR IGNORE INTO bot_memory (id, member_id, project_id, memory_id, kind, title, body, created_at)
|
|
192
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
193
|
-
).run(ids(), memberId, input.projectId, memory.id, kind, title, body, now)
|
|
194
|
-
syncedTo = memberId
|
|
195
|
-
}
|
|
196
|
-
return { memory, created, syncedTo }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export interface UpdateMemoryInput {
|
|
200
|
-
memoryId: string
|
|
201
|
-
title?: string
|
|
202
|
-
body?: string
|
|
203
|
-
status?: string
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** 改标题/正文/状态;能力类记忆的 bot 快照跟着更新。 */
|
|
207
|
-
export function updateMemory(
|
|
208
|
-
db: DatabaseSync,
|
|
209
|
-
now: number,
|
|
210
|
-
input: UpdateMemoryInput,
|
|
211
|
-
): ProjectMemoryView {
|
|
212
|
-
const row = db.prepare('SELECT * FROM project_memory WHERE id = ?').get(input.memoryId) as ProjectMemoryRow | undefined
|
|
213
|
-
if (!row) throw stateInvalid(`记忆不存在:${input.memoryId}`)
|
|
214
|
-
const title = input.title === undefined ? row.title : text(input.title, '记忆标题', MAX_TITLE)
|
|
215
|
-
const body = input.body === undefined ? row.body : text(input.body, '记忆正文', MAX_BODY)
|
|
216
|
-
const status = input.status === undefined ? row.status : oneOf(input.status, '记忆状态', MEMORY_STATUSES)
|
|
217
|
-
const revision = Number(row.revision) + 1
|
|
218
|
-
db.prepare('UPDATE project_memory SET title = ?, body = ?, status = ?, revision = ?, updated_at = ? WHERE id = ?').run(
|
|
219
|
-
title,
|
|
220
|
-
body,
|
|
221
|
-
status,
|
|
222
|
-
revision,
|
|
223
|
-
now,
|
|
224
|
-
row.id,
|
|
225
|
-
)
|
|
226
|
-
if (row.kind === 'capability' && row.member_id) {
|
|
227
|
-
db.prepare('UPDATE bot_memory SET title = ?, body = ? WHERE memory_id = ?').run(title, body, row.id)
|
|
228
|
-
}
|
|
229
|
-
return (
|
|
230
|
-
(db
|
|
231
|
-
.prepare(
|
|
232
|
-
`SELECT m.*, mb.name AS member_name, cb.name AS created_by_name
|
|
233
|
-
FROM project_memory m
|
|
234
|
-
LEFT JOIN member mb ON mb.id = m.member_id
|
|
235
|
-
LEFT JOIN member cb ON cb.id = m.created_by
|
|
236
|
-
WHERE m.id = ?`,
|
|
237
|
-
)
|
|
238
|
-
.get(row.id) as ProjectMemoryView | undefined) ?? { ...row, title, body, status, revision, member_name: null, created_by_name: null }
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function listProjectMemories(
|
|
243
|
-
db: DatabaseSync,
|
|
244
|
-
filter: { projectId: string; status?: string; kind?: string; limit?: number },
|
|
245
|
-
): ProjectMemoryView[] {
|
|
246
|
-
projectOrThrow(db, filter.projectId)
|
|
247
|
-
const clauses = ['m.project_id = ?']
|
|
248
|
-
const params: unknown[] = [filter.projectId]
|
|
249
|
-
if (filter.status) {
|
|
250
|
-
clauses.push('m.status = ?')
|
|
251
|
-
params.push(oneOf(filter.status, '记忆状态', MEMORY_STATUSES))
|
|
252
|
-
}
|
|
253
|
-
if (filter.kind) {
|
|
254
|
-
clauses.push('m.kind = ?')
|
|
255
|
-
params.push(oneOf(filter.kind, '记忆类型', MEMORY_KINDS))
|
|
256
|
-
}
|
|
257
|
-
const limit = Math.min(Math.max(filter.limit ?? 200, 1), 500)
|
|
258
|
-
return db
|
|
259
|
-
.prepare(
|
|
260
|
-
`SELECT m.*, mb.name AS member_name, cb.name AS created_by_name
|
|
261
|
-
FROM project_memory m
|
|
262
|
-
LEFT JOIN member mb ON mb.id = m.member_id
|
|
263
|
-
LEFT JOIN member cb ON cb.id = m.created_by
|
|
264
|
-
WHERE ${clauses.join(' AND ')}
|
|
265
|
-
ORDER BY m.created_at DESC, m.id DESC
|
|
266
|
-
LIMIT ${limit}`,
|
|
267
|
-
)
|
|
268
|
-
.all(...(params as never[])) as ProjectMemoryView[]
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export function listMemberMemories(db: DatabaseSync, memberId: string, limit = 50): MemberMemoryRow[] {
|
|
272
|
-
return db
|
|
273
|
-
.prepare(
|
|
274
|
-
`SELECT mm.*, p.name AS project_name
|
|
275
|
-
FROM bot_memory mm
|
|
276
|
-
LEFT JOIN project p ON p.id = mm.project_id
|
|
277
|
-
WHERE mm.member_id = ?
|
|
278
|
-
ORDER BY mm.created_at DESC, mm.id DESC
|
|
279
|
-
LIMIT ?`,
|
|
280
|
-
)
|
|
281
|
-
.all(memberId, Math.min(Math.max(limit, 1), 200)) as MemberMemoryRow[]
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function block(rows: ProjectMemoryView[], title: string): string {
|
|
285
|
-
if (!rows.length) return ''
|
|
286
|
-
const lines = rows.map((row) => {
|
|
287
|
-
const body = normalize(row.body)
|
|
288
|
-
const detail = body && body !== normalize(row.title) ? `:${body.slice(0, 180)}` : ''
|
|
289
|
-
return `- [${MEMORY_KIND_LABEL[row.kind] ?? row.kind}] ${row.title.slice(0, 80)}${detail}`
|
|
290
|
-
})
|
|
291
|
-
return `【${title}】\n${lines.join('\n')}`
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** 注入项目群会话的上下文:最近的有效记忆,最多 8 条。 */
|
|
295
|
-
export function projectMemoryBlock(db: DatabaseSync, projectId: string, limit = 8): string {
|
|
296
|
-
const rows = listProjectMemories(db, { projectId, status: 'active', limit })
|
|
297
|
-
.slice()
|
|
298
|
-
.reverse()
|
|
299
|
-
return block(rows, `项目记忆 · ${rows.length} 条`)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** 注入单个 bot 会话的上下文:它从项目里同步过的能力记忆。 */
|
|
303
|
-
export function memberMemoryBlock(db: DatabaseSync, memberId: string, limit = 6): string {
|
|
304
|
-
const rows = listMemberMemories(db, memberId, limit)
|
|
305
|
-
.slice()
|
|
306
|
-
.reverse()
|
|
307
|
-
if (!rows.length) return ''
|
|
308
|
-
return `【bot 记忆(从项目同步)】\n${rows
|
|
309
|
-
.map((row) => `- [${MEMORY_KIND_LABEL[row.kind as MemoryKind] ?? row.kind}] ${row.title.slice(0, 80)}(${row.project_name ?? ''})`)
|
|
310
|
-
.join('\n')}`
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ───────────────────────── 群聊自动发现 ─────────────────────────
|
|
314
|
-
|
|
315
|
-
export interface MemoryCandidate {
|
|
316
|
-
kind: MemoryKind
|
|
317
|
-
title: string
|
|
318
|
-
body: string
|
|
319
|
-
memberId: string | null
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
export interface ExtractContext {
|
|
323
|
-
authorKind: 'human' | 'bot'
|
|
324
|
-
authorId: string
|
|
325
|
-
authorName: string
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** 顺序即优先级:一句话同时命中多个类型时,按更“值得记”的那个归类。 */
|
|
329
|
-
const CUES: Array<[MemoryKind, RegExp]> = [
|
|
330
|
-
['decision', /(决定|定了|敲定|拍板|确定用|结论[::是]|采用|统一用|以后都|默认用|就这么|最终方案)/],
|
|
331
|
-
['lesson', /(教训|踩坑|注意|下次|避免|问题出在|原因是|根因|不要再|容易错)/],
|
|
332
|
-
['preference', /(偏好|喜欢|不喜欢|习惯|优先|尽量|更希望|不希望|别用|不要用|最好)/],
|
|
333
|
-
['capability', /(我会|我能|擅长|可以处理|处理流程|步骤是|工具是|方法就是|做法是)/],
|
|
334
|
-
['fact', /(口径[::]|定义是|数据来源|阈值|统计口径|事实[::]|发现[::]|结论[::]|目前实际)/],
|
|
335
|
-
]
|
|
336
|
-
|
|
337
|
-
function makeTitle(body: string): string {
|
|
338
|
-
let title = body
|
|
339
|
-
.replace(/^[@@]\s*\S+\s*/, '')
|
|
340
|
-
.replace(/^(决定|结论|注意|教训|偏好|事实|发现|口径|定义)\s*[::,,\s]\s*/, '')
|
|
341
|
-
.trim()
|
|
342
|
-
if (title.length > 28) title = `${title.slice(0, 28)}…`
|
|
343
|
-
if (title.length < 4) title = body.slice(0, 28)
|
|
344
|
-
return title
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* 从一条群聊消息里提取候选记忆(纯函数)。
|
|
349
|
-
* 保守规则:只认关键词、句长 8–240、问句与寒暄跳过、单条最多 2 个。
|
|
350
|
-
*/
|
|
351
|
-
export function extractMemoryCandidates(text: string, ctx: ExtractContext): MemoryCandidate[] {
|
|
352
|
-
const clean = String(text || '')
|
|
353
|
-
.replace(/```[\s\S]*?```/g, ' ')
|
|
354
|
-
.replace(/^\s*[@@]\s*\S+\s*/, '')
|
|
355
|
-
const sentences = clean
|
|
356
|
-
.split(/(?<=[。!?!?;;])|\n+/)
|
|
357
|
-
.map((part) => part.trim())
|
|
358
|
-
.filter(Boolean)
|
|
359
|
-
const out: MemoryCandidate[] = []
|
|
360
|
-
for (const body of sentences) {
|
|
361
|
-
if (body.length < 8 || body.length > 240) continue
|
|
362
|
-
if (/^(收到|好的|好|明白|稍等|谢谢|ok|OK|可以)[。.!!]?$/.test(body)) continue
|
|
363
|
-
if (/[??]\s*$/.test(body)) continue
|
|
364
|
-
const hit = CUES.find(([kind, pattern]) => pattern.test(body) && (kind !== 'capability' || ctx.authorKind === 'bot'))
|
|
365
|
-
if (!hit) continue
|
|
366
|
-
out.push({
|
|
367
|
-
kind: hit[0],
|
|
368
|
-
title: makeTitle(body),
|
|
369
|
-
body,
|
|
370
|
-
memberId: hit[0] === 'capability' ? ctx.authorId : null,
|
|
371
|
-
})
|
|
372
|
-
if (out.length >= 2) break
|
|
373
|
-
}
|
|
374
|
-
return out
|
|
375
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 主体解析与能力校验。
|
|
3
|
-
*
|
|
4
|
-
* 铁律(backend-design §4.1 / §11):
|
|
5
|
-
* - 人的能力只来自「在岗岗位」的并集:`member_position` 的 [starts_at, ends_at) 必须覆盖当前时刻;
|
|
6
|
-
* - bot 的能力只来自 `member_permission`,且审批/发布类能力由库触发器拒绝授予;
|
|
7
|
-
* - 系统主体 `id='system'` 只用于自动化写入,不参与业务授权判断;
|
|
8
|
-
* - 没有在岗岗位的人 = 只读:任何写命令一律 POLICY_DENIED(对应 ㉑ 第 3 格)。
|
|
9
|
-
*
|
|
10
|
-
* 能力名先按首版闭环需要收敛成一组常量;8 个岗位的完整矩阵仍需业务签字(status §5),
|
|
11
|
-
* 所以这里只定义「有哪些能力名」,不内置任何岗位 → 能力的映射。
|
|
12
|
-
*/
|
|
13
|
-
import type { DatabaseSync } from 'node:sqlite'
|
|
14
|
-
import { denied } from '../domain/errors.ts'
|
|
15
|
-
import type { Principal } from './types.ts'
|
|
16
|
-
|
|
17
|
-
export const CAPABILITY = {
|
|
18
|
-
workItemCreate: 'work_item.create',
|
|
19
|
-
workItemAccept: 'work_item.accept',
|
|
20
|
-
workItemManage: 'work_item.manage',
|
|
21
|
-
workItemClaim: 'work_item.claim',
|
|
22
|
-
workItemExecute: 'work_item.execute',
|
|
23
|
-
workItemRequestClosure: 'work_item.request_closure',
|
|
24
|
-
evidenceAppend: 'evidence.append',
|
|
25
|
-
checkEvaluate: 'check.evaluate',
|
|
26
|
-
proposalCreate: 'proposal.create',
|
|
27
|
-
proposalDecide: 'proposal.decide',
|
|
28
|
-
approvalDecide: 'approval.decide',
|
|
29
|
-
connectorIngest: 'connector.ingest',
|
|
30
|
-
// M6:项目 / 指标 / 知识(人的能力仍只从在岗岗位来)
|
|
31
|
-
projectCreate: 'project.create',
|
|
32
|
-
projectManage: 'project.manage',
|
|
33
|
-
metricRecord: 'metric.record',
|
|
34
|
-
knowledgeWrite: 'knowledge.write',
|
|
35
|
-
knowledgeSync: 'knowledge.sync',
|
|
36
|
-
} as const
|
|
37
|
-
|
|
38
|
-
/** 只能给人、不得授予 bot 的能力(与库触发器 trg_bot_no_human_capability 保持一致)。 */
|
|
39
|
-
export const HUMAN_ONLY_CAPABILITIES = [
|
|
40
|
-
'approval.decide',
|
|
41
|
-
'proposal.decide',
|
|
42
|
-
'work_item.close',
|
|
43
|
-
'skill.publish',
|
|
44
|
-
'target.publish',
|
|
45
|
-
'review.confirm',
|
|
46
|
-
'project.archive',
|
|
47
|
-
'member.manage',
|
|
48
|
-
'position.manage',
|
|
49
|
-
'permission.grant',
|
|
50
|
-
] as const
|
|
51
|
-
|
|
52
|
-
export function resolvePrincipal(db: DatabaseSync, memberId: string, nowMs: number): Principal {
|
|
53
|
-
const member = db
|
|
54
|
-
.prepare('SELECT id, kind, name, role, status, current_version FROM member WHERE id = ?')
|
|
55
|
-
.get(memberId) as
|
|
56
|
-
| { id: string; kind: string; name: string; role: string; status: string; current_version: number }
|
|
57
|
-
| undefined
|
|
58
|
-
if (!member) throw denied(`主体不存在:${memberId}`)
|
|
59
|
-
if (member.status !== 'active') throw denied(`主体已停用:${member.name}`)
|
|
60
|
-
|
|
61
|
-
const capabilities = new Set<string>()
|
|
62
|
-
const positions: { positionId: string; name: string }[] = []
|
|
63
|
-
|
|
64
|
-
if (member.kind === 'human') {
|
|
65
|
-
const rows = db
|
|
66
|
-
.prepare(
|
|
67
|
-
`SELECT pp.capability AS capability, p.id AS position_id, p.name AS position_name
|
|
68
|
-
FROM member_position mp
|
|
69
|
-
JOIN position p ON p.id = mp.position_id AND p.status = 'active'
|
|
70
|
-
JOIN position_permission pp ON pp.position_id = mp.position_id
|
|
71
|
-
WHERE mp.member_id = ?
|
|
72
|
-
AND mp.starts_at <= ?
|
|
73
|
-
AND (mp.ends_at IS NULL OR mp.ends_at > ?)`,
|
|
74
|
-
)
|
|
75
|
-
.all(member.id, nowMs, nowMs) as { capability: string; position_id: string; position_name: string }[]
|
|
76
|
-
for (const row of rows) {
|
|
77
|
-
capabilities.add(row.capability)
|
|
78
|
-
if (!positions.some((item) => item.positionId === row.position_id)) {
|
|
79
|
-
positions.push({ positionId: row.position_id, name: row.position_name })
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} else if (member.kind === 'bot') {
|
|
83
|
-
const rows = db
|
|
84
|
-
.prepare('SELECT capability FROM member_permission WHERE member_id = ?')
|
|
85
|
-
.all(member.id) as { capability: string }[]
|
|
86
|
-
for (const row of rows) capabilities.add(row.capability)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
memberId: member.id,
|
|
91
|
-
kind: member.kind as Principal['kind'],
|
|
92
|
-
name: member.name,
|
|
93
|
-
role: member.role,
|
|
94
|
-
status: member.status,
|
|
95
|
-
currentVersion: Number(member.current_version),
|
|
96
|
-
positions,
|
|
97
|
-
capabilities,
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function requireCapability(principal: Principal, capability: string): void {
|
|
102
|
-
if (principal.kind === 'system') return
|
|
103
|
-
if (!principal.capabilities.has(capability)) {
|
|
104
|
-
throw denied(`「${principal.name}」没有 ${capability} 能力`, '确认岗位是否在岗,或由管理员补充授权')
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function requireHuman(principal: Principal): void {
|
|
109
|
-
if (principal.kind !== 'human') throw denied('该操作只允许人执行')
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function requireBot(principal: Principal): void {
|
|
113
|
-
if (principal.kind !== 'bot') throw denied('该操作只允许 bot 执行')
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function isSystem(principal: Principal): boolean {
|
|
117
|
-
return principal.kind === 'system'
|
|
118
|
-
}
|