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
|
@@ -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
|
-
}
|
package/src/service/members.ts
DELETED
|
@@ -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
|
-
}
|