dsh-hams 0.1.3 → 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 (50) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/README.md +4 -0
  3. package/dist/index.js +11335 -0
  4. package/package.json +9 -5
  5. package/src/domain/check.ts +0 -193
  6. package/src/domain/errors.ts +0 -59
  7. package/src/domain/evidence.ts +0 -134
  8. package/src/domain/ids.ts +0 -31
  9. package/src/host/chat-runtime.ts +0 -489
  10. package/src/host/dsh-adapter.ts +0 -867
  11. package/src/host/session-adapter.ts +0 -276
  12. package/src/http/handler.ts +0 -1132
  13. package/src/index.js +0 -53
  14. package/src/service/audit.ts +0 -92
  15. package/src/service/chat.ts +0 -1100
  16. package/src/service/contracts.ts +0 -193
  17. package/src/service/defaults.ts +0 -182
  18. package/src/service/deliverables.ts +0 -240
  19. package/src/service/hams.ts +0 -3103
  20. package/src/service/knowledge.ts +0 -940
  21. package/src/service/member-config.ts +0 -39
  22. package/src/service/members.ts +0 -506
  23. package/src/service/memory.ts +0 -375
  24. package/src/service/permissions.ts +0 -118
  25. package/src/service/project-lifecycle.ts +0 -233
  26. package/src/service/projects.ts +0 -840
  27. package/src/service/receipts.ts +0 -63
  28. package/src/service/templates.ts +0 -140
  29. package/src/service/tick.ts +0 -751
  30. package/src/service/tools.ts +0 -711
  31. package/src/service/types.ts +0 -219
  32. package/src/service/work-item-create.ts +0 -159
  33. package/src/service/workspace-profile.ts +0 -443
  34. package/src/store/backup.ts +0 -239
  35. package/src/store/bootstrap.ts +0 -225
  36. package/src/store/db.ts +0 -179
  37. package/src/store/lock.ts +0 -194
  38. package/src/store/migrations.ts +0 -211
  39. package/src/store/schema/001_p1.sql +0 -846
  40. package/src/store/schema/001_p1.sql.ts +0 -865
  41. package/src/store/schema/002_knowledge.sql +0 -54
  42. package/src/store/schema/002_knowledge.sql.ts +0 -68
  43. package/src/store/schema/003_projects.sql.ts +0 -3
  44. package/src/store/schema/004_team.sql.ts +0 -50
  45. package/src/store/schema/005_target_check.sql.ts +0 -22
  46. package/src/store/schema/006_target_flow.sql.ts +0 -19
  47. package/src/store/schema/007_project_delete.sql.ts +0 -17
  48. package/src/store/schema/008_memory.sql.ts +0 -47
  49. package/src/store/schema/generate-projects.mjs +0 -16
  50. package/src/store/seed.ts +0 -114
