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.
- package/CHANGELOG.md +3 -3
- package/README.md +4 -0
- package/dist/index.js +11335 -0
- package/package.json +9 -5
- package/src/domain/check.ts +0 -193
- package/src/domain/errors.ts +0 -59
- package/src/domain/evidence.ts +0 -134
- package/src/domain/ids.ts +0 -31
- package/src/host/chat-runtime.ts +0 -489
- package/src/host/dsh-adapter.ts +0 -867
- package/src/host/session-adapter.ts +0 -276
- package/src/http/handler.ts +0 -1132
- package/src/index.js +0 -53
- package/src/service/audit.ts +0 -92
- package/src/service/chat.ts +0 -1100
- package/src/service/contracts.ts +0 -193
- package/src/service/defaults.ts +0 -182
- package/src/service/deliverables.ts +0 -240
- package/src/service/hams.ts +0 -3103
- package/src/service/knowledge.ts +0 -940
- package/src/service/member-config.ts +0 -39
- package/src/service/members.ts +0 -506
- package/src/service/memory.ts +0 -375
- package/src/service/permissions.ts +0 -118
- package/src/service/project-lifecycle.ts +0 -233
- package/src/service/projects.ts +0 -840
- package/src/service/receipts.ts +0 -63
- package/src/service/templates.ts +0 -140
- package/src/service/tick.ts +0 -751
- package/src/service/tools.ts +0 -711
- package/src/service/types.ts +0 -219
- package/src/service/work-item-create.ts +0 -159
- package/src/service/workspace-profile.ts +0 -443
- package/src/store/backup.ts +0 -239
- package/src/store/bootstrap.ts +0 -225
- package/src/store/db.ts +0 -179
- package/src/store/lock.ts +0 -194
- package/src/store/migrations.ts +0 -211
- package/src/store/schema/001_p1.sql +0 -846
- package/src/store/schema/001_p1.sql.ts +0 -865
- package/src/store/schema/002_knowledge.sql +0 -54
- package/src/store/schema/002_knowledge.sql.ts +0 -68
- package/src/store/schema/003_projects.sql.ts +0 -3
- package/src/store/schema/004_team.sql.ts +0 -50
- package/src/store/schema/005_target_check.sql.ts +0 -22
- package/src/store/schema/006_target_flow.sql.ts +0 -19
- package/src/store/schema/007_project_delete.sql.ts +0 -17
- package/src/store/schema/008_memory.sql.ts +0 -47
- package/src/store/schema/generate-projects.mjs +0 -16
- package/src/store/seed.ts +0 -114
package/src/host/chat-runtime.ts
DELETED
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 宿主侧对话运行时:把 HAMS 的单聊/群聊接到真实的 DSH agent 会话上。
|
|
3
|
-
*
|
|
4
|
-
* 三条边界(与设计一致):
|
|
5
|
-
* - **不改宿主 profile**:bot 用的 `hams-bot` preset 在插件运行时注册(C11 实测结论:
|
|
6
|
-
* preset 是 agent 作用域的组合,不碰公司 YAML 与 permissionPresets);空组合 = 没有
|
|
7
|
-
* shell / 文件写这类 agent 作用域插件,bot 只剩全局注册的 HAMS 工具。
|
|
8
|
-
* - **不抄聊天正文**:消息用 `sessionQuery.readSession()` 从会话日志读(活会话也走它),
|
|
9
|
-
* HAMS 库只存会话指针。
|
|
10
|
-
* - **不 import 宿主包**:插件不在宿主的 node_modules 里,所有宿主能力都从 `ctx.get()`
|
|
11
|
-
* 现取;拿不到就报 HOST_CAPABILITY_MISSING,绝不假装有会话。
|
|
12
|
-
*/
|
|
13
|
-
import path from 'node:path'
|
|
14
|
-
import { mkdir } from 'node:fs/promises'
|
|
15
|
-
import { randomUUID } from 'node:crypto'
|
|
16
|
-
import type { ChatMessage, ChatModelRow, ChatPresetRow, ChatRuntime } from '../service/chat.ts'
|
|
17
|
-
|
|
18
|
-
interface SessionLike {
|
|
19
|
-
id: string
|
|
20
|
-
events?: unknown
|
|
21
|
-
}
|
|
22
|
-
interface AgentLike {
|
|
23
|
-
session: SessionLike
|
|
24
|
-
status?: string
|
|
25
|
-
followup(message: unknown): void
|
|
26
|
-
inject?(message: unknown): void
|
|
27
|
-
cancel(cause: { kind: 'user' }, options: { keepInbox: true }): void
|
|
28
|
-
}
|
|
29
|
-
interface AgentHandleLike {
|
|
30
|
-
agent: AgentLike
|
|
31
|
-
dispose(): void | Promise<void>
|
|
32
|
-
}
|
|
33
|
-
interface AgentsService {
|
|
34
|
-
create(options: Record<string, unknown>): Promise<AgentHandleLike>
|
|
35
|
-
resume(options: Record<string, unknown>): Promise<AgentHandleLike>
|
|
36
|
-
get?(sessionId: string): { status?: string } | undefined
|
|
37
|
-
}
|
|
38
|
-
interface SessionQueryService {
|
|
39
|
-
readSession(sessionId: string): Promise<{ events?: readonly unknown[] }>
|
|
40
|
-
}
|
|
41
|
-
interface PresetsService {
|
|
42
|
-
list(): Promise<ChatPresetRow[]>
|
|
43
|
-
register(definition: { id: string; name?: string; description?: string; order?: number; plugins: readonly unknown[] }): Promise<() => Promise<void>>
|
|
44
|
-
resolve(id?: string): Promise<ChatPresetRow & { id: string }>
|
|
45
|
-
mount(agentCtx: unknown, id: string): Promise<unknown>
|
|
46
|
-
presetForSession?(session: unknown): string | null
|
|
47
|
-
}
|
|
48
|
-
interface LlmService {
|
|
49
|
-
listProviders(): { id: string; name: string }[]
|
|
50
|
-
listModels(provider: string): Promise<{ provider: string; id: string; name: string }[]>
|
|
51
|
-
}
|
|
52
|
-
interface DefaultModelService {
|
|
53
|
-
currentSelection(): { provider: string; model: string; reasoningEffort?: string } | undefined
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export const HAMS_BOT_PRESET = 'hams-bot'
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* bot 组合:persona(告诉它是 HAMS 的执行体)+ 压缩(长会话必备)。
|
|
60
|
-
* 刻意不装 bash / tool-fs / subagent / 联网:C11 要求 bot 会话里没有 shell 与文件写,
|
|
61
|
-
* 而且「装了再关 write」在 compositionInventory 里无法自证,干脆不装。
|
|
62
|
-
*/
|
|
63
|
-
const HAMS_BOT_PRESET_DEFINITION = {
|
|
64
|
-
id: HAMS_BOT_PRESET,
|
|
65
|
-
name: 'HAMS bot',
|
|
66
|
-
description: 'HAMS 工作项执行体:没有 shell、文件写、委派与联网,只用宿主全局工具(含 HAMS 窄语义工具)',
|
|
67
|
-
order: 90,
|
|
68
|
-
plugins: [
|
|
69
|
-
{
|
|
70
|
-
id: 'persona',
|
|
71
|
-
name: '@deepseek-ai/dsh-persona',
|
|
72
|
-
config: {
|
|
73
|
-
prefix:
|
|
74
|
-
'You are a HAMS work-item agent. Facts about the work item come from the HAMS tools; never invent state or close a work item yourself. ' +
|
|
75
|
-
'Answer the human in Chinese, briefly. When the human is chatting in a project group, you are the coordinator: you may break work into tasks and assign them to other bots via the HAMS tools.',
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: 'compaction',
|
|
80
|
-
name: 'cordis:group',
|
|
81
|
-
group: true,
|
|
82
|
-
isolate: { compaction: true, toolResultPruner: true },
|
|
83
|
-
config: [
|
|
84
|
-
{ id: 'compaction-basic', name: '@deepseek-ai/dsh-compaction-basic' },
|
|
85
|
-
{ id: 'command-compact', name: '@deepseek-ai/dsh-command-compact' },
|
|
86
|
-
{
|
|
87
|
-
id: 'tool-result-pruner',
|
|
88
|
-
name: '@deepseek-ai/dsh-compaction-tool-result-pruner',
|
|
89
|
-
config: { thresholdChars: 8192, headChars: 4096, tailChars: 1024 },
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
} as const
|
|
95
|
-
|
|
96
|
-
export interface HostChatRuntimeOptions {
|
|
97
|
-
ctx: { get?: (name: string) => unknown }
|
|
98
|
-
/** `$DSH_HOME/hams`:bot 会话的工作目录在 `<dataDir>/bots/<memberId>`。 */
|
|
99
|
-
dataDir: string
|
|
100
|
-
log?: (message: string) => void
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** 会话事件 → 聊天消息。只取真人输入与助手正文,工具调用与推理不进聊天。 */
|
|
104
|
-
export function chatMessagesFromEvents(events: readonly unknown[]): { messages: ChatMessage[]; running: boolean; error?: string } {
|
|
105
|
-
const messages: ChatMessage[] = []
|
|
106
|
-
const openTurns = new Set<number>()
|
|
107
|
-
let running = false
|
|
108
|
-
/** 最近一轮以 error 结束时的原因:模型没配、鉴权失败、provider 挂了都靠这个浮到界面上。 */
|
|
109
|
-
let lastError: string | undefined
|
|
110
|
-
const text = (content: unknown): string => {
|
|
111
|
-
if (!Array.isArray(content)) return ''
|
|
112
|
-
return content
|
|
113
|
-
.filter((block) => block && typeof block === 'object' && (block as { type?: string }).type === 'text' && typeof (block as { text?: unknown }).text === 'string')
|
|
114
|
-
.map((block) => String((block as { text: string }).text))
|
|
115
|
-
.join('\n')
|
|
116
|
-
.trim()
|
|
117
|
-
}
|
|
118
|
-
for (const raw of events) {
|
|
119
|
-
if (!raw || typeof raw !== 'object') continue
|
|
120
|
-
const event = raw as { type?: string; seq?: number; time?: number; data?: Record<string, unknown>; surfaceOp?: unknown }
|
|
121
|
-
// 压缩会用 replace 把一段历史替换成摘要副本;人看的 transcript 只取 append 的事件。
|
|
122
|
-
if (event.surfaceOp !== undefined && event.surfaceOp !== 'append') continue
|
|
123
|
-
const data = event.data || {}
|
|
124
|
-
if (event.type === 'turn/start') {
|
|
125
|
-
const turn = Number(data.turn)
|
|
126
|
-
if (Number.isFinite(turn)) openTurns.add(turn)
|
|
127
|
-
running = true
|
|
128
|
-
lastError = undefined
|
|
129
|
-
continue
|
|
130
|
-
}
|
|
131
|
-
if (event.type === 'turn/end') {
|
|
132
|
-
const turn = Number(data.turn)
|
|
133
|
-
if (Number.isFinite(turn)) openTurns.delete(turn)
|
|
134
|
-
running = openTurns.size > 0
|
|
135
|
-
const reason = data.reason as { kind?: string; error?: { message?: unknown } } | undefined
|
|
136
|
-
if (reason && reason.kind === 'error') {
|
|
137
|
-
const message = reason.error && typeof reason.error.message === 'string' ? reason.error.message : ''
|
|
138
|
-
lastError = message || '这一轮执行失败(宿主没有给出原因)'
|
|
139
|
-
}
|
|
140
|
-
continue
|
|
141
|
-
}
|
|
142
|
-
if (event.type === 'user/message') {
|
|
143
|
-
const source = data.source as { kind?: string } | undefined
|
|
144
|
-
if (!source || source.kind !== 'user') continue
|
|
145
|
-
const body = text(data.content)
|
|
146
|
-
if (!body) continue
|
|
147
|
-
messages.push({ id: String(data.id || `u-${messages.length}`), role: 'user', text: body, at: Number(event.time) || 0, seq: Number(event.seq) || 0 })
|
|
148
|
-
continue
|
|
149
|
-
}
|
|
150
|
-
if (event.type === 'assistant/message') {
|
|
151
|
-
const message = data.message as { id?: string; content?: unknown } | undefined
|
|
152
|
-
const body = text(message && message.content)
|
|
153
|
-
if (!body) continue
|
|
154
|
-
messages.push({ id: String((message && message.id) || `a-${messages.length}`), role: 'assistant', text: body, at: Number(event.time) || 0, seq: Number(event.seq) || 0 })
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return { messages, running: running && openTurns.size > 0, ...(lastError ? { error: lastError } : {}) }
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* 模型串 → agents 的 agentOptions。
|
|
162
|
-
*
|
|
163
|
-
* model id 本身可能带 `/`(本机默认路由就是 `auto/deepseek-v4.1-flash`),所以不能按第一个
|
|
164
|
-
* 斜杠硬切:先跨 provider 精确匹配完整 id,再看前缀是不是已注册 provider,最后才当裸名字
|
|
165
|
-
* 交给默认 provider。目录探测失败时按前缀/默认处理,不阻塞开会话。
|
|
166
|
-
*/
|
|
167
|
-
export interface DefaultSelection { provider?: string; model?: string; reasoningEffort?: string }
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* 会话要用的 AgentOptions:优先 bot 自己配的模型;没配就用宿主默认模型兜底。
|
|
171
|
-
* 两边都不存在时返回 undefined(调用方应给出「配置模型」的明确错误,而不是让宿主抛 has no provider/model)。
|
|
172
|
-
*/
|
|
173
|
-
export async function resolveAgentOptions(
|
|
174
|
-
choice: string | null,
|
|
175
|
-
llmService?: LlmService,
|
|
176
|
-
fallback?: DefaultSelection,
|
|
177
|
-
): Promise<{ provider?: string; model: string; reasoningEffort?: string } | undefined> {
|
|
178
|
-
const parsed = await parseModelChoice(choice, llmService, fallback?.provider)
|
|
179
|
-
if (parsed?.model) return parsed
|
|
180
|
-
if (!fallback?.provider || !fallback.model) return undefined
|
|
181
|
-
return {
|
|
182
|
-
provider: String(fallback.provider),
|
|
183
|
-
model: String(fallback.model),
|
|
184
|
-
...(fallback.reasoningEffort ? { reasoningEffort: String(fallback.reasoningEffort) } : {}),
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export async function parseModelChoice(
|
|
189
|
-
value: string | null,
|
|
190
|
-
llmService?: LlmService,
|
|
191
|
-
fallbackProvider?: string,
|
|
192
|
-
): Promise<{ provider?: string; model: string } | undefined> {
|
|
193
|
-
const raw = (value || '').trim()
|
|
194
|
-
if (!raw) return undefined
|
|
195
|
-
let providerIds: string[] = []
|
|
196
|
-
if (llmService) {
|
|
197
|
-
try {
|
|
198
|
-
providerIds = llmService.listProviders().map((row) => row.id)
|
|
199
|
-
} catch {
|
|
200
|
-
providerIds = []
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (llmService && providerIds.length) {
|
|
204
|
-
for (const provider of providerIds) {
|
|
205
|
-
try {
|
|
206
|
-
const models = await llmService.listModels(provider)
|
|
207
|
-
if (models.some((row) => row.id === raw)) return { provider, model: raw }
|
|
208
|
-
} catch {
|
|
209
|
-
// 单个 provider 目录失败不影响其它
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const longestFirst = [...providerIds].sort((a, b) => b.length - a.length)
|
|
213
|
-
for (const provider of longestFirst) {
|
|
214
|
-
if (raw.startsWith(`${provider}/`)) return { provider, model: raw.slice(provider.length + 1) }
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return { provider: fallbackProvider, model: raw }
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** 注册 bot preset(进程内只做一次);装不起来返回 null,让调用方退回宿主默认 preset。 */
|
|
221
|
-
let presetPromise: Promise<string | null> | null = null
|
|
222
|
-
export function ensureHamsBotPreset(ctx: { get?: (name: string) => unknown }, log?: (line: string) => void): Promise<string | null> {
|
|
223
|
-
if (presetPromise) return presetPromise
|
|
224
|
-
const note = log || (() => {})
|
|
225
|
-
presetPromise = (async () => {
|
|
226
|
-
const registry = (() => {
|
|
227
|
-
try {
|
|
228
|
-
const found = ctx.get ? ctx.get('agentPresets') : undefined
|
|
229
|
-
return found && typeof found === 'object' ? (found as PresetsService) : undefined
|
|
230
|
-
} catch {
|
|
231
|
-
return undefined
|
|
232
|
-
}
|
|
233
|
-
})()
|
|
234
|
-
if (!registry) return null
|
|
235
|
-
try {
|
|
236
|
-
const list = await registry.list()
|
|
237
|
-
if (Array.isArray(list) && list.some((row) => row && row.id === HAMS_BOT_PRESET)) {
|
|
238
|
-
const resolved = await registry.resolve(HAMS_BOT_PRESET)
|
|
239
|
-
if (resolved && resolved.broken) {
|
|
240
|
-
note(`[hams] agent preset ${HAMS_BOT_PRESET} 不可用:${resolved.broken}`)
|
|
241
|
-
return null
|
|
242
|
-
}
|
|
243
|
-
return HAMS_BOT_PRESET
|
|
244
|
-
}
|
|
245
|
-
// register 不因装不起来而 reject:失败只会落成 record.broken,所以必须再 resolve 一次。
|
|
246
|
-
await registry.register(HAMS_BOT_PRESET_DEFINITION as never)
|
|
247
|
-
const resolved = await registry.resolve(HAMS_BOT_PRESET)
|
|
248
|
-
if (resolved && resolved.broken) {
|
|
249
|
-
note(`[hams] agent preset ${HAMS_BOT_PRESET} 不可用:${resolved.broken}`)
|
|
250
|
-
return null
|
|
251
|
-
}
|
|
252
|
-
note(`[hams] 已注册 agent preset ${HAMS_BOT_PRESET}`)
|
|
253
|
-
return HAMS_BOT_PRESET
|
|
254
|
-
} catch (error) {
|
|
255
|
-
note(`[hams] 注册 ${HAMS_BOT_PRESET} 失败,退回宿主默认 preset:${String(error)}`)
|
|
256
|
-
return null
|
|
257
|
-
}
|
|
258
|
-
})()
|
|
259
|
-
return presetPromise
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** 目录名只保留安全字符:会话 key 里带冒号与成员 id,不能直接当路径。 */
|
|
263
|
-
function safeSegment(value: string): string {
|
|
264
|
-
return String(value || 'default').replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'default'
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export function createHostChatRuntime(options: HostChatRuntimeOptions): ChatRuntime & { dispose(): Promise<void> } {
|
|
268
|
-
const { ctx, dataDir } = options
|
|
269
|
-
const log = options.log || (() => {})
|
|
270
|
-
const handles = new Map<string, Promise<AgentHandleLike>>()
|
|
271
|
-
const openings = new Map<string, Promise<AgentHandleLike>>()
|
|
272
|
-
const meta = new Map<string, { memberId: string; preset: string; model: string | null }>()
|
|
273
|
-
const contextSent = new Set<string>()
|
|
274
|
-
let modelsCache: { at: number; rows: ChatModelRow[] } | null = null
|
|
275
|
-
let closed = false
|
|
276
|
-
|
|
277
|
-
const service = <T>(name: string): T | undefined => {
|
|
278
|
-
if (typeof ctx.get !== 'function') return undefined
|
|
279
|
-
try {
|
|
280
|
-
const found = ctx.get(name)
|
|
281
|
-
return found && typeof found === 'object' ? (found as T) : undefined
|
|
282
|
-
} catch {
|
|
283
|
-
return undefined
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
const agents = () => service<AgentsService>('agents')
|
|
287
|
-
const sessionQuery = () => service<SessionQueryService>('sessionQuery')
|
|
288
|
-
const presets = () => service<PresetsService>('agentPresets')
|
|
289
|
-
const llm = () => service<LlmService>('llm')
|
|
290
|
-
const defaultModelService = () => service<DefaultModelService>('agentDefaultModel')
|
|
291
|
-
|
|
292
|
-
/** 把「你是谁、在哪个项目」作为注入上下文送进会话:模型看得到,聊天记录里不显示。 */
|
|
293
|
-
function sendContext(handle: AgentHandleLike, sessionId: string, context?: string): void {
|
|
294
|
-
if (!context || contextSent.has(sessionId) || typeof handle.agent.inject !== 'function') return
|
|
295
|
-
try {
|
|
296
|
-
handle.agent.inject({
|
|
297
|
-
id: `msg-${randomUUID()}`,
|
|
298
|
-
role: 'user',
|
|
299
|
-
content: [{ type: 'text', text: context }],
|
|
300
|
-
source: { kind: 'hams-chat-context', form: 'instructions' },
|
|
301
|
-
})
|
|
302
|
-
contextSent.add(sessionId)
|
|
303
|
-
} catch (error) {
|
|
304
|
-
log(`[hams] 注入对话上下文失败:${String(error)}`)
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function openSession(input: { sessionId?: string; memberId: string; preset: string; model: string | null; context?: string; cwdKey?: string }): Promise<AgentHandleLike> {
|
|
309
|
-
const registry = agents()
|
|
310
|
-
if (!registry) throw new Error('宿主没有 agents 服务')
|
|
311
|
-
// bot 没配模型时用宿主默认模型兜底:否则新会话一开就报 has no provider/model。
|
|
312
|
-
const parsedModel = await resolveAgentOptions(input.model, llm(), defaultModelService()?.currentSelection?.())
|
|
313
|
-
if (!parsedModel) throw new Error('这个 bot 没有配置模型,宿主也没有默认模型;请给 bot 选一个模型或设置宿主默认模型')
|
|
314
|
-
const agentOptions = Object.fromEntries(Object.entries(parsedModel).filter(([, value]) => value !== undefined && value !== ''))
|
|
315
|
-
const presetId = input.preset === HAMS_BOT_PRESET ? await ensureHamsBotPreset(ctx, log) : input.preset
|
|
316
|
-
// 一个 bot 被多个项目引用时,每个对话各占一个子目录,免得以后给了文件工具互相串。
|
|
317
|
-
const cwd = path.resolve(dataDir, 'bots', input.memberId, 'chats', safeSegment(input.cwdKey || 'default'))
|
|
318
|
-
const base = path.resolve(dataDir, 'bots') + path.sep
|
|
319
|
-
if (!cwd.startsWith(base)) throw new Error('bot 工作目录越界')
|
|
320
|
-
await mkdir(cwd, { recursive: true })
|
|
321
|
-
const meta = { cwd, ...(presetId ? { agentPreset: presetId } : {}) }
|
|
322
|
-
const registryPresets = presets()
|
|
323
|
-
// meta.agentPreset 只写 session header;真正挂载组合必须在 setup 里 mount,否则是「假安全」。
|
|
324
|
-
const setup = presetId && registryPresets && typeof registryPresets.mount === 'function'
|
|
325
|
-
? async (agentCtx: unknown) => { await registryPresets.mount(agentCtx, presetId) }
|
|
326
|
-
: undefined
|
|
327
|
-
if (input.sessionId) {
|
|
328
|
-
// resume 只认 resumeSessionId/agentOptions/setup;meta 只在 create 时写。
|
|
329
|
-
const handle = await registry.resume({ resumeSessionId: input.sessionId, agentOptions, ...(setup ? { setup } : {}) })
|
|
330
|
-
sendContext(handle, input.sessionId, input.context)
|
|
331
|
-
return handle
|
|
332
|
-
}
|
|
333
|
-
const sessionId = `hams-${input.memberId}-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`
|
|
334
|
-
const handle = await registry.create({ sessionId, meta, agentOptions, ...(setup ? { setup } : {}) })
|
|
335
|
-
sendContext(handle, sessionId, input.context)
|
|
336
|
-
return handle
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function handleFor(sessionId: string, input: { memberId: string; preset: string; model: string | null; context?: string }): Promise<AgentHandleLike> {
|
|
340
|
-
const existing = handles.get(sessionId)
|
|
341
|
-
if (existing) return existing
|
|
342
|
-
const pending = openings.get(sessionId)
|
|
343
|
-
if (pending) return pending
|
|
344
|
-
const opening = openSession({ sessionId, memberId: input.memberId, preset: input.preset, model: input.model, context: input.context })
|
|
345
|
-
openings.set(sessionId, opening)
|
|
346
|
-
return opening
|
|
347
|
-
.then((handle) => { handles.set(sessionId, Promise.resolve(handle)); return handle })
|
|
348
|
-
.finally(() => { openings.delete(sessionId) })
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const runtime: ChatRuntime & { dispose(): Promise<void> } = {
|
|
352
|
-
get available() {
|
|
353
|
-
return Boolean(agents())
|
|
354
|
-
},
|
|
355
|
-
get unavailableReason() {
|
|
356
|
-
return agents() ? undefined : '宿主没有 agents 服务(只读实例或没有 agent 运行时)'
|
|
357
|
-
},
|
|
358
|
-
async ensure(input) {
|
|
359
|
-
if (closed) throw new Error('对话运行时已停止')
|
|
360
|
-
if (input.sessionId) {
|
|
361
|
-
meta.set(input.sessionId, { memberId: input.memberId, preset: input.agentPreset || HAMS_BOT_PRESET, model: input.model })
|
|
362
|
-
await handleFor(input.sessionId, { ...meta.get(input.sessionId)!, context: input.context })
|
|
363
|
-
return { sessionId: input.sessionId }
|
|
364
|
-
}
|
|
365
|
-
const presetId = input.agentPreset || HAMS_BOT_PRESET
|
|
366
|
-
const handle = await openSession({
|
|
367
|
-
memberId: input.memberId,
|
|
368
|
-
preset: presetId,
|
|
369
|
-
model: input.model,
|
|
370
|
-
cwdKey: input.key,
|
|
371
|
-
context: input.context,
|
|
372
|
-
})
|
|
373
|
-
const sessionId = handle.agent.session.id
|
|
374
|
-
meta.set(sessionId, { memberId: input.memberId, preset: presetId, model: input.model })
|
|
375
|
-
handles.set(sessionId, Promise.resolve(handle))
|
|
376
|
-
return { sessionId }
|
|
377
|
-
},
|
|
378
|
-
async send(sessionId, text) {
|
|
379
|
-
const info = meta.get(sessionId)
|
|
380
|
-
if (!info) throw new Error('未知会话:先发一次 chat/open 再发消息')
|
|
381
|
-
const handle = await handleFor(sessionId, info)
|
|
382
|
-
handle.agent.followup({
|
|
383
|
-
id: `msg-${randomUUID()}`,
|
|
384
|
-
role: 'user',
|
|
385
|
-
content: [{ type: 'text', text }],
|
|
386
|
-
source: { kind: 'user' },
|
|
387
|
-
})
|
|
388
|
-
},
|
|
389
|
-
async status(sessionId) {
|
|
390
|
-
const live = agents()?.get?.(sessionId)
|
|
391
|
-
const status = live && typeof live.status === 'string' ? live.status : undefined
|
|
392
|
-
if (status) return { running: status === 'running' }
|
|
393
|
-
const query = sessionQuery()
|
|
394
|
-
let events: readonly unknown[] = []
|
|
395
|
-
if (query) {
|
|
396
|
-
try {
|
|
397
|
-
const snapshot = await query.readSession(sessionId)
|
|
398
|
-
events = snapshot && Array.isArray(snapshot.events) ? snapshot.events : []
|
|
399
|
-
} catch {
|
|
400
|
-
return { running: false }
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
const scanned = chatMessagesFromEvents(events)
|
|
404
|
-
return { running: scanned.running, ...(scanned.error ? { error: scanned.error } : {}) }
|
|
405
|
-
},
|
|
406
|
-
async read(sessionId) {
|
|
407
|
-
const query = sessionQuery()
|
|
408
|
-
let events: readonly unknown[] = []
|
|
409
|
-
if (query) {
|
|
410
|
-
try {
|
|
411
|
-
const snapshot = await query.readSession(sessionId)
|
|
412
|
-
events = snapshot && Array.isArray(snapshot.events) ? snapshot.events : []
|
|
413
|
-
} catch (error) {
|
|
414
|
-
log(`[hams] 读会话 ${sessionId} 失败:${String(error)}`)
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
const scanned = chatMessagesFromEvents(events)
|
|
418
|
-
const live = agents()?.get?.(sessionId)
|
|
419
|
-
const status = live && typeof live.status === 'string' ? live.status : undefined
|
|
420
|
-
return { messages: scanned.messages, running: status ? status === 'running' : scanned.running, ...(scanned.error ? { error: scanned.error } : {}) }
|
|
421
|
-
},
|
|
422
|
-
async stop(sessionId) {
|
|
423
|
-
const pending = handles.get(sessionId)
|
|
424
|
-
if (!pending) return
|
|
425
|
-
const handle = await pending
|
|
426
|
-
handle.agent.cancel({ kind: 'user' }, { keepInbox: true })
|
|
427
|
-
},
|
|
428
|
-
async defaultModel() {
|
|
429
|
-
const selection = defaultModelService()?.currentSelection?.()
|
|
430
|
-
if (!selection || !selection.provider || !selection.model) return null
|
|
431
|
-
return { provider: String(selection.provider), model: String(selection.model) }
|
|
432
|
-
},
|
|
433
|
-
async presets() {
|
|
434
|
-
const registry = presets()
|
|
435
|
-
if (!registry) return []
|
|
436
|
-
try {
|
|
437
|
-
const list = await registry.list()
|
|
438
|
-
return Array.isArray(list) ? list.filter((row) => row && row.id) : []
|
|
439
|
-
} catch (error) {
|
|
440
|
-
log(`[hams] 读 preset 目录失败:${String(error)}`)
|
|
441
|
-
return []
|
|
442
|
-
}
|
|
443
|
-
},
|
|
444
|
-
async models() {
|
|
445
|
-
const now = Date.now()
|
|
446
|
-
if (modelsCache && now - modelsCache.at < 60_000) return modelsCache.rows
|
|
447
|
-
const rows: ChatModelRow[] = []
|
|
448
|
-
const seen = new Set<string>()
|
|
449
|
-
const push = (provider: string, model: string, label?: string) => {
|
|
450
|
-
const key = `${provider}/${model}`
|
|
451
|
-
if (!provider || !model || seen.has(key)) return
|
|
452
|
-
seen.add(key)
|
|
453
|
-
rows.push({ provider, model, label })
|
|
454
|
-
}
|
|
455
|
-
const llmService = llm()
|
|
456
|
-
if (llmService) {
|
|
457
|
-
try {
|
|
458
|
-
for (const provider of llmService.listProviders()) {
|
|
459
|
-
try {
|
|
460
|
-
for (const model of await llmService.listModels(provider.id)) push(model.provider || provider.id, model.id, model.name)
|
|
461
|
-
} catch {
|
|
462
|
-
// 单个 provider 目录失败不影响别的
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
} catch (error) {
|
|
466
|
-
log(`[hams] 读模型目录失败:${String(error)}`)
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
const fallback = defaultModelService()?.currentSelection()
|
|
470
|
-
if (fallback && fallback.provider && fallback.model) push(fallback.provider, fallback.model, `${fallback.model}(宿主默认)`)
|
|
471
|
-
modelsCache = { at: now, rows }
|
|
472
|
-
return rows
|
|
473
|
-
},
|
|
474
|
-
async dispose() {
|
|
475
|
-
closed = true
|
|
476
|
-
for (const pending of [...handles.values(), ...openings.values()]) {
|
|
477
|
-
try {
|
|
478
|
-
const handle = await pending
|
|
479
|
-
await handle.dispose()
|
|
480
|
-
} catch {
|
|
481
|
-
// 关闭阶段的失败只记日志,不阻塞插件卸载
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
handles.clear()
|
|
485
|
-
openings.clear()
|
|
486
|
-
},
|
|
487
|
-
}
|
|
488
|
-
return runtime
|
|
489
|
-
}
|