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.
Files changed (49) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +11335 -0
  3. package/package.json +9 -5
  4. package/src/domain/check.ts +0 -193
  5. package/src/domain/errors.ts +0 -59
  6. package/src/domain/evidence.ts +0 -134
  7. package/src/domain/ids.ts +0 -31
  8. package/src/host/chat-runtime.ts +0 -489
  9. package/src/host/dsh-adapter.ts +0 -867
  10. package/src/host/session-adapter.ts +0 -276
  11. package/src/http/handler.ts +0 -1132
  12. package/src/index.js +0 -53
  13. package/src/service/audit.ts +0 -92
  14. package/src/service/chat.ts +0 -1100
  15. package/src/service/contracts.ts +0 -193
  16. package/src/service/defaults.ts +0 -182
  17. package/src/service/deliverables.ts +0 -240
  18. package/src/service/hams.ts +0 -3103
  19. package/src/service/knowledge.ts +0 -940
  20. package/src/service/member-config.ts +0 -39
  21. package/src/service/members.ts +0 -506
  22. package/src/service/memory.ts +0 -375
  23. package/src/service/permissions.ts +0 -118
  24. package/src/service/project-lifecycle.ts +0 -233
  25. package/src/service/projects.ts +0 -840
  26. package/src/service/receipts.ts +0 -63
  27. package/src/service/templates.ts +0 -140
  28. package/src/service/tick.ts +0 -751
  29. package/src/service/tools.ts +0 -711
  30. package/src/service/types.ts +0 -219
  31. package/src/service/work-item-create.ts +0 -159
  32. package/src/service/workspace-profile.ts +0 -443
  33. package/src/store/backup.ts +0 -239
  34. package/src/store/bootstrap.ts +0 -225
  35. package/src/store/db.ts +0 -179
  36. package/src/store/lock.ts +0 -194
  37. package/src/store/migrations.ts +0 -211
  38. package/src/store/schema/001_p1.sql +0 -846
  39. package/src/store/schema/001_p1.sql.ts +0 -865
  40. package/src/store/schema/002_knowledge.sql +0 -54
  41. package/src/store/schema/002_knowledge.sql.ts +0 -68
  42. package/src/store/schema/003_projects.sql.ts +0 -3
  43. package/src/store/schema/004_team.sql.ts +0 -50
  44. package/src/store/schema/005_target_check.sql.ts +0 -22
  45. package/src/store/schema/006_target_flow.sql.ts +0 -19
  46. package/src/store/schema/007_project_delete.sql.ts +0 -17
  47. package/src/store/schema/008_memory.sql.ts +0 -47
  48. package/src/store/schema/generate-projects.mjs +0 -16
  49. package/src/store/seed.ts +0 -114
@@ -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
- }