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,276 +0,0 @@
|
|
|
1
|
-
/** Real DSH agent lifecycle. Business rows only hold host session references; no transcript IO. */
|
|
2
|
-
import { randomUUID } from 'node:crypto'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { mkdir } from 'node:fs/promises'
|
|
5
|
-
import type { DatabaseSync } from 'node:sqlite'
|
|
6
|
-
import { HamsError } from '../domain/errors.ts'
|
|
7
|
-
import type { HamsService } from '../service/hams.ts'
|
|
8
|
-
import type { CommandContext, CommandReceipt, RunRow, ThreadRow, WorkItemRow } from '../service/types.ts'
|
|
9
|
-
import { CAPABILITY, requireCapability, requireHuman, resolvePrincipal } from '../service/permissions.ts'
|
|
10
|
-
import { TOOL_NAMES } from '../service/tools.ts'
|
|
11
|
-
|
|
12
|
-
export interface HostAgent {
|
|
13
|
-
session: { id: string }
|
|
14
|
-
status?: string
|
|
15
|
-
followup(message: unknown): void
|
|
16
|
-
cancel(cause: { kind: 'user' }, options: { keepInbox: true }): unknown
|
|
17
|
-
}
|
|
18
|
-
export interface HostAgentHandle { agent: HostAgent; dispose(): unknown | Promise<unknown> }
|
|
19
|
-
export interface HostAgents {
|
|
20
|
-
create(options: { sessionId: string; meta: Record<string, unknown>; agentOptions: Record<string, unknown>; setup?: unknown }): Promise<HostAgentHandle>
|
|
21
|
-
resume(options: { resumeSessionId: string; agentOptions: Record<string, unknown>; setup?: unknown }): Promise<HostAgentHandle>
|
|
22
|
-
}
|
|
23
|
-
export interface BotConfig { memberId: string; version: number; agentPreset: string; model: string | null }
|
|
24
|
-
export interface SessionAdapterOptions {
|
|
25
|
-
db: DatabaseSync
|
|
26
|
-
service: HamsService
|
|
27
|
-
agents: HostAgents
|
|
28
|
-
dataDir: string
|
|
29
|
-
/** SDK createUserMessage, injected so tests never import/start the host runtime. */
|
|
30
|
-
createUserMessage(input: { content: { type: 'text'; text: string }[]; source: { kind: 'user' } }): unknown
|
|
31
|
-
/** Must resolve/mount the named preset, installModelSelection, and bind configured model. */
|
|
32
|
-
compose(config: BotConfig): Promise<{ agentOptions: Record<string, unknown>; setup?: unknown; agentPreset?: string | null }>
|
|
33
|
-
/** 成员配置里没写 preset 时的兜底(HAMS 自带 hams-bot)。 */
|
|
34
|
-
defaultPreset?: string
|
|
35
|
-
now?: () => number
|
|
36
|
-
timeZone?: string
|
|
37
|
-
sessionId?: () => string
|
|
38
|
-
}
|
|
39
|
-
export interface RunFacts {
|
|
40
|
-
stateAndEpoch: unknown
|
|
41
|
-
evidenceAndHashes: unknown
|
|
42
|
-
taskAndCriteria: unknown
|
|
43
|
-
approvalsAndBlockers: unknown
|
|
44
|
-
budgetAndCapabilities: unknown
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Only the SDK-owned Agent session is trusted. Flat sessionId and model arguments are ignored. */
|
|
48
|
-
export function hostToolContext(exec: unknown): { sessionId: string; callId?: string } {
|
|
49
|
-
const value = exec as { agent?: { session?: { id?: unknown } }; callId?: unknown } | undefined
|
|
50
|
-
const id = value?.agent?.session?.id
|
|
51
|
-
return { sessionId: typeof id === 'string' ? id : '', ...(typeof value?.callId === 'string' ? { callId: value.callId } : {}) }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function buildRunFacts(db: DatabaseSync, runId: string, now = Date.now(), timeZone = 'Asia/Shanghai'): RunFacts {
|
|
55
|
-
const run = db.prepare('SELECT * FROM task_run WHERE id=?').get(runId) as RunRow | undefined
|
|
56
|
-
if (!run) throw new HamsError('STATE_INVALID', `运行不存在:${runId}`)
|
|
57
|
-
const item = db.prepare('SELECT * FROM work_item WHERE id=?').get(run.work_item_id) as WorkItemRow
|
|
58
|
-
const template = item.template_version_id ? db.prepare('SELECT * FROM work_template_version WHERE id=?').get(item.template_version_id) : null
|
|
59
|
-
const date = new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(now))
|
|
60
|
-
const utc = new Date(`${date}T00:00:00Z`)
|
|
61
|
-
utc.setUTCDate(utc.getUTCDate() - ((utc.getUTCDay() + 6) % 7))
|
|
62
|
-
const week = utc.toISOString().slice(0, 10)
|
|
63
|
-
const budgets = item.project_id ? db.prepare(`SELECT period_kind,period_start,metric,used,limit_value,
|
|
64
|
-
MAX(0,limit_value-used) AS remaining FROM project_budget WHERE project_id=?
|
|
65
|
-
AND ((period_kind='day' AND period_start=?) OR (period_kind='week' AND period_start=?)) ORDER BY period_kind,metric`).all(item.project_id, date, week) : []
|
|
66
|
-
return {
|
|
67
|
-
stateAndEpoch: { workItemId: item.id, status: item.status, revision: item.revision, executionEpoch: item.execution_epoch, fence: run.fence, runtimeEpoch: run.runtime_epoch },
|
|
68
|
-
evidenceAndHashes: { evidenceSetHash: item.evidence_set_hash, evidence: db.prepare("SELECT id,kind,ref,content_hash,observed_at,source FROM evidence WHERE work_item_id=? AND status='active' ORDER BY id").all(item.id) },
|
|
69
|
-
taskAndCriteria: { runId, trigger: run.trigger, checkId: run.check_id, title: item.title, memberVersion: run.member_version, templateVersion: template ?? null },
|
|
70
|
-
approvalsAndBlockers: { blockedReason: item.blocked_reason, approvals: db.prepare("SELECT id,status,reason_text,expires_at FROM approval WHERE subject_kind='work_item_close' AND subject_id=? AND status='pending' ORDER BY id").all(item.id) },
|
|
71
|
-
budgetAndCapabilities: {
|
|
72
|
-
budgets,
|
|
73
|
-
permissions: db.prepare('SELECT capability,scope FROM member_permission WHERE member_id=? ORDER BY capability,scope').all(run.member_id),
|
|
74
|
-
// 政策白名单为空 ≠ 没有工具:HAMS 工具是全局注册的,真实可用。把两者分开写,避免 bot 误判「tools 为空所以干不了」。
|
|
75
|
-
toolPolicy: db.prepare('SELECT tool_name FROM member_tool_policy WHERE member_id=? AND enabled=1 ORDER BY tool_name').all(run.member_id),
|
|
76
|
-
hamsTools: [...TOOL_NAMES],
|
|
77
|
-
toolPolicyNote: 'toolPolicy 是额外白名单;hamsTools 一定可用。宿主另有全局工具(如 working_directory)。',
|
|
78
|
-
},
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export class HamsSessionAdapter {
|
|
83
|
-
private options: SessionAdapterOptions
|
|
84
|
-
private handles = new Map<string, Promise<HostAgentHandle>>()
|
|
85
|
-
private locks = new Map<string, Promise<unknown>>()
|
|
86
|
-
private delivered = new Set<string>()
|
|
87
|
-
private closed = false
|
|
88
|
-
constructor(options: SessionAdapterOptions) { this.options = options }
|
|
89
|
-
|
|
90
|
-
private serial<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
91
|
-
const next = (this.locks.get(key) ?? Promise.resolve()).catch(() => {}).then(() => {
|
|
92
|
-
if (this.closed) throw new HamsError('HOST_CAPABILITY_MISSING', '会话适配器已停止')
|
|
93
|
-
return fn()
|
|
94
|
-
})
|
|
95
|
-
this.locks.set(key, next)
|
|
96
|
-
void next.finally(() => { if (this.locks.get(key) === next) this.locks.delete(key) }).catch(() => {})
|
|
97
|
-
return next
|
|
98
|
-
}
|
|
99
|
-
private thread(workItemId: string): ThreadRow | undefined {
|
|
100
|
-
return this.options.db.prepare("SELECT * FROM work_item_thread WHERE work_item_id=? AND status IN ('active','pending_rotate') ORDER BY generation DESC LIMIT 1").get(workItemId) as ThreadRow | undefined
|
|
101
|
-
}
|
|
102
|
-
private config(memberId: string, version?: number): BotConfig {
|
|
103
|
-
const row = this.options.db.prepare(`SELECT v.*,m.status,m.kind FROM member_version v JOIN member m ON m.id=v.member_id WHERE v.member_id=? AND v.version=${version === undefined ? 'm.current_version' : '?'}`).get(...(version === undefined ? [memberId] : [memberId, version])) as { version: number; agent_preset: string; model: string | null; kind: string; status: string } | undefined
|
|
104
|
-
if (!row || row.kind !== 'bot' || row.status !== 'active') throw new HamsError('HOST_CAPABILITY_MISSING', 'bot 没有有效且已发布的宿主 preset')
|
|
105
|
-
return { memberId, version: row.version, agentPreset: row.agent_preset || this.options.defaultPreset || '', model: row.model }
|
|
106
|
-
}
|
|
107
|
-
private needsRotation(thread: ThreadRow): boolean {
|
|
108
|
-
return thread.status === 'pending_rotate' || Boolean(this.options.db.prepare("SELECT 1 FROM member_version WHERE member_id=? AND version>? AND change_class='major'").get(thread.member_id, thread.member_version))
|
|
109
|
-
}
|
|
110
|
-
private async open(config: BotConfig, existing?: string, cwdKey?: string): Promise<HostAgentHandle> {
|
|
111
|
-
if (existing && this.handles.has(existing)) return this.handles.get(existing)!
|
|
112
|
-
const id = existing ?? (this.options.sessionId ?? (() => `session-${randomUUID()}`))()
|
|
113
|
-
if (!id.trim() || this.handles.has(id)) throw new HamsError('STATE_INVALID', '宿主会话 id 重复或为空')
|
|
114
|
-
const pending = (async () => {
|
|
115
|
-
const composition = await this.options.compose(config)
|
|
116
|
-
const { agentPreset: composedPreset, ...rest } = composition
|
|
117
|
-
const presetId = composedPreset ?? config.agentPreset
|
|
118
|
-
// 一个 bot 可能同时是多个工作项的负责人:工作项各占一个子目录,不和别的任务混在一起。
|
|
119
|
-
const cwd = path.resolve(this.options.dataDir, 'bots', config.memberId, 'work', String(cwdKey || 'default').replace(/[^a-zA-Z0-9._-]+/g, '-') || 'default')
|
|
120
|
-
const base = path.resolve(this.options.dataDir, 'bots') + path.sep
|
|
121
|
-
if (!cwd.startsWith(base)) throw new HamsError('POLICY_DENIED', 'bot 工作目录越界')
|
|
122
|
-
await mkdir(cwd, { recursive: true })
|
|
123
|
-
const handle = existing
|
|
124
|
-
? await this.options.agents.resume({ resumeSessionId: id, ...rest })
|
|
125
|
-
: await this.options.agents.create({ sessionId: id, meta: { cwd, ...(presetId ? { agentPreset: presetId } : {}) }, ...rest })
|
|
126
|
-
if (handle.agent.session.id !== id) { await handle.dispose(); throw new HamsError('HOST_CAPABILITY_MISSING', '宿主返回的会话身份与请求不一致') }
|
|
127
|
-
return handle
|
|
128
|
-
})()
|
|
129
|
-
this.handles.set(id, pending)
|
|
130
|
-
try { return await pending } catch (error) { this.handles.delete(id); throw error }
|
|
131
|
-
}
|
|
132
|
-
async release(sessionId: string): Promise<void> {
|
|
133
|
-
const pending = this.handles.get(sessionId)
|
|
134
|
-
if (!pending) return
|
|
135
|
-
// Keep the entry until disposal completes: another open must not race the writer teardown.
|
|
136
|
-
try { await (await pending).dispose() } finally { this.handles.delete(sessionId) }
|
|
137
|
-
}
|
|
138
|
-
async cancel(sessionId: string): Promise<{ accepted: true }> {
|
|
139
|
-
const thread = this.options.db.prepare('SELECT * FROM work_item_thread WHERE session_id=?').get(sessionId) as ThreadRow | undefined
|
|
140
|
-
if (!thread) throw new HamsError('POLICY_DENIED', '会话未绑定 HAMS 工作项')
|
|
141
|
-
const handle = await this.open(this.config(thread.member_id, thread.member_version), sessionId, thread.work_item_id)
|
|
142
|
-
await handle.agent.cancel({ kind: 'user' }, { keepInbox: true })
|
|
143
|
-
return { accepted: true }
|
|
144
|
-
}
|
|
145
|
-
async claim(ctx: CommandContext, input: { workItemId: string }): Promise<CommandReceipt> {
|
|
146
|
-
return this.serial(input.workItemId, async () => {
|
|
147
|
-
const { service } = this.options
|
|
148
|
-
const item = service.getWorkItem(ctx, input.workItemId)
|
|
149
|
-
if (item.status !== 'ready') throw new HamsError('STATE_INVALID', '只有 ready 的工作项可以认领')
|
|
150
|
-
const principal = resolvePrincipal(this.options.db, ctx.principalId, this.now())
|
|
151
|
-
// 系统派发替被指派的 bot 认领:execute 就够(与 service.claim 的 system 分支一致)。
|
|
152
|
-
if (ctx.channel === 'system' && !principal.capabilities.has(CAPABILITY.workItemClaim)) {
|
|
153
|
-
requireCapability(principal, CAPABILITY.workItemExecute)
|
|
154
|
-
} else {
|
|
155
|
-
requireCapability(principal, CAPABILITY.workItemClaim)
|
|
156
|
-
}
|
|
157
|
-
const thread = this.thread(item.id)
|
|
158
|
-
const fresh = !thread || this.needsRotation(thread) || thread.member_id !== ctx.principalId
|
|
159
|
-
const handle = await this.open(this.config(ctx.principalId, fresh ? undefined : thread!.member_version), fresh ? undefined : thread!.session_id, item.id)
|
|
160
|
-
try {
|
|
161
|
-
const receipt = service.claim(ctx, { workItemId: item.id, sessionId: handle.agent.session.id })
|
|
162
|
-
if (thread && thread.session_id !== handle.agent.session.id) await this.release(thread.session_id)
|
|
163
|
-
return receipt
|
|
164
|
-
} catch (error) { if (fresh) await this.release(handle.agent.session.id); throw error }
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
async startRun(runId: string, text = ''): Promise<{ sessionId: string; runId: string; accepted: true }> {
|
|
168
|
-
const run = this.options.db.prepare('SELECT * FROM task_run WHERE id=?').get(runId) as RunRow | undefined
|
|
169
|
-
if (!run) throw new HamsError('STATE_INVALID', `运行不存在:${runId}`)
|
|
170
|
-
return this.serial(run.work_item_id, () => this.start(runId, text))
|
|
171
|
-
}
|
|
172
|
-
private async start(runId: string, text: string) {
|
|
173
|
-
const { db, service } = this.options
|
|
174
|
-
const run = db.prepare('SELECT * FROM task_run WHERE id=?').get(runId) as RunRow
|
|
175
|
-
let thread = db.prepare('SELECT * FROM work_item_thread WHERE id=?').get(run.thread_id) as ThreadRow
|
|
176
|
-
if (this.delivered.has(runId)) return { sessionId: thread.session_id, runId, accepted: true as const }
|
|
177
|
-
if (run.status !== 'queued') throw new HamsError('STATE_INVALID', '只能投递 queued 运行;结果不明时不自动重发')
|
|
178
|
-
const rotate = this.needsRotation(thread)
|
|
179
|
-
const oldId = thread.session_id
|
|
180
|
-
const handle = await this.open(this.config(run.member_id, rotate ? undefined : thread.member_version), rotate ? undefined : oldId, run.work_item_id)
|
|
181
|
-
try {
|
|
182
|
-
service.startRun({ principalId: run.member_id, channel: 'system', correlationId: `host-start:${runId}` }, { runId, sessionId: handle.agent.session.id })
|
|
183
|
-
thread = this.thread(run.work_item_id)!
|
|
184
|
-
const facts = buildRunFacts(db, runId, this.now(), this.options.timeZone)
|
|
185
|
-
handle.agent.followup(this.options.createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: `HAMS 数据库事实快照;本轮以此为准。\n${JSON.stringify(facts)}\n本轮指令:${text || '推进本轮任务,并通过 HAMS 工具提交结果或阻塞。'}` }] }))
|
|
186
|
-
this.delivered.add(runId)
|
|
187
|
-
if (rotate) await this.release(oldId)
|
|
188
|
-
return { sessionId: thread.session_id, runId, accepted: true as const }
|
|
189
|
-
} catch (error) {
|
|
190
|
-
// Admission may have partially succeeded. Never label a host delivery failure as success/retry it blindly.
|
|
191
|
-
db.prepare("UPDATE task_run SET status='unknown',finished_at=?,error_kind='host_delivery_unknown' WHERE id=? AND status='running'").run(this.now(), runId)
|
|
192
|
-
if (rotate && this.thread(run.work_item_id)?.session_id !== handle.agent.session.id) await this.release(handle.agent.session.id)
|
|
193
|
-
throw error
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
async rotateThread(ctx: CommandContext, input: { workItemId: string; reason?: string }): Promise<CommandReceipt> {
|
|
197
|
-
return this.serial(input.workItemId, async () => {
|
|
198
|
-
const item = this.options.service.getWorkItem(ctx, input.workItemId)
|
|
199
|
-
this.authorize(ctx)
|
|
200
|
-
if (!item.allowedActions.includes('rotateThread') || !item.assignee_id) throw new HamsError('STATE_INVALID', '当前工作项不能换新会话')
|
|
201
|
-
const old = this.thread(item.id)
|
|
202
|
-
const handle = await this.open(this.config(item.assignee_id), undefined, item.id)
|
|
203
|
-
try {
|
|
204
|
-
const result = this.options.service.rotateThread(ctx, { ...input, sessionId: handle.agent.session.id })
|
|
205
|
-
if (old) await this.release(old.session_id)
|
|
206
|
-
return result
|
|
207
|
-
} catch (error) { await this.release(handle.agent.session.id); throw error }
|
|
208
|
-
})
|
|
209
|
-
}
|
|
210
|
-
async instruct(ctx: CommandContext, input: { workItemId: string; text: string }): Promise<unknown> {
|
|
211
|
-
return this.serial(input.workItemId, async () => {
|
|
212
|
-
this.authorize(ctx)
|
|
213
|
-
if (!input.text?.trim()) throw new HamsError('STATE_INVALID', '追加指令不能为空')
|
|
214
|
-
const item = this.options.service.getWorkItem(ctx, input.workItemId)
|
|
215
|
-
if (['completed','cancelled','failed','paused'].includes(item.status)) throw new HamsError('STATE_INVALID', '当前工作项不接受追加指令')
|
|
216
|
-
const run = this.options.db.prepare("SELECT * FROM task_run WHERE work_item_id=? AND status IN ('queued','running') ORDER BY created_at DESC LIMIT 1").get(item.id) as RunRow | undefined
|
|
217
|
-
if (!run) throw new HamsError('STATE_INVALID', '没有待执行或正在运行的轮次;请先重试或等待复查')
|
|
218
|
-
if (run.status === 'queued') return this.start(run.id, input.text)
|
|
219
|
-
const thread = this.thread(item.id)
|
|
220
|
-
if (!thread || thread.status !== 'active') throw new HamsError('STATE_INVALID', '当前会话已等待换代')
|
|
221
|
-
const handle = await this.open(this.config(thread.member_id, thread.member_version), thread.session_id, item.id)
|
|
222
|
-
handle.agent.followup(this.options.createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: `HAMS 最新事实:\n${JSON.stringify(buildRunFacts(this.options.db, run.id, this.now(), this.options.timeZone))}\n追加指令:${input.text}` }] }))
|
|
223
|
-
return { sessionId: thread.session_id, runId: run.id, accepted: true }
|
|
224
|
-
})
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* 指派型任务:ready + 有 bot 负责人 + 没有排队/在跑的轮次 → 替它认领并投递这一轮。
|
|
228
|
-
* 这是「bot 给 bot 分派任务」的落地点:协调 bot 只负责指派,真正的执行在被指派 bot 自己的会话里。
|
|
229
|
-
*/
|
|
230
|
-
async dispatchAssignments(limit = 10): Promise<{ started: number; errors: string[] }> {
|
|
231
|
-
const rows = this.options.db
|
|
232
|
-
.prepare(
|
|
233
|
-
`SELECT i.id, i.assignee_id FROM work_item i JOIN member m ON m.id = i.assignee_id
|
|
234
|
-
WHERE i.status = 'ready' AND m.kind = 'bot' AND m.status = 'active'
|
|
235
|
-
AND NOT EXISTS (SELECT 1 FROM task_run r WHERE r.work_item_id = i.id AND r.status IN ('queued','running'))
|
|
236
|
-
ORDER BY i.updated_at LIMIT ?`,
|
|
237
|
-
)
|
|
238
|
-
.all(limit) as { id: string; assignee_id: string }[]
|
|
239
|
-
const report = { started: 0, errors: [] as string[] }
|
|
240
|
-
for (const row of rows) {
|
|
241
|
-
try {
|
|
242
|
-
const receipt = await this.claim({ principalId: row.assignee_id, channel: 'system', correlationId: `assign:${row.id}` }, { workItemId: row.id })
|
|
243
|
-
const runId = receipt.detail && typeof receipt.detail === 'object' ? (receipt.detail as { runId?: string }).runId : undefined
|
|
244
|
-
if (runId) await this.startRun(runId)
|
|
245
|
-
report.started++
|
|
246
|
-
} catch (error) {
|
|
247
|
-
report.errors.push(`${row.id}: ${this.errorText(error)}`)
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return report
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
private errorText(error: unknown): string {
|
|
254
|
-
if (error && typeof error === 'object' && 'code' in error) return `${(error as { code?: string }).code}: ${(error as { message?: string }).message ?? ''}`
|
|
255
|
-
return String(error)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Call after each business tick. No recovery of unknown/running deliveries. */
|
|
259
|
-
async dispatchQueued(limit = 20): Promise<{ started: number; errors: string[] }> {
|
|
260
|
-
const rows = this.options.db.prepare("SELECT id FROM task_run WHERE status='queued' ORDER BY created_at LIMIT ?").all(limit) as { id: string }[]
|
|
261
|
-
const report = { started: 0, errors: [] as string[] }
|
|
262
|
-
for (const row of rows) try { await this.startRun(row.id); report.started++ } catch (error) { report.errors.push(`${row.id}: ${String(error)}`) }
|
|
263
|
-
return report
|
|
264
|
-
}
|
|
265
|
-
private authorize(ctx: CommandContext) {
|
|
266
|
-
const principal = resolvePrincipal(this.options.db, ctx.principalId, this.now())
|
|
267
|
-
requireHuman(principal); requireCapability(principal, CAPABILITY.workItemManage)
|
|
268
|
-
if (ctx.channel !== 'ui') throw new HamsError('POLICY_DENIED', '人工会话操作只接受 UI 通道')
|
|
269
|
-
}
|
|
270
|
-
private now() { return (this.options.now ?? Date.now)() }
|
|
271
|
-
async dispose(): Promise<void> {
|
|
272
|
-
this.closed = true
|
|
273
|
-
await Promise.allSettled([...this.locks.values()])
|
|
274
|
-
for (const id of [...this.handles.keys()]) await this.release(id)
|
|
275
|
-
}
|
|
276
|
-
}
|