@@ -1,39 +0,0 @@
1
- import type { DatabaseSync } from 'node:sqlite'
2
- import { createHash } from 'node:crypto'
3
- import type { IdFactory } from '../domain/ids.ts'
4
- import { stateInvalid } from '../domain/errors.ts'
5
- export interface PublishConfigInput {memberId:string;changeClass:'minor'|'major';responsibility?:string;agentPreset?:string|null;model?:string|null;memoryScope?:Record<string,unknown>}
6
- export function publishConfig(db:DatabaseSync,ids:IdFactory,now:number,actor:string,input:PublishConfigInput) {
7
- const member=db.prepare('SELECT * FROM member WHERE id=?').get(input.memberId) as Record<string,any>|undefined
8
- if(!member||member.kind!=='bot'||member.status==='retired') throw stateInvalid('只能为未退休bot发布配置')
9
- if(!['minor','major'].includes(input.changeClass)) throw stateInvalid('变更级别必须为minor或major')
10
- const previous=db.prepare('SELECT * FROM member_version WHERE member_id=? AND version=?').get(member.id,member.current_version) as Record<string,any>|undefined
11
- const string=(value:unknown,max:number):string|null=>{if(value===null)return null;if(typeof value!=='string'||value.length>max)throw stateInvalid('配置文本类型或长度不合法');return value.trim()||null}
12
- const model=input.model===undefined?previous?.model??null:string(input.model,120)
13
- const agentPreset=input.agentPreset===undefined?previous?.agent_preset??null:string(input.agentPreset,80)
14
- const responsibility=input.responsibility===undefined?member.responsibility:string(input.responsibility,4000)??''
15
- if(input.memoryScope!==undefined&&(!input.memoryScope||typeof input.memoryScope!=='object'||Array.isArray(input.memoryScope)||JSON.stringify(input.memoryScope).length>8000))throw stateInvalid('记忆范围必须为JSON对象且不超过8000字')
16
- const memory=input.memoryScope===undefined?previous?.memory_scope_json??'{}':JSON.stringify(input.memoryScope)
17
- // Changing execution model/preset/scope changes the run boundary and requires explicit major publication.
18
- if(input.changeClass==='minor' && (model!==(previous?.model??null)||agentPreset!==(previous?.agent_preset??null)||memory!==(previous?.memory_scope_json??'{}'))) throw stateInvalid('模型、preset或记忆范围变化需显式选择大改')
19
- const version=member.current_version+1,id=ids(),digest=createHash('sha256').update(JSON.stringify({responsibility,model,agentPreset,memory})).digest('hex')
20
- db.prepare('INSERT INTO member_version (id,member_id,version,agent_preset,model,memory_scope_json,prompt_digest,change_class,published_by,published_at) VALUES (?,?,?,?,?,?,?,?,?,?)').run(id,member.id,version,agentPreset,model,memory,digest,input.changeClass,actor,now)
21
- // Skills remain attached to each immutable version, even when only its prompt changes.
22
- if(previous) db.prepare('INSERT INTO member_version_skill (member_version_id,skill_id,skill_version_id) SELECT ?,skill_id,skill_version_id FROM member_version_skill WHERE member_version_id=?').run(id,previous.id)
23
- db.prepare('UPDATE member SET current_version=?,responsibility=?,updated_at=? WHERE id=?').run(version,responsibility,now,member.id)
24
- return {version,versionId:id,changeClass:input.changeClass,responsibility,model,agentPreset,memoryScope:JSON.parse(memory)}
25
- }
26
- export function memberActivity(db:DatabaseSync,now:number,memberId:string,timeZone='Asia/Shanghai') {
27
- const format=new Intl.DateTimeFormat('en-CA',{timeZone,year:'numeric',month:'2-digit',day:'2-digit'})
28
- const today=format.format(now), keys:string[]=[]
29
- // Calendar dates are generated at UTC noon to avoid local DST midnight gaps.
30
- const noon=Date.parse(today+'T12:00:00Z')
31
- for(let i=83;i>=0;i--)keys.push(new Date(noon-i*86400000).toISOString().slice(0,10))
32
- const buckets=new Map(keys.map(day=>[day,{day,count:0,runs:0,succeeded:0,failed:0}]))
33
- const runs=db.prepare('SELECT created_at,status FROM task_run WHERE member_id=? AND created_at>=? AND created_at<=?').all(memberId,now-85*86400000,now) as {created_at:number;status:string}[]
34
- for(const run of runs){const b=buckets.get(format.format(run.created_at));if(!b)continue;b.runs++;b.count++;if(run.status==='succeeded')b.succeeded++;if(run.status==='failed')b.failed++}
35
- const events=db.prepare('SELECT created_at FROM audit_log WHERE actor_id=? AND created_at>=? AND created_at<=?').all(memberId,now-85*86400000,now) as {created_at:number}[]
36
- for(const event of events){const b=buckets.get(format.format(event.created_at));if(b)b.count++}
37
- const days=[...buckets.values()]
38
- return {days,total:days.reduce((n,d)=>n+d.count,0),from:keys[0],to:keys[83],timeZone,definition:'count = task_run creations + audit actions; runs counts execution attempts'}
39
- }
@@ -1,506 +0,0 @@
1
- /**
2
- * 人机成员读模型与落库实现(M6)。
3
- *
4
- * 与 `permissions.ts` 的分工:这里只回答「这个成员是谁、在什么岗位、有哪些能力、
5
- * 手上还有多少活」,不参与「当前请求的主体能不能做某事」——后者仍由
6
- * `resolvePrincipal` + `requireCapability` 决定,避免读模型顺手把权限放出去。
7
- *
8
- * 两条硬规则(schema 触发器 + backend-design §4):
9
- * - 岗位只能给人(`trg_member_position_human_only`);人的能力只来自在岗岗位的并集;
10
- * - bot 的能力只来自 `member_permission`,审批/发布类能力由库触发器拒绝授予。
11
- */
12
- import type { DatabaseSync } from 'node:sqlite'
13
- import { createHash } from 'node:crypto'
14
- import { stateInvalid } from '../domain/errors.ts'
15
- import type { IdFactory } from '../domain/ids.ts'
16
- import { CAPABILITY } from './permissions.ts'
17
-
18
- export const MEMBER_KINDS = ['human', 'bot'] as const
19
- export const MEMBER_ROLES = ['coordinator', 'executor', 'observer'] as const
20
- export const MEMBER_STATUSES = ['active', 'paused', 'retired'] as const
21
-
22
- export interface MemberRow {
23
- id: string
24
- kind: string
25
- name: string
26
- domain: string | null
27
- responsibility: string
28
- role: string
29
- status: string
30
- current_version: number
31
- owner_human_id: string | null
32
- created_at: number
33
- updated_at: number
34
- }
35
-
36
- export interface MemberSummary {
37
- id: string
38
- kind: string
39
- name: string
40
- domain: string | null
41
- responsibility: string
42
- role: string
43
- status: string
44
- current_version: number
45
- owner_human_id: string | null
46
- owner_name: string | null
47
- created_at: number
48
- updated_at: number
49
- positions: { id: string; name: string }[]
50
- capabilities: string[]
51
- work: { total: number; open: number; byStatus: Record<string, number> }
52
- runs: { total: number; last_at: number | null }
53
- }
54
-
55
- export interface MemberDetail extends MemberSummary {
56
- position_history: {
57
- position_id: string
58
- position_name: string
59
- starts_at: number
60
- ends_at: number | null
61
- assigned_by: string
62
- active: boolean
63
- }[]
64
- permissions: { capability: string; scope: string; granted_by: string; granted_at: number }[]
65
- recent_work: {
66
- id: string
67
- title: string
68
- status: string
69
- project_id: string | null
70
- owner_human_id: string
71
- assignee_id: string | null
72
- due_at: number | null
73
- next_action_at: number | null
74
- updated_at: number
75
- }[]
76
- recent_runs: {
77
- id: string
78
- work_item_id: string
79
- status: string
80
- trigger: string
81
- started_at: number | null
82
- finished_at: number | null
83
- created_at: number
84
- }[]
85
- bot_versions: {
86
- id: string
87
- version: number
88
- model: string | null
89
- agent_preset: string | null
90
- change_class: string
91
- published_by: string
92
- published_at: number
93
- }[]
94
- login: {
95
- id: string
96
- login: string
97
- display_name: string
98
- auth_source: string
99
- is_admin: number
100
- status: string
101
- last_login_at: number | null
102
- } | null
103
- }
104
-
105
- export interface PositionSummary {
106
- id: string
107
- name: string
108
- domain: string | null
109
- description: string
110
- status: string
111
- capabilities: { capability: string; scope: string }[]
112
- holders: number
113
- }
114
-
115
- const MAX_NAME = 60
116
- const MAX_TEXT = 500
117
-
118
- function requiredText(value: unknown, field: string, max = MAX_NAME): string {
119
- if (typeof value !== 'string') throw stateInvalid(`${field} 必须是字符串`)
120
- const trimmed = value.trim()
121
- if (!trimmed) throw stateInvalid(`${field} 不能为空`)
122
- if (trimmed.length > max) throw stateInvalid(`${field} 超过 ${max} 字`)
123
- return trimmed
124
- }
125
-
126
- function optionalText(value: unknown, field: string, max = MAX_TEXT): string | null {
127
- if (value === undefined || value === null) return null
128
- if (typeof value !== 'string') throw stateInvalid(`${field} 必须是字符串`)
129
- const trimmed = value.trim()
130
- if (!trimmed) return null
131
- if (trimmed.length > max) throw stateInvalid(`${field} 超过 ${max} 字`)
132
- return trimmed
133
- }
134
-
135
- function oneOf<T extends string>(value: unknown, field: string, allowed: readonly T[], fallback?: T): T {
136
- if (value === undefined && fallback !== undefined) return fallback
137
- if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {
138
- throw stateInvalid(`${field} 只能是 ${allowed.join(' / ')}`)
139
- }
140
- return value as T
141
- }
142
-
143
- function capabilitiesOf(db: DatabaseSync, memberId: string, kind: string, now: number): string[] {
144
- if (kind === 'bot') {
145
- return (db.prepare('SELECT capability FROM member_permission WHERE member_id = ?').all(memberId) as {
146
- capability: string
147
- }[]).map((row) => row.capability)
148
- }
149
- if (kind !== 'human') return []
150
- return (
151
- db
152
- .prepare(
153
- `SELECT DISTINCT pp.capability AS capability
154
- FROM member_position mp
155
- JOIN position p ON p.id = mp.position_id AND p.status = 'active'
156
- JOIN position_permission pp ON pp.position_id = mp.position_id
157
- WHERE mp.member_id = ? AND mp.starts_at <= ? AND (mp.ends_at IS NULL OR mp.ends_at > ?)`,
158
- )
159
- .all(memberId, now, now) as { capability: string }[]
160
- ).map((row) => row.capability)
161
- }
162
-
163
- function workStats(db: DatabaseSync, memberId: string): MemberSummary['work'] {
164
- const rows = db
165
- .prepare(
166
- `SELECT status, COUNT(*) AS c FROM work_item
167
- WHERE owner_human_id = ? OR assignee_id = ?
168
- GROUP BY status`,
169
- )
170
- .all(memberId, memberId) as { status: string; c: number }[]
171
- const byStatus: Record<string, number> = {}
172
- let total = 0
173
- for (const row of rows) {
174
- byStatus[row.status] = Number(row.c)
175
- total += Number(row.c)
176
- }
177
- const closed = (byStatus.completed ?? 0) + (byStatus.cancelled ?? 0)
178
- return { total, open: total - closed, byStatus }
179
- }
180
-
181
- function positionsOf(db: DatabaseSync, memberId: string): { id: string; name: string }[] {
182
- return db
183
- .prepare(
184
- `SELECT DISTINCT p.id, p.name FROM member_position mp
185
- JOIN position p ON p.id = mp.position_id
186
- WHERE mp.member_id = ? AND mp.ends_at IS NULL
187
- ORDER BY p.name`,
188
- )
189
- .all(memberId) as { id: string; name: string }[]
190
- }
191
-
192
- export function listMembers(
193
- db: DatabaseSync,
194
- now: number,
195
- filter: { kind?: string; status?: string; projectId?: string; limit?: number } = {},
196
- ): MemberSummary[] {
197
- const clauses: string[] = ["m.kind <> 'system'"]
198
- const params: unknown[] = []
199
- // 临时 bot 只属于某个项目群:默认不出现在全局成员列表,按 projectId 查时才带出来。
200
- if (filter.projectId) {
201
- clauses.push('m.scope_project_id = ?')
202
- params.push(filter.projectId)
203
- } else {
204
- clauses.push('m.scope_project_id IS NULL')
205
- }
206
- if (filter.kind) {
207
- clauses.push('m.kind = ?')
208
- params.push(filter.kind)
209
- }
210
- if (filter.status) {
211
- clauses.push('m.status = ?')
212
- params.push(filter.status)
213
- }
214
- const limit = Math.min(Math.max(filter.limit ?? 100, 1), 200)
215
- const rows = db
216
- .prepare(
217
- `SELECT m.*, o.name AS owner_name,
218
- (SELECT COUNT(*) FROM task_run r WHERE r.member_id = m.id) AS run_total,
219
- (SELECT MAX(r.created_at) FROM task_run r WHERE r.member_id = m.id) AS last_run_at
220
- FROM member m
221
- LEFT JOIN member o ON o.id = m.owner_human_id
222
- WHERE ${clauses.join(' AND ')}
223
- ORDER BY CASE m.kind WHEN 'bot' THEN 1 ELSE 0 END, m.status, m.name
224
- LIMIT ${limit}`,
225
- )
226
- .all(...(params as never[])) as (MemberRow & { owner_name: string | null; run_total: number; last_run_at: number | null })[]
227
-
228
- return rows.map((row) => ({
229
- id: row.id,
230
- kind: row.kind,
231
- name: row.name,
232
- domain: row.domain,
233
- responsibility: row.responsibility,
234
- role: row.role,
235
- status: row.status,
236
- current_version: Number(row.current_version),
237
- owner_human_id: row.owner_human_id,
238
- owner_name: row.owner_name,
239
- created_at: Number(row.created_at),
240
- updated_at: Number(row.updated_at),
241
- positions: positionsOf(db, row.id),
242
- // 停用 / 退休的主体按 resolvePrincipal 的口径不再拥有任何能力(界面显示为空)。
243
- capabilities: row.status === 'active' ? capabilitiesOf(db, row.id, row.kind, now).sort() : [],
244
- work: workStats(db, row.id),
245
- runs: { total: Number(row.run_total), last_at: row.last_run_at === null ? null : Number(row.last_run_at) },
246
- }))
247
- }
248
-
249
- export function getMember(db: DatabaseSync, now: number, memberId: string): MemberDetail {
250
- const summary = listMembers(db, now, { limit: 200 }).find((item) => item.id === memberId)
251
- if (!summary) throw stateInvalid(`成员不存在:${memberId}`)
252
-
253
- const positionHistory = db
254
- .prepare(
255
- `SELECT mp.position_id, p.name AS position_name, mp.starts_at, mp.ends_at, mp.assigned_by
256
- FROM member_position mp JOIN position p ON p.id = mp.position_id
257
- WHERE mp.member_id = ?
258
- ORDER BY mp.starts_at DESC`,
259
- )
260
- .all(memberId) as {
261
- position_id: string
262
- position_name: string
263
- starts_at: number
264
- ends_at: number | null
265
- assigned_by: string
266
- }[]
267
-
268
- const permissions = db
269
- .prepare(
270
- `SELECT capability, scope, granted_by, granted_at FROM member_permission
271
- WHERE member_id = ? ORDER BY capability, scope`,
272
- )
273
- .all(memberId) as MemberDetail['permissions']
274
-
275
- const recentWork = db
276
- .prepare(
277
- `SELECT id, title, status, project_id, owner_human_id, assignee_id, due_at, next_action_at, updated_at
278
- FROM work_item WHERE owner_human_id = ? OR assignee_id = ?
279
- ORDER BY updated_at DESC LIMIT 10`,
280
- )
281
- .all(memberId, memberId) as MemberDetail['recent_work']
282
-
283
- const recentRuns = db
284
- .prepare(
285
- `SELECT id, work_item_id, status, trigger, started_at, finished_at, created_at
286
- FROM task_run WHERE member_id = ?
287
- ORDER BY created_at DESC LIMIT 10`,
288
- )
289
- .all(memberId) as MemberDetail['recent_runs']
290
-
291
- const botVersions = db
292
- .prepare(
293
- `SELECT id, version, model, agent_preset, change_class, published_by, published_at
294
- FROM member_version WHERE member_id = ?
295
- ORDER BY version DESC LIMIT 10`,
296
- )
297
- .all(memberId) as MemberDetail['bot_versions']
298
-
299
- const login = (db
300
- .prepare(
301
- `SELECT id, login, display_name, auth_source, is_admin, status, last_login_at
302
- FROM hams_user WHERE member_id = ? LIMIT 1`,
303
- )
304
- .get(memberId) as MemberDetail['login']) ?? null
305
-
306
- return {
307
- ...summary,
308
- position_history: positionHistory.map((item) => ({
309
- ...item,
310
- starts_at: Number(item.starts_at),
311
- ends_at: item.ends_at === null ? null : Number(item.ends_at),
312
- active: item.ends_at === null,
313
- })),
314
- permissions: permissions.map((item) => ({ ...item, granted_at: Number(item.granted_at) })),
315
- recent_work: recentWork.map((item) => ({
316
- ...item,
317
- due_at: item.due_at === null ? null : Number(item.due_at),
318
- next_action_at: item.next_action_at === null ? null : Number(item.next_action_at),
319
- updated_at: Number(item.updated_at),
320
- })),
321
- recent_runs: recentRuns.map((item) => ({
322
- ...item,
323
- started_at: item.started_at === null ? null : Number(item.started_at),
324
- finished_at: item.finished_at === null ? null : Number(item.finished_at),
325
- created_at: Number(item.created_at),
326
- })),
327
- bot_versions: botVersions.map((item) => ({ ...item, version: Number(item.version), published_at: Number(item.published_at) })),
328
- login,
329
- }
330
- }
331
-
332
- export function listPositions(db: DatabaseSync): PositionSummary[] {
333
- const positions = db
334
- .prepare('SELECT id, name, domain, description, status FROM position ORDER BY name')
335
- .all() as { id: string; name: string; domain: string | null; description: string; status: string }[]
336
- return positions.map((position) => ({
337
- ...position,
338
- capabilities: db
339
- .prepare('SELECT capability, scope FROM position_permission WHERE position_id = ? ORDER BY capability, scope')
340
- .all(position.id) as { capability: string; scope: string }[],
341
- holders: Number(
342
- (
343
- db
344
- .prepare(
345
- `SELECT COUNT(*) AS c FROM member_position
346
- WHERE position_id = ? AND ends_at IS NULL`,
347
- )
348
- .get(position.id) as { c: number }
349
- ).c,
350
- ),
351
- }))
352
- }
353
-
354
- export interface CreateMemberInput {
355
- kind: string
356
- name: string
357
- role?: string
358
- domain?: string | null
359
- responsibility?: string | null
360
- positionId?: string | null
361
- ownerHumanId?: string | null
362
- agentPreset?: string | null
363
- model?: string | null
364
- capabilities?: string[]
365
- scope?: string | null
366
- }
367
-
368
- export interface InsertedMember {
369
- id: string
370
- kind: string
371
- version: number
372
- capabilities: string[]
373
- }
374
-
375
- /** bot 可以拿到的能力白名单:工具通道 + 业务动作,不含审批/发布/管理。 */
376
- export const BOT_GRANTABLE_CAPABILITIES = [
377
- CAPABILITY.workItemClaim,
378
- CAPABILITY.workItemExecute,
379
- CAPABILITY.workItemRequestClosure,
380
- CAPABILITY.evidenceAppend,
381
- CAPABILITY.checkEvaluate,
382
- CAPABILITY.proposalCreate,
383
- CAPABILITY.connectorIngest,
384
- CAPABILITY.metricRecord,
385
- CAPABILITY.knowledgeWrite,
386
- ] as const
387
-
388
- export function insertMember(
389
- db: DatabaseSync,
390
- ids: IdFactory,
391
- now: number,
392
- actorId: string,
393
- input: CreateMemberInput,
394
- ): InsertedMember {
395
- const kind = oneOf(input.kind, '成员类型', MEMBER_KINDS)
396
- const name = requiredText(input.name, '姓名')
397
- const role = oneOf(input.role, '角色', MEMBER_ROLES, 'executor')
398
- const domain = optionalText(input.domain, '业务域', 40)
399
- const responsibility = optionalText(input.responsibility, '职责', MAX_TEXT) ?? ''
400
- const duplicate = db.prepare('SELECT id FROM member WHERE name = ?').get(name) as { id: string } | undefined
401
- if (duplicate) throw stateInvalid(`成员名已存在:${name}`, '换一个名字,或打开已有成员')
402
-
403
- const id = ids()
404
- if (kind === 'human') {
405
- const positionId = requiredText(input.positionId, '岗位')
406
- const position = db.prepare("SELECT id, status FROM position WHERE id = ?").get(positionId) as
407
- | { id: string; status: string }
408
- | undefined
409
- if (!position) throw stateInvalid(`岗位不存在:${positionId}`, '先在岗位列表里选一个')
410
- if (position.status !== 'active') throw stateInvalid(`岗位已停用:${positionId}`)
411
- db.prepare(
412
- `INSERT INTO member (id, kind, name, domain, responsibility, role, status, current_version,
413
- owner_human_id, created_at, updated_at)
414
- VALUES (?, 'human', ?, ?, ?, ?, 'active', 0, NULL, ?, ?)`,
415
- ).run(id, name, domain, responsibility, role, now, now)
416
- db.prepare(
417
- `INSERT INTO member_position (member_id, position_id, starts_at, ends_at, assigned_by)
418
- VALUES (?, ?, ?, NULL, ?)`,
419
- ).run(id, positionId, now, actorId)
420
- return { id, kind, version: 0, capabilities: capabilitiesOf(db, id, kind, now).sort() }
421
- }
422
-
423
- // bot:配置快照发布即不可变;责任人必须是存在的人。
424
- const ownerId = optionalText(input.ownerHumanId, '责任人', 64) ?? actorId
425
- const owner = db.prepare('SELECT id, kind FROM member WHERE id = ?').get(ownerId) as
426
- | { id: string; kind: string }
427
- | undefined
428
- if (!owner || owner.kind !== 'human') throw stateInvalid('bot 的责任人必须是存在的人')
429
- const agentPreset = optionalText(input.agentPreset, 'agent preset', 80)
430
- const model = optionalText(input.model, '模型', 120)
431
- const granted = [...new Set(Array.isArray(input.capabilities) ? input.capabilities : [])]
432
- for (const capability of granted) {
433
- if (!(BOT_GRANTABLE_CAPABILITIES as readonly string[]).includes(capability)) {
434
- throw stateInvalid(`bot 不能获得能力:${capability}`, '审批、发布、成员管理只能给人')
435
- }
436
- }
437
- const scope = optionalText(input.scope, '授权范围', 80) ?? 'global'
438
- if (!/^(global|domain:[^:]+|project:[^:]+)$/.test(scope)) {
439
- throw stateInvalid('授权范围只能是 global / domain:<域> / project:<id>')
440
- }
441
- const promptDigest = createHash('sha256')
442
- .update(JSON.stringify({ name, responsibility, agentPreset, model }))
443
- .digest('hex')
444
-
445
- db.prepare(
446
- `INSERT INTO member (id, kind, name, domain, responsibility, role, status, current_version,
447
- owner_human_id, created_at, updated_at)
448
- VALUES (?, 'bot', ?, ?, ?, ?, 'active', 1, ?, ?, ?)`,
449
- ).run(id, name, domain, responsibility, role, ownerId, now, now)
450
- db.prepare(
451
- `INSERT INTO member_version
452
- (id, member_id, version, agent_preset, model, memory_scope_json, prompt_digest,
453
- change_class, published_by, published_at)
454
- VALUES (?, ?, 1, ?, ?, '{}', ?, 'major', ?, ?)`,
455
- ).run(ids(), id, agentPreset, model, promptDigest, actorId, now)
456
- const grant = db.prepare(
457
- `INSERT INTO member_permission (member_id, capability, scope, granted_by, granted_at)
458
- VALUES (?, ?, ?, ?, ?)`,
459
- )
460
- for (const capability of [...granted].sort()) grant.run(id, capability, scope, actorId, now)
461
- return { id, kind, version: 1, capabilities: capabilitiesOf(db, id, kind, now).sort() }
462
- }
463
-
464
- export interface MemberStatusChange {
465
- before: string
466
- after: string
467
- closedPositions: number
468
- }
469
-
470
- export function setMemberStatus(
471
- db: DatabaseSync,
472
- now: number,
473
- actorId: string,
474
- memberId: string,
475
- to: string,
476
- ): MemberStatusChange {
477
- const row = db.prepare('SELECT * FROM member WHERE id = ?').get(memberId) as MemberRow | undefined
478
- if (!row) throw stateInvalid(`成员不存在:${memberId}`)
479
- if (row.kind === 'system') throw stateInvalid('系统主体不能停用')
480
- if (memberId === actorId) throw stateInvalid('不能改变自己的状态', '请由另一名有成员管理权限的人操作')
481
- const target = oneOf(to, '成员状态', MEMBER_STATUSES)
482
- const before = row.status
483
- if (before === target) return { before, after: target, closedPositions: 0 }
484
-
485
- const allowed: Record<string, string[]> = {
486
- active: ['paused', 'retired'],
487
- paused: ['active', 'retired'],
488
- retired: ['active'],
489
- }
490
- if (!allowed[before]?.includes(target)) {
491
- throw stateInvalid(`成员状态不能从 ${before} 改为 ${target}`)
492
- }
493
- db.prepare('UPDATE member SET status = ?, updated_at = ? WHERE id = ?').run(target, now, memberId)
494
- let closed = 0
495
- if (target === 'retired' && row.kind === 'human') {
496
- const result = db
497
- .prepare(
498
- `UPDATE member_position
499
- SET ends_at = CASE WHEN starts_at >= ? THEN starts_at + 1 ELSE ? END
500
- WHERE member_id = ? AND ends_at IS NULL`,
501
- )
502
- .run(now, now, memberId)
503
- closed = Number(result.changes ?? 0)
504
- }
505
- return { before, after: target, closedPositions: closed }
506
- }