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
package/src/service/chat.ts
DELETED
|
@@ -1,1100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 对话:单聊(智能体)与群聊(项目)。
|
|
3
|
-
*
|
|
4
|
-
* 这一层只管「把谁放到哪个宿主会话里」以及线程指针的存取,不碰消息正文:
|
|
5
|
-
* - 单聊 = 与某个 bot 的常驻会话;
|
|
6
|
-
* - 群聊 = 项目推进 bot(coordinator)的常驻会话,群里其他人的工作进展由它查工作项来回答。
|
|
7
|
-
*
|
|
8
|
-
* 会话本身由宿主 `agents` 创建/恢复,消息从会话日志读(`sessionQuery`),
|
|
9
|
-
* HAMS 库只存 `chat:<kind>:<id>` 这个指针,避免把聊天正文再抄一份。
|
|
10
|
-
* 宿主没有 agent 服务时(只读实例 / sdk-minimal 之外的部署),
|
|
11
|
-
* 所有对话接口返回 HOST_CAPABILITY_MISSING 和下一步,而不是假装成功。
|
|
12
|
-
*/
|
|
13
|
-
import type { DatabaseSync } from 'node:sqlite'
|
|
14
|
-
import { createHash } from 'node:crypto'
|
|
15
|
-
import { HamsError } from '../domain/errors.ts'
|
|
16
|
-
import { createIdFactory, type IdFactory } from '../domain/ids.ts'
|
|
17
|
-
import { CAPABILITY, requireCapability, resolvePrincipal } from './permissions.ts'
|
|
18
|
-
import type { CommandContext } from './types.ts'
|
|
19
|
-
import {
|
|
20
|
-
createMemory,
|
|
21
|
-
extractMemoryCandidates,
|
|
22
|
-
MEMORY_KIND_LABEL,
|
|
23
|
-
memberMemoryBlock,
|
|
24
|
-
projectMemoryBlock,
|
|
25
|
-
type MemoryKind,
|
|
26
|
-
} from './memory.ts'
|
|
27
|
-
|
|
28
|
-
export type ChatKind = 'member' | 'project'
|
|
29
|
-
|
|
30
|
-
export interface ChatMessage {
|
|
31
|
-
id: string
|
|
32
|
-
role: 'user' | 'assistant' | 'system'
|
|
33
|
-
text: string
|
|
34
|
-
at: number
|
|
35
|
-
/** 会话事件序号;群聊合并记录用它做水位,避免重复。 */
|
|
36
|
-
seq?: number
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface ChatPresetRow {
|
|
40
|
-
id: string
|
|
41
|
-
name?: string
|
|
42
|
-
description?: string
|
|
43
|
-
broken?: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface ChatModelRow {
|
|
47
|
-
provider: string
|
|
48
|
-
model: string
|
|
49
|
-
label?: string
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** 宿主侧能力:由 `src/host/chat-runtime.ts` 实现,测试里用假对象。 */
|
|
53
|
-
export interface ChatRuntime {
|
|
54
|
-
readonly available: boolean
|
|
55
|
-
readonly unavailableReason?: string
|
|
56
|
-
ensure(input: {
|
|
57
|
-
sessionId?: string
|
|
58
|
-
key: string
|
|
59
|
-
memberId: string
|
|
60
|
-
memberName: string
|
|
61
|
-
memberVersion: number
|
|
62
|
-
agentPreset: string
|
|
63
|
-
model: string | null
|
|
64
|
-
title: string
|
|
65
|
-
/** 首次建会话时注入的上下文(不显示在聊天记录里),告诉 bot 它是谁、在哪个项目里。 */
|
|
66
|
-
context?: string
|
|
67
|
-
}): Promise<{ sessionId: string }>
|
|
68
|
-
send(sessionId: string, text: string): Promise<void>
|
|
69
|
-
read(sessionId: string): Promise<{ messages: ChatMessage[]; running: boolean; error?: string }>
|
|
70
|
-
/** 群聊里单个成员是否在跑(没有会话就 false)。 */
|
|
71
|
-
status?(sessionId: string): Promise<{ running: boolean; error?: string }>
|
|
72
|
-
/** 宿主当前默认模型;bot 没配模型时用它,而不是建一个注定报错的会话。 */
|
|
73
|
-
defaultModel?(): Promise<{ provider: string; model: string } | null>
|
|
74
|
-
stop(sessionId: string): Promise<void>
|
|
75
|
-
presets(): Promise<ChatPresetRow[]>
|
|
76
|
-
models(): Promise<ChatModelRow[]>
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface ChatPeer {
|
|
80
|
-
kind: ChatKind
|
|
81
|
-
targetId: string
|
|
82
|
-
title: string
|
|
83
|
-
memberId: string
|
|
84
|
-
memberName: string
|
|
85
|
-
memberVersion: number
|
|
86
|
-
agentPreset: string
|
|
87
|
-
model: string | null
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface ChatThreadView extends ChatPeer {
|
|
91
|
-
sessionId: string
|
|
92
|
-
createdAt: number
|
|
93
|
-
updatedAt: number
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface ThreadPointer {
|
|
97
|
-
sessionId: string
|
|
98
|
-
memberId: string
|
|
99
|
-
memberVersion: number
|
|
100
|
-
agentPreset: string
|
|
101
|
-
model: string | null
|
|
102
|
-
at: number
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
export type TeamKind = 'coordinator' | 'member' | 'temp'
|
|
107
|
-
|
|
108
|
-
export interface TeamMemberView {
|
|
109
|
-
memberId: string
|
|
110
|
-
name: string
|
|
111
|
-
kind: TeamKind
|
|
112
|
-
status: string
|
|
113
|
-
brief: string
|
|
114
|
-
sessionId: string | null
|
|
115
|
-
running: boolean
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export interface GroupMessageView {
|
|
119
|
-
id: string
|
|
120
|
-
authorKind: 'human' | 'bot' | 'system'
|
|
121
|
-
authorId: string
|
|
122
|
-
authorName: string
|
|
123
|
-
text: string
|
|
124
|
-
at: number
|
|
125
|
-
kind: 'chat' | 'mention' | 'system'
|
|
126
|
-
toMemberId: string | null
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface GroupChatView {
|
|
130
|
-
projectId: string
|
|
131
|
-
title: string
|
|
132
|
-
coordinator: TeamMemberView | null
|
|
133
|
-
members: TeamMemberView[]
|
|
134
|
-
messages: GroupMessageView[]
|
|
135
|
-
running: boolean
|
|
136
|
-
available: boolean
|
|
137
|
-
reason?: string
|
|
138
|
-
error?: string
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const DEFAULT_BOT_PRESET = 'hams-bot'
|
|
142
|
-
|
|
143
|
-
function escapeRegExp(value: string): string {
|
|
144
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export class ChatService {
|
|
148
|
-
private db: DatabaseSync
|
|
149
|
-
private runtime: ChatRuntime
|
|
150
|
-
private now: () => number
|
|
151
|
-
private id: IdFactory
|
|
152
|
-
|
|
153
|
-
constructor(options: { db: DatabaseSync; runtime: ChatRuntime; now?: () => number; idFactory?: IdFactory }) {
|
|
154
|
-
this.db = options.db
|
|
155
|
-
this.runtime = options.runtime
|
|
156
|
-
this.now = options.now ?? (() => Date.now())
|
|
157
|
-
this.id = options.idFactory ?? createIdFactory(this.now)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
get available(): boolean {
|
|
161
|
-
return this.runtime.available
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
get reason(): string | undefined {
|
|
165
|
-
return this.runtime.unavailableReason
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private human(ctx: CommandContext): void {
|
|
169
|
-
const row = this.db
|
|
170
|
-
.prepare("SELECT kind FROM member WHERE id = ?")
|
|
171
|
-
.get(ctx.principalId) as { kind: string } | undefined
|
|
172
|
-
if (!row || row.kind !== 'human') {
|
|
173
|
-
throw new HamsError('POLICY_DENIED', '只有人能发起对话;bot 之间不私下传话', {
|
|
174
|
-
nextStep: 'bot 的协作通过工作项与提议,不通过对话',
|
|
175
|
-
})
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** 解析对话对象:单聊落到 bot 本人,群聊落到项目推进 bot。 */
|
|
180
|
-
private peer(ctx: CommandContext, kind: ChatKind, targetId: string): ChatPeer {
|
|
181
|
-
this.human(ctx)
|
|
182
|
-
if (kind === 'project') {
|
|
183
|
-
const project = this.db
|
|
184
|
-
.prepare('SELECT id, name, coordinator_id, deleted_at FROM project WHERE id = ?')
|
|
185
|
-
.get(targetId) as { id: string; name: string; coordinator_id: string | null; deleted_at: number | null } | undefined
|
|
186
|
-
if (!project || project.deleted_at) throw new HamsError('STATE_INVALID', `项目不存在:${targetId}`, { nextStep: '刷新项目列表后重试' })
|
|
187
|
-
if (!project.coordinator_id) {
|
|
188
|
-
throw new HamsError('STATE_INVALID', '这个项目还没有推进 bot,群聊没有回答的人', {
|
|
189
|
-
nextStep: '在项目详情里指定「推进 bot」(新建或编辑项目时可以选),再进群聊',
|
|
190
|
-
})
|
|
191
|
-
}
|
|
192
|
-
return { ...this.bot(project.coordinator_id), kind, targetId, title: project.name }
|
|
193
|
-
}
|
|
194
|
-
const bot = this.bot(targetId)
|
|
195
|
-
return { ...bot, kind: 'member', targetId, title: bot.memberName }
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
private bot(memberId: string): { memberId: string; memberName: string; memberVersion: number; agentPreset: string; model: string | null } {
|
|
199
|
-
const member = this.db
|
|
200
|
-
.prepare('SELECT id, name, status, kind, current_version FROM member WHERE id = ?')
|
|
201
|
-
.get(memberId) as { id: string; name: string; status: string; kind: string; current_version: number } | undefined
|
|
202
|
-
if (!member || member.kind !== 'bot') throw new HamsError('STATE_INVALID', `不是 bot 成员:${memberId}`)
|
|
203
|
-
if (member.status !== 'active') {
|
|
204
|
-
throw new HamsError('STATE_INVALID', `bot 当前不在岗(${member.status}),先恢复在岗再对话`, {
|
|
205
|
-
nextStep: '在智能体详情里点「恢复在岗」',
|
|
206
|
-
})
|
|
207
|
-
}
|
|
208
|
-
const version = Number(member.current_version || 0)
|
|
209
|
-
const config = version
|
|
210
|
-
? (this.db
|
|
211
|
-
.prepare('SELECT agent_preset, model FROM member_version WHERE member_id = ? AND version = ?')
|
|
212
|
-
.get(memberId, version) as { agent_preset: string | null; model: string | null } | undefined)
|
|
213
|
-
: undefined
|
|
214
|
-
return {
|
|
215
|
-
memberId,
|
|
216
|
-
memberName: member.name,
|
|
217
|
-
memberVersion: version,
|
|
218
|
-
agentPreset: (config?.agent_preset || '').trim() || DEFAULT_BOT_PRESET,
|
|
219
|
-
model: config?.model ?? null,
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
private pointer(kind: ChatKind, targetId: string): ThreadPointer | null {
|
|
224
|
-
const row = this.db
|
|
225
|
-
.prepare('SELECT value FROM hams_meta WHERE key = ?')
|
|
226
|
-
.get(`chat:${kind}:${targetId}`) as { value: string } | undefined
|
|
227
|
-
if (!row) return null
|
|
228
|
-
try {
|
|
229
|
-
const parsed = JSON.parse(row.value) as ThreadPointer
|
|
230
|
-
return parsed && typeof parsed.sessionId === 'string' ? parsed : null
|
|
231
|
-
} catch {
|
|
232
|
-
return null
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
private savePointer(kind: ChatKind, targetId: string, pointer: ThreadPointer): void {
|
|
237
|
-
this.db
|
|
238
|
-
.prepare("INSERT OR REPLACE INTO hams_meta (key, value) VALUES (?, ?)")
|
|
239
|
-
.run(`chat:${kind}:${targetId}`, JSON.stringify(pointer))
|
|
240
|
-
// 反向索引:工具层凭 sessionId 认人,聊天会话也要能解析出是哪个 bot。
|
|
241
|
-
this.db
|
|
242
|
-
.prepare("INSERT OR REPLACE INTO hams_meta (key, value) VALUES (?, ?)")
|
|
243
|
-
.run(`chat:session:${pointer.sessionId}`, pointer.memberId)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
private requireRuntime(): void {
|
|
247
|
-
if (!this.runtime.available) {
|
|
248
|
-
throw new HamsError('HOST_CAPABILITY_MISSING', this.runtime.unavailableReason || '宿主没有提供 agent 会话服务', {
|
|
249
|
-
nextStep: '确认 DSH 运行时带 agents / sessionQuery 服务后重启插件;只读实例不能开对话',
|
|
250
|
-
})
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* 打开(必要时创建)对话。配置版本、preset 或模型变了就换一个新会话,
|
|
256
|
-
* 旧会话留在宿主里可查,但当前对话只跟最新配置走。
|
|
257
|
-
*/
|
|
258
|
-
async open(ctx: CommandContext, input: { kind: ChatKind; targetId: string }): Promise<ChatThreadView> {
|
|
259
|
-
const peer = this.peer(ctx, input.kind, input.targetId)
|
|
260
|
-
this.requireRuntime()
|
|
261
|
-
const existing = this.pointer(input.kind, input.targetId)
|
|
262
|
-
// bot 没配模型就用宿主默认模型;两边都没有就明确拒绝,不要建一个「发了没反应」的会话。
|
|
263
|
-
let model = peer.model
|
|
264
|
-
if (!model) {
|
|
265
|
-
const fallback = this.runtime.defaultModel ? await this.runtime.defaultModel() : null
|
|
266
|
-
model = fallback ? `${fallback.provider}/${fallback.model}` : null
|
|
267
|
-
}
|
|
268
|
-
if (!model) {
|
|
269
|
-
throw new HamsError('HOST_CAPABILITY_MISSING', `${peer.memberName} 没有配置模型,宿主也没有默认模型`, {
|
|
270
|
-
nextStep: '在智能体「配置」里选一个模型并保存;或在宿主设置里指定默认模型',
|
|
271
|
-
})
|
|
272
|
-
}
|
|
273
|
-
const sameConfig =
|
|
274
|
-
existing &&
|
|
275
|
-
existing.memberId === peer.memberId &&
|
|
276
|
-
existing.memberVersion === peer.memberVersion &&
|
|
277
|
-
existing.agentPreset === peer.agentPreset &&
|
|
278
|
-
(existing.model || null) === model
|
|
279
|
-
const context = input.kind === 'project'
|
|
280
|
-
? `你在 HAMS 项目《${peer.title}》(projectId=${input.targetId})的群聊里,是这个项目的推进 bot。`
|
|
281
|
-
+ '你可以用 hams_list_project_work 查这个项目的工作项进展,用 hams_assign_task 把 ready 工作项指派给其他 bot;'
|
|
282
|
-
+ '关单与审批只能由人做。用中文简短回答。'
|
|
283
|
-
+ (projectMemoryBlock(this.db, input.targetId) ? `\n${projectMemoryBlock(this.db, input.targetId)}` : '')
|
|
284
|
-
: `你在 HAMS 里作为《${peer.memberName}》与人类单聊,身份是成员 ${peer.memberId}。`
|
|
285
|
-
+ '涉及工作项时用 HAMS 工具读写事实,不要编造状态;关单与审批只能由人做。用中文简短回答。'
|
|
286
|
-
+ (memberMemoryBlock(this.db, peer.memberId) ? `\n${memberMemoryBlock(this.db, peer.memberId)}` : '')
|
|
287
|
-
const { sessionId } = await this.runtime.ensure({
|
|
288
|
-
sessionId: sameConfig ? existing.sessionId : undefined,
|
|
289
|
-
key: `chat:${input.kind}:${input.targetId}`,
|
|
290
|
-
memberId: peer.memberId,
|
|
291
|
-
memberName: peer.memberName,
|
|
292
|
-
memberVersion: peer.memberVersion,
|
|
293
|
-
agentPreset: peer.agentPreset,
|
|
294
|
-
model,
|
|
295
|
-
title: input.kind === 'project' ? `项目群聊 · ${peer.title}` : `单聊 · ${peer.memberName}`,
|
|
296
|
-
context,
|
|
297
|
-
})
|
|
298
|
-
const at = Date.now()
|
|
299
|
-
const pointer: ThreadPointer = {
|
|
300
|
-
sessionId,
|
|
301
|
-
memberId: peer.memberId,
|
|
302
|
-
memberVersion: peer.memberVersion,
|
|
303
|
-
agentPreset: peer.agentPreset,
|
|
304
|
-
model,
|
|
305
|
-
at,
|
|
306
|
-
}
|
|
307
|
-
if (!sameConfig || !existing) this.savePointer(input.kind, input.targetId, pointer)
|
|
308
|
-
return { ...peer, model, sessionId, createdAt: existing?.at ?? at, updatedAt: at }
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
async send(ctx: CommandContext, input: { kind: ChatKind; targetId: string; text: string }): Promise<{ sessionId: string; accepted: true }> {
|
|
312
|
-
const text = String(input.text || '').trim()
|
|
313
|
-
if (!text) throw new HamsError('STATE_INVALID', '消息不能为空')
|
|
314
|
-
if (text.length > 8000) throw new HamsError('STATE_INVALID', '单条消息不能超过 8000 字')
|
|
315
|
-
const thread = await this.open(ctx, { kind: input.kind, targetId: input.targetId })
|
|
316
|
-
// 单聊里把 bot 从项目同步过来的能力记忆带进每一轮,不依赖会话首次创建的上下文。
|
|
317
|
-
const memory = thread.kind === 'member' ? memberMemoryBlock(this.db, thread.memberId) : ''
|
|
318
|
-
await this.runtime.send(thread.sessionId, memory ? `${memory}\n\n${text}` : text)
|
|
319
|
-
return { sessionId: thread.sessionId, accepted: true }
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async read(ctx: CommandContext, input: { kind: ChatKind; targetId: string }): Promise<{ thread: ChatThreadView | null; messages: ChatMessage[]; running: boolean; available: boolean; reason?: string; error?: string }> {
|
|
323
|
-
this.human(ctx)
|
|
324
|
-
if (!this.runtime.available) {
|
|
325
|
-
return { thread: null, messages: [], running: false, available: false, reason: this.runtime.unavailableReason || '宿主没有提供 agent 会话服务' }
|
|
326
|
-
}
|
|
327
|
-
const pointer = this.pointer(input.kind, input.targetId)
|
|
328
|
-
if (!pointer) return { thread: null, messages: [], running: false, available: true }
|
|
329
|
-
let peer: ChatPeer
|
|
330
|
-
try {
|
|
331
|
-
peer = this.peer(ctx, input.kind, input.targetId)
|
|
332
|
-
} catch (error) {
|
|
333
|
-
// 目标暂时不可用(bot 停岗 / 项目没推进 bot):把已有消息照常返回,界面只显示提示。
|
|
334
|
-
peer = {
|
|
335
|
-
kind: input.kind,
|
|
336
|
-
targetId: input.targetId,
|
|
337
|
-
title: input.kind === 'project' ? '项目' : '智能体',
|
|
338
|
-
memberId: pointer.memberId,
|
|
339
|
-
memberName: '',
|
|
340
|
-
memberVersion: pointer.memberVersion,
|
|
341
|
-
agentPreset: pointer.agentPreset,
|
|
342
|
-
model: pointer.model,
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
const { messages, running, error } = await this.runtime.read(pointer.sessionId)
|
|
346
|
-
return {
|
|
347
|
-
thread: { ...peer, sessionId: pointer.sessionId, createdAt: pointer.at, updatedAt: pointer.at },
|
|
348
|
-
messages,
|
|
349
|
-
running,
|
|
350
|
-
available: true,
|
|
351
|
-
...(error ? { error } : {}),
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
async stop(ctx: CommandContext, input: { kind: ChatKind; targetId: string }): Promise<{ stopped: true }> {
|
|
356
|
-
this.human(ctx)
|
|
357
|
-
this.requireRuntime()
|
|
358
|
-
const pointer = this.pointer(input.kind, input.targetId)
|
|
359
|
-
if (!pointer) throw new HamsError('STATE_INVALID', '还没有对话可以停止')
|
|
360
|
-
await this.runtime.stop(pointer.sessionId)
|
|
361
|
-
return { stopped: true }
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async presets(): Promise<ChatPresetRow[]> {
|
|
365
|
-
if (!this.runtime.available) return []
|
|
366
|
-
return this.runtime.presets()
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async models(): Promise<ChatModelRow[]> {
|
|
370
|
-
if (!this.runtime.available) return []
|
|
371
|
-
return this.runtime.models()
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// ───────────────────────── 项目群:团队与多 bot 群聊 ─────────────────────────
|
|
375
|
-
|
|
376
|
-
private projectRow(projectId: string): { id: string; name: string; coordinator_id: string | null; owner_human_id: string; status: string } {
|
|
377
|
-
const row = this.db
|
|
378
|
-
.prepare('SELECT id, name, coordinator_id, owner_human_id, status, deleted_at FROM project WHERE id = ?')
|
|
379
|
-
.get(projectId) as
|
|
380
|
-
| { id: string; name: string; coordinator_id: string | null; owner_human_id: string; status: string; deleted_at: number | null }
|
|
381
|
-
| undefined
|
|
382
|
-
if (!row || row.deleted_at) throw new HamsError('STATE_INVALID', `项目不存在:${projectId}`, { nextStep: '刷新项目列表后重试' })
|
|
383
|
-
return row
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
private memberRow(memberId: string): { id: string; name: string; kind: string; status: string } | undefined {
|
|
387
|
-
return this.db.prepare('SELECT id, name, kind, status FROM member WHERE id = ?').get(memberId) as
|
|
388
|
-
| { id: string; name: string; kind: string; status: string }
|
|
389
|
-
| undefined
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
private participantRow(projectId: string, memberId: string): { kind: 'member' | 'temp'; brief: string; removed_at: number | null } | undefined {
|
|
393
|
-
return this.db
|
|
394
|
-
.prepare('SELECT kind, brief, removed_at FROM project_participant WHERE project_id = ? AND member_id = ?')
|
|
395
|
-
.get(projectId, memberId) as { kind: 'member' | 'temp'; brief: string; removed_at: number | null } | undefined
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
private requireProjectAdmin(ctx: CommandContext): ReturnType<typeof resolvePrincipal> {
|
|
399
|
-
const principal = resolvePrincipal(this.db, ctx.principalId, this.now())
|
|
400
|
-
if (principal.kind !== 'human') {
|
|
401
|
-
throw new HamsError('POLICY_DENIED', '只有项目管理员能手动调整团队成员;推进 bot 请用 hams_group_spawn_bot', {
|
|
402
|
-
nextStep: '让管理员在项目「团队」里加人',
|
|
403
|
-
})
|
|
404
|
-
}
|
|
405
|
-
requireCapability(principal, CAPABILITY.projectManage)
|
|
406
|
-
return principal
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
private pointerAt(key: string): ThreadPointer | null {
|
|
410
|
-
const row = this.db.prepare('SELECT value FROM hams_meta WHERE key = ?').get(key) as { value: string } | undefined
|
|
411
|
-
if (!row) return null
|
|
412
|
-
try {
|
|
413
|
-
const parsed = JSON.parse(row.value) as ThreadPointer
|
|
414
|
-
return parsed && typeof parsed.sessionId === 'string' ? parsed : null
|
|
415
|
-
} catch {
|
|
416
|
-
return null
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
private savePointerAt(key: string, pointer: ThreadPointer): void {
|
|
421
|
-
this.db.prepare('INSERT OR REPLACE INTO hams_meta (key, value) VALUES (?, ?)').run(key, JSON.stringify(pointer))
|
|
422
|
-
this.db.prepare('INSERT OR REPLACE INTO hams_meta (key, value) VALUES (?, ?)').run(`chat:session:${pointer.sessionId}`, pointer.memberId)
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/** 群聊里某个成员自己的会话 key;推进 bot 沿用 chat:project:<id>,旧消息不断。 */
|
|
426
|
-
private groupKey(projectId: string, memberId: string, isCoordinator: boolean): string {
|
|
427
|
-
return isCoordinator ? `chat:project:${projectId}` : `chat:group:${projectId}:${memberId}`
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
private async ensureGroupSession(
|
|
431
|
-
project: { id: string; name: string },
|
|
432
|
-
member: { memberId: string; memberName: string; memberVersion: number; agentPreset: string; model: string | null },
|
|
433
|
-
isCoordinator: boolean,
|
|
434
|
-
context: string,
|
|
435
|
-
): Promise<{ sessionId: string; running: boolean }> {
|
|
436
|
-
this.requireRuntime()
|
|
437
|
-
const key = this.groupKey(project.id, member.memberId, isCoordinator)
|
|
438
|
-
const existing = this.pointerAt(key)
|
|
439
|
-
let model = member.model
|
|
440
|
-
if (!model) {
|
|
441
|
-
const fallback = this.runtime.defaultModel ? await this.runtime.defaultModel() : null
|
|
442
|
-
model = fallback ? `${fallback.provider}/${fallback.model}` : null
|
|
443
|
-
}
|
|
444
|
-
if (!model) {
|
|
445
|
-
throw new HamsError('HOST_CAPABILITY_MISSING', `${member.memberName} 没有配置模型,宿主也没有默认模型`, {
|
|
446
|
-
nextStep: '在智能体「配置」里选一个模型并保存;或在宿主设置里指定默认模型',
|
|
447
|
-
})
|
|
448
|
-
}
|
|
449
|
-
const same = Boolean(
|
|
450
|
-
existing &&
|
|
451
|
-
existing.memberVersion === member.memberVersion &&
|
|
452
|
-
existing.agentPreset === member.agentPreset &&
|
|
453
|
-
(existing.model || null) === model,
|
|
454
|
-
)
|
|
455
|
-
const { sessionId } = await this.runtime.ensure({
|
|
456
|
-
sessionId: same ? existing!.sessionId : undefined,
|
|
457
|
-
key,
|
|
458
|
-
memberId: member.memberId,
|
|
459
|
-
memberName: member.memberName,
|
|
460
|
-
memberVersion: member.memberVersion,
|
|
461
|
-
agentPreset: member.agentPreset,
|
|
462
|
-
model,
|
|
463
|
-
title: `项目群 · ${project.name} · ${member.memberName}`,
|
|
464
|
-
context,
|
|
465
|
-
})
|
|
466
|
-
const at = this.now()
|
|
467
|
-
const pointer: ThreadPointer = {
|
|
468
|
-
sessionId,
|
|
469
|
-
memberId: member.memberId,
|
|
470
|
-
memberVersion: member.memberVersion,
|
|
471
|
-
agentPreset: member.agentPreset,
|
|
472
|
-
model,
|
|
473
|
-
at,
|
|
474
|
-
}
|
|
475
|
-
this.savePointerAt(key, pointer)
|
|
476
|
-
this.db.prepare('INSERT OR REPLACE INTO hams_meta (key, value) VALUES (?, ?)').run(`chat:group:session:${sessionId}`, project.id)
|
|
477
|
-
return { sessionId, running: false }
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/** 群里现在有谁:推进 bot + 参与者(手动加入的常驻 bot 与临时 bot)。 */
|
|
481
|
-
private teamRows(projectId: string): Array<{ memberId: string; name: string; kind: TeamKind; status: string; brief: string; addedAt?: number }> {
|
|
482
|
-
const project = this.projectRow(projectId)
|
|
483
|
-
const rows: Array<{ memberId: string; name: string; kind: TeamKind; status: string; brief: string; addedAt?: number }> = []
|
|
484
|
-
if (project.coordinator_id) {
|
|
485
|
-
const coordinator = this.memberRow(project.coordinator_id)
|
|
486
|
-
if (coordinator) rows.push({ memberId: coordinator.id, name: coordinator.name, kind: 'coordinator', status: coordinator.status, brief: '项目推进 bot:拆任务、分配、汇总' })
|
|
487
|
-
}
|
|
488
|
-
const members = this.db
|
|
489
|
-
.prepare(
|
|
490
|
-
`SELECT pp.member_id, pp.kind, pp.brief, pp.added_at, m.name, m.status
|
|
491
|
-
FROM project_participant pp JOIN member m ON m.id = pp.member_id
|
|
492
|
-
WHERE pp.project_id = ? AND pp.removed_at IS NULL
|
|
493
|
-
ORDER BY pp.added_at`,
|
|
494
|
-
)
|
|
495
|
-
.all(projectId) as Array<{ member_id: string; kind: 'member' | 'temp'; brief: string; added_at: number; name: string; status: string }>
|
|
496
|
-
for (const row of members) {
|
|
497
|
-
rows.push({ memberId: row.member_id, name: row.name, kind: row.kind, status: row.status, brief: row.brief, addedAt: row.added_at })
|
|
498
|
-
}
|
|
499
|
-
return rows
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
private rosterText(projectId: string): string {
|
|
503
|
-
const project = this.projectRow(projectId)
|
|
504
|
-
const rows = this.teamRows(projectId)
|
|
505
|
-
const lines = rows.map((row) => `- ${row.name}(${row.kind === 'coordinator' ? '推进 bot' : row.kind === 'temp' ? '临时 bot' : '成员'},id=${row.memberId})`)
|
|
506
|
-
return `【项目群 ${project.name}】团队成员:\n${lines.join('\n')}`
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
private recentTranscript(projectId: string, limit = 12): string {
|
|
510
|
-
const rows = this.db
|
|
511
|
-
.prepare('SELECT author_name, text, kind, to_member_id FROM project_message WHERE project_id = ? ORDER BY at DESC, id DESC LIMIT ?')
|
|
512
|
-
.all(projectId, limit) as Array<{ author_name: string; text: string; kind: string; to_member_id: string | null }>
|
|
513
|
-
return rows
|
|
514
|
-
.reverse()
|
|
515
|
-
.map((row) => `${row.author_name}${row.kind === 'mention' ? ' @某人' : ''}:${row.text}`)
|
|
516
|
-
.join('\n')
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
private appendGroupMessage(input: {
|
|
520
|
-
projectId: string
|
|
521
|
-
authorKind: 'human' | 'bot' | 'system'
|
|
522
|
-
authorId: string
|
|
523
|
-
authorName: string
|
|
524
|
-
text: string
|
|
525
|
-
kind?: 'chat' | 'mention' | 'system'
|
|
526
|
-
toMemberId?: string | null
|
|
527
|
-
sessionId?: string | null
|
|
528
|
-
}): string {
|
|
529
|
-
const id = this.id()
|
|
530
|
-
this.db
|
|
531
|
-
.prepare(
|
|
532
|
-
`INSERT INTO project_message (id, project_id, author_kind, author_id, author_name, text, at, kind, to_member_id, session_id)
|
|
533
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
534
|
-
)
|
|
535
|
-
.run(
|
|
536
|
-
id,
|
|
537
|
-
input.projectId,
|
|
538
|
-
input.authorKind,
|
|
539
|
-
input.authorId,
|
|
540
|
-
input.authorName,
|
|
541
|
-
input.text,
|
|
542
|
-
this.now(),
|
|
543
|
-
input.kind || 'chat',
|
|
544
|
-
input.toMemberId ?? null,
|
|
545
|
-
input.sessionId ?? null,
|
|
546
|
-
)
|
|
547
|
-
return id
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/** 把每个成员会话里新产生的助手回复合并进群聊记录(按 seq 游标,幂等)。 */
|
|
551
|
-
private async syncGroup(projectId: string): Promise<void> {
|
|
552
|
-
const project = this.projectRow(projectId)
|
|
553
|
-
const members = this.teamRows(projectId)
|
|
554
|
-
for (const member of members) {
|
|
555
|
-
const isCoordinator = member.kind === 'coordinator'
|
|
556
|
-
const pointer = this.pointerAt(this.groupKey(projectId, member.memberId, isCoordinator))
|
|
557
|
-
if (!pointer) continue
|
|
558
|
-
const cursor = this.db.prepare('SELECT last_seq FROM chat_cursor WHERE session_id = ?').get(pointer.sessionId) as
|
|
559
|
-
| { last_seq: number }
|
|
560
|
-
| undefined
|
|
561
|
-
let lastSeq = cursor?.last_seq ?? 0
|
|
562
|
-
const { messages } = await this.runtime.read(pointer.sessionId)
|
|
563
|
-
for (const message of messages) {
|
|
564
|
-
if (message.role !== 'assistant') continue
|
|
565
|
-
const seq = Number(message.seq ?? 0)
|
|
566
|
-
if (seq <= lastSeq) continue
|
|
567
|
-
const messageId = this.appendGroupMessage({
|
|
568
|
-
projectId,
|
|
569
|
-
authorKind: 'bot',
|
|
570
|
-
authorId: member.memberId,
|
|
571
|
-
authorName: member.name,
|
|
572
|
-
text: message.text,
|
|
573
|
-
kind: 'chat',
|
|
574
|
-
sessionId: pointer.sessionId,
|
|
575
|
-
})
|
|
576
|
-
this.collectMemories(projectId, {
|
|
577
|
-
id: messageId,
|
|
578
|
-
authorKind: 'bot',
|
|
579
|
-
authorId: member.memberId,
|
|
580
|
-
authorName: member.name,
|
|
581
|
-
text: message.text,
|
|
582
|
-
})
|
|
583
|
-
lastSeq = seq
|
|
584
|
-
}
|
|
585
|
-
this.db
|
|
586
|
-
.prepare('INSERT OR REPLACE INTO chat_cursor (session_id, project_id, last_seq) VALUES (?, ?, ?)')
|
|
587
|
-
.run(pointer.sessionId, project.id, lastSeq)
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
async team(ctx: CommandContext, input: { projectId: string }): Promise<{ projectId: string; title: string; coordinator: TeamMemberView | null; members: TeamMemberView[] }> {
|
|
592
|
-
resolvePrincipal(this.db, ctx.principalId, this.now())
|
|
593
|
-
const project = this.projectRow(input.projectId)
|
|
594
|
-
const rows = this.teamRows(input.projectId)
|
|
595
|
-
const views: TeamMemberView[] = []
|
|
596
|
-
for (const row of rows) {
|
|
597
|
-
const isCoordinator = row.kind === 'coordinator'
|
|
598
|
-
const pointer = this.pointerAt(this.groupKey(input.projectId, row.memberId, isCoordinator))
|
|
599
|
-
let running = false
|
|
600
|
-
if (pointer && this.runtime.available && this.runtime.status) {
|
|
601
|
-
try {
|
|
602
|
-
running = (await this.runtime.status(pointer.sessionId)).running
|
|
603
|
-
} catch {
|
|
604
|
-
running = false
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
views.push({
|
|
608
|
-
memberId: row.memberId,
|
|
609
|
-
name: row.name,
|
|
610
|
-
kind: row.kind,
|
|
611
|
-
status: row.status,
|
|
612
|
-
brief: row.brief,
|
|
613
|
-
sessionId: pointer?.sessionId ?? null,
|
|
614
|
-
running,
|
|
615
|
-
})
|
|
616
|
-
}
|
|
617
|
-
const coordinator = views.find((row) => row.kind === 'coordinator') ?? null
|
|
618
|
-
return { projectId: input.projectId, title: project.name, coordinator, members: views.filter((row) => row.kind !== 'coordinator') }
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/** 手动把在岗 bot 拉进项目群。重复加入是幂等的(撤销软删)。 */
|
|
622
|
-
addParticipant(ctx: CommandContext, input: { projectId: string; memberId: string }): { ok: true; memberId: string } {
|
|
623
|
-
this.requireProjectAdmin(ctx)
|
|
624
|
-
const project = this.projectRow(input.projectId)
|
|
625
|
-
if (project.status !== 'active') throw new HamsError('STATE_INVALID', '归档/暂停的项目不能再加人')
|
|
626
|
-
if (project.coordinator_id === input.memberId) throw new HamsError('STATE_INVALID', '推进 bot 已经在群里了')
|
|
627
|
-
const bot = this.bot(input.memberId)
|
|
628
|
-
const existing = this.participantRow(input.projectId, input.memberId)
|
|
629
|
-
this.db
|
|
630
|
-
.prepare(
|
|
631
|
-
`INSERT INTO project_participant (project_id, member_id, kind, brief, added_by, added_at, removed_by, removed_at)
|
|
632
|
-
VALUES (?, ?, 'member', '', ?, ?, NULL, NULL)
|
|
633
|
-
ON CONFLICT(project_id, member_id) DO UPDATE SET kind = 'member', removed_by = NULL, removed_at = NULL, added_by = excluded.added_by, added_at = excluded.added_at`,
|
|
634
|
-
)
|
|
635
|
-
.run(input.projectId, input.memberId, ctx.principalId, this.now())
|
|
636
|
-
void existing
|
|
637
|
-
void bot
|
|
638
|
-
return { ok: true, memberId: input.memberId }
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
removeParticipant(ctx: CommandContext, input: { projectId: string; memberId: string }): { ok: true } {
|
|
642
|
-
this.requireProjectAdmin(ctx)
|
|
643
|
-
const project = this.projectRow(input.projectId)
|
|
644
|
-
if (project.coordinator_id === input.memberId) throw new HamsError('STATE_INVALID', '推进 bot 不能从群里移除;改项目推进 bot 请走项目设置')
|
|
645
|
-
const row = this.participantRow(input.projectId, input.memberId)
|
|
646
|
-
if (!row || row.removed_at) throw new HamsError('STATE_INVALID', '这个人不在群里')
|
|
647
|
-
const at = this.now()
|
|
648
|
-
this.db
|
|
649
|
-
.prepare('UPDATE project_participant SET removed_at = ?, removed_by = ? WHERE project_id = ? AND member_id = ?')
|
|
650
|
-
.run(at, ctx.principalId, input.projectId, input.memberId)
|
|
651
|
-
if (row.kind === 'temp') {
|
|
652
|
-
this.db
|
|
653
|
-
.prepare("UPDATE member SET status = 'retired', updated_at = ? WHERE id = ? AND status = 'active'")
|
|
654
|
-
.run(at, input.memberId)
|
|
655
|
-
void this.stopGroupSession(input.projectId, input.memberId, false)
|
|
656
|
-
}
|
|
657
|
-
return { ok: true }
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
private async stopGroupSession(projectId: string, memberId: string, isCoordinator: boolean): Promise<void> {
|
|
661
|
-
const pointer = this.pointerAt(this.groupKey(projectId, memberId, isCoordinator))
|
|
662
|
-
if (!pointer || !this.runtime.available) return
|
|
663
|
-
try {
|
|
664
|
-
await this.runtime.stop(pointer.sessionId)
|
|
665
|
-
} catch {
|
|
666
|
-
// 会话可能已被宿主回收;停不掉不影响移除
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* 推进 bot 自己创建临时 bot:只属于这个项目群(member.scope_project_id),
|
|
672
|
-
* 退休时随参与者一起下线。
|
|
673
|
-
*/
|
|
674
|
-
spawnTempBot(ctx: CommandContext, input: { projectId: string; name: string; brief?: string }): { memberId: string; name: string } {
|
|
675
|
-
const principal = resolvePrincipal(this.db, ctx.principalId, this.now())
|
|
676
|
-
const project = this.projectRow(input.projectId)
|
|
677
|
-
if (project.status !== 'active') throw new HamsError('STATE_INVALID', '项目不在进行中,不能创建临时 bot')
|
|
678
|
-
const isCoordinator = principal.kind === 'bot' && project.coordinator_id === principal.memberId
|
|
679
|
-
if (principal.kind === 'human') {
|
|
680
|
-
requireCapability(principal, CAPABILITY.projectManage)
|
|
681
|
-
} else if (!isCoordinator) {
|
|
682
|
-
throw new HamsError('POLICY_DENIED', '只有项目推进 bot 或项目管理员能创建临时 bot', {
|
|
683
|
-
nextStep: '让推进 bot 在群里自己建,或让管理员在「团队」里建',
|
|
684
|
-
})
|
|
685
|
-
}
|
|
686
|
-
const name = String(input.name || '').trim()
|
|
687
|
-
if (!name) throw new HamsError('STATE_INVALID', '临时 bot 必须有名字')
|
|
688
|
-
if (name.length > 24) throw new HamsError('STATE_INVALID', '名字最多 24 个字')
|
|
689
|
-
const brief = String(input.brief || '').trim().slice(0, 500)
|
|
690
|
-
const at = this.now()
|
|
691
|
-
const memberId = this.id()
|
|
692
|
-
this.db
|
|
693
|
-
.prepare(
|
|
694
|
-
`INSERT INTO member (id, kind, name, domain, responsibility, role, status, current_version, owner_human_id, scope_project_id, created_at, updated_at)
|
|
695
|
-
VALUES (?, 'bot', ?, NULL, ?, 'executor', 'active', 1, ?, ?, ?, ?)`,
|
|
696
|
-
)
|
|
697
|
-
.run(memberId, name, brief, project.owner_human_id, project.id, at, at)
|
|
698
|
-
const digest = createHash('sha256').update(`${name}\n${brief}`).digest('hex').slice(0, 16)
|
|
699
|
-
this.db
|
|
700
|
-
.prepare(
|
|
701
|
-
`INSERT INTO member_version (id, member_id, version, agent_preset, model, memory_scope_json, prompt_digest, change_class, published_by, published_at)
|
|
702
|
-
VALUES (?, ?, 1, 'hams-bot', NULL, '{}', ?, 'major', ?, ?)`,
|
|
703
|
-
)
|
|
704
|
-
.run(this.id(), memberId, digest, principal.memberId, at)
|
|
705
|
-
const grant = this.db.prepare('INSERT OR IGNORE INTO member_permission (member_id, capability, scope, granted_by, granted_at) VALUES (?, ?, ?, ?, ?)')
|
|
706
|
-
for (const capability of ['work_item.execute', 'work_item.claim', 'evidence.append']) {
|
|
707
|
-
grant.run(memberId, capability, `project:${project.id}`, principal.memberId, at)
|
|
708
|
-
}
|
|
709
|
-
this.db
|
|
710
|
-
.prepare(
|
|
711
|
-
`INSERT INTO project_participant (project_id, member_id, kind, brief, added_by, added_at)
|
|
712
|
-
VALUES (?, ?, 'temp', ?, ?, ?)`,
|
|
713
|
-
)
|
|
714
|
-
.run(project.id, memberId, brief, principal.memberId, at)
|
|
715
|
-
return { memberId, name }
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
async groupRead(ctx: CommandContext, input: { projectId: string }): Promise<GroupChatView> {
|
|
719
|
-
resolvePrincipal(this.db, ctx.principalId, this.now())
|
|
720
|
-
const project = this.projectRow(input.projectId)
|
|
721
|
-
if (!this.runtime.available) {
|
|
722
|
-
return { projectId: input.projectId, title: project.name, coordinator: null, members: [], messages: [], running: false, available: false, reason: this.runtime.unavailableReason }
|
|
723
|
-
}
|
|
724
|
-
await this.syncGroup(input.projectId)
|
|
725
|
-
const team = await this.team(ctx, { projectId: input.projectId })
|
|
726
|
-
const rows = this.db
|
|
727
|
-
.prepare('SELECT * FROM project_message WHERE project_id = ? ORDER BY at, id LIMIT 400')
|
|
728
|
-
.all(input.projectId) as Array<{
|
|
729
|
-
id: string
|
|
730
|
-
author_kind: 'human' | 'bot' | 'system'
|
|
731
|
-
author_id: string
|
|
732
|
-
author_name: string
|
|
733
|
-
text: string
|
|
734
|
-
at: number
|
|
735
|
-
kind: 'chat' | 'mention' | 'system'
|
|
736
|
-
to_member_id: string | null
|
|
737
|
-
}>
|
|
738
|
-
const messages: GroupMessageView[] = rows.map((row) => ({
|
|
739
|
-
id: row.id,
|
|
740
|
-
authorKind: row.author_kind,
|
|
741
|
-
authorId: row.author_id,
|
|
742
|
-
authorName: row.author_name,
|
|
743
|
-
text: row.text,
|
|
744
|
-
at: row.at,
|
|
745
|
-
kind: row.kind,
|
|
746
|
-
toMemberId: row.to_member_id,
|
|
747
|
-
}))
|
|
748
|
-
const last = messages[messages.length - 1]
|
|
749
|
-
const coordinatorRow = this.db.prepare('SELECT coordinator_id FROM project WHERE id = ?').get(input.projectId) as { coordinator_id: string | null }
|
|
750
|
-
const coordinatorPointer = coordinatorRow.coordinator_id ? this.pointerAt(this.groupKey(input.projectId, coordinatorRow.coordinator_id, true)) : null
|
|
751
|
-
let error: string | undefined
|
|
752
|
-
if (coordinatorPointer) {
|
|
753
|
-
try {
|
|
754
|
-
const state = await this.runtime.read(coordinatorPointer.sessionId)
|
|
755
|
-
error = state.error
|
|
756
|
-
} catch {
|
|
757
|
-
error = undefined
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
return {
|
|
761
|
-
projectId: input.projectId,
|
|
762
|
-
title: project.name,
|
|
763
|
-
coordinator: team.coordinator,
|
|
764
|
-
members: team.members,
|
|
765
|
-
messages,
|
|
766
|
-
running: Boolean(team.coordinator?.running || team.members.some((row) => row.running)),
|
|
767
|
-
available: true,
|
|
768
|
-
...(error ? { error } : {}),
|
|
769
|
-
}
|
|
770
|
-
void last
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
/** 人往群里发一句话:默认交给推进 bot;它可以用 hams_group_send @ 具体成员。 */
|
|
774
|
-
async groupSend(
|
|
775
|
-
ctx: CommandContext,
|
|
776
|
-
input: { projectId: string; text: string },
|
|
777
|
-
): Promise<{ messageId: string; sessionId: string; mentions: string[] }> {
|
|
778
|
-
this.human(ctx)
|
|
779
|
-
this.requireRuntime()
|
|
780
|
-
const text = String(input.text || '').trim()
|
|
781
|
-
if (!text) throw new HamsError('STATE_INVALID', '消息不能为空')
|
|
782
|
-
if (text.length > 8000) throw new HamsError('STATE_INVALID', '单条消息不能超过 8000 字')
|
|
783
|
-
const project = this.projectRow(input.projectId)
|
|
784
|
-
if (!project.coordinator_id) throw new HamsError('STATE_INVALID', '这个项目还没有推进 bot,群聊没有回答的人', { nextStep: '先在项目里指定推进 bot' })
|
|
785
|
-
const human = this.memberRow(ctx.principalId)
|
|
786
|
-
const humanName = human?.name || ctx.principalId
|
|
787
|
-
// 人类在群里 @ 谁,就把这句话直接交给谁;没 @ 才由推进 bot 接。
|
|
788
|
-
const mentioned = this.mentionedMembers(input.projectId, text, ctx.principalId)
|
|
789
|
-
const targets = mentioned.length
|
|
790
|
-
? mentioned
|
|
791
|
-
: [
|
|
792
|
-
this.teamRows(input.projectId).find((row) => row.memberId === project.coordinator_id) ?? {
|
|
793
|
-
memberId: project.coordinator_id,
|
|
794
|
-
name: this.memberRow(project.coordinator_id)?.name || '推进 bot',
|
|
795
|
-
kind: 'coordinator' as TeamKind,
|
|
796
|
-
status: 'active',
|
|
797
|
-
brief: '项目推进 bot',
|
|
798
|
-
},
|
|
799
|
-
]
|
|
800
|
-
const first = targets[0]
|
|
801
|
-
const messageId = this.appendGroupMessage({
|
|
802
|
-
projectId: input.projectId,
|
|
803
|
-
authorKind: 'human',
|
|
804
|
-
authorId: ctx.principalId,
|
|
805
|
-
authorName: humanName,
|
|
806
|
-
text,
|
|
807
|
-
kind: mentioned.length ? 'mention' : 'chat',
|
|
808
|
-
toMemberId: mentioned.length ? first.memberId : null,
|
|
809
|
-
})
|
|
810
|
-
this.collectMemories(input.projectId, {
|
|
811
|
-
id: messageId,
|
|
812
|
-
authorKind: 'human',
|
|
813
|
-
authorId: ctx.principalId,
|
|
814
|
-
authorName: humanName,
|
|
815
|
-
text,
|
|
816
|
-
})
|
|
817
|
-
const sessionIds: string[] = []
|
|
818
|
-
for (const row of targets) {
|
|
819
|
-
sessionIds.push(await this.deliverHumanMessage(project, row, text, humanName, mentioned.length > 0))
|
|
820
|
-
}
|
|
821
|
-
return { messageId, sessionId: sessionIds[0], mentions: mentioned.map((row) => row.memberId) }
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
/**
|
|
825
|
-
* 群里被 @ 的 bot:先看团队成员,再看全局在岗 bot。
|
|
826
|
-
* @ 一个还没进群的 bot = 明确要它来排查,直接拉进群(人是发起者,写进参与者)。
|
|
827
|
-
*/
|
|
828
|
-
private mentionedMembers(projectId: string, text: string, actorId: string): Array<{ memberId: string; name: string; kind: TeamKind; status: string; brief: string }> {
|
|
829
|
-
const bots = this.db
|
|
830
|
-
.prepare(
|
|
831
|
-
`SELECT id AS memberId, name FROM member
|
|
832
|
-
WHERE kind = 'bot' AND status = 'active'
|
|
833
|
-
AND (scope_project_id IS NULL OR scope_project_id = ?)
|
|
834
|
-
ORDER BY length(name) DESC, name`,
|
|
835
|
-
)
|
|
836
|
-
.all(projectId) as Array<{ memberId: string; name: string }>
|
|
837
|
-
let rest = text
|
|
838
|
-
const hits: string[] = []
|
|
839
|
-
for (const bot of bots) {
|
|
840
|
-
const token = new RegExp(`[@@]\\s*${escapeRegExp(bot.name)}`)
|
|
841
|
-
if (!token.test(rest)) continue
|
|
842
|
-
hits.push(bot.memberId)
|
|
843
|
-
rest = rest.replace(new RegExp(`[@@]\\s*${escapeRegExp(bot.name)}`, 'g'), '@')
|
|
844
|
-
}
|
|
845
|
-
if (!hits.length) return []
|
|
846
|
-
for (const memberId of hits) {
|
|
847
|
-
const member = this.teamRows(projectId).find((row) => row.memberId === memberId)
|
|
848
|
-
if (!member) {
|
|
849
|
-
this.db
|
|
850
|
-
.prepare(
|
|
851
|
-
`INSERT INTO project_participant (project_id, member_id, kind, brief, added_by, added_at, removed_by, removed_at)
|
|
852
|
-
VALUES (?, ?, 'member', '被 @ 拉进群', ?, ?, NULL, NULL)
|
|
853
|
-
ON CONFLICT(project_id, member_id) DO UPDATE SET removed_by = NULL, removed_at = NULL`,
|
|
854
|
-
)
|
|
855
|
-
.run(projectId, memberId, actorId, this.now())
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
const team = this.teamRows(projectId)
|
|
859
|
-
return hits
|
|
860
|
-
.map((memberId) => team.find((row) => row.memberId === memberId))
|
|
861
|
-
.filter((row): row is (typeof team)[number] => Boolean(row))
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* 群聊里的自动记忆收集:规则提取候选,写进项目记忆;真的新增了才在群里留一条系统记录。
|
|
866
|
-
* 只处理真人和 bot 的发言,不处理系统记录,避免自我循环。
|
|
867
|
-
*/
|
|
868
|
-
private collectMemories(
|
|
869
|
-
projectId: string,
|
|
870
|
-
message: { id: string; authorKind: 'human' | 'bot'; authorId: string; authorName: string; text: string },
|
|
871
|
-
): number {
|
|
872
|
-
try {
|
|
873
|
-
const candidates = extractMemoryCandidates(message.text, {
|
|
874
|
-
authorKind: message.authorKind,
|
|
875
|
-
authorId: message.authorId,
|
|
876
|
-
authorName: message.authorName,
|
|
877
|
-
})
|
|
878
|
-
if (!candidates.length) return 0
|
|
879
|
-
const created: typeof candidates = []
|
|
880
|
-
candidates.forEach((candidate, index) => {
|
|
881
|
-
const result = createMemory(this.db, this.id, this.now(), message.authorId, {
|
|
882
|
-
projectId,
|
|
883
|
-
kind: candidate.kind,
|
|
884
|
-
title: candidate.title,
|
|
885
|
-
body: candidate.body,
|
|
886
|
-
origin: 'auto',
|
|
887
|
-
memberId: candidate.memberId,
|
|
888
|
-
sourceRef: `message:${message.id}`,
|
|
889
|
-
dedupeKey: `auto:${message.id}:${index}`,
|
|
890
|
-
})
|
|
891
|
-
if (result.created) created.push(candidate)
|
|
892
|
-
})
|
|
893
|
-
if (!created.length) return 0
|
|
894
|
-
this.appendGroupMessage({
|
|
895
|
-
projectId,
|
|
896
|
-
authorKind: 'system',
|
|
897
|
-
authorId: 'system',
|
|
898
|
-
authorName: '记忆',
|
|
899
|
-
text: `自动收集 ${created.length} 条记忆:${created
|
|
900
|
-
.map((candidate) => `[${MEMORY_KIND_LABEL[candidate.kind]}] ${candidate.title}`)
|
|
901
|
-
.join(';')}`,
|
|
902
|
-
kind: 'system',
|
|
903
|
-
})
|
|
904
|
-
return created.length
|
|
905
|
-
} catch {
|
|
906
|
-
// 记忆收集失败不能影响群聊本身;下一轮/下一次消息会再试。
|
|
907
|
-
return 0
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
/** 工具路径:bot 主动把项目里值得记住的内容写进记忆;能力类默认同步给发言的 bot。 */
|
|
912
|
-
rememberBySession(
|
|
913
|
-
sessionId: string,
|
|
914
|
-
input: { title: string; body: string; kind: string; botId?: string | null },
|
|
915
|
-
): { ok: true; memoryId: string; created: boolean; syncedTo: string | null } {
|
|
916
|
-
const projectId = this.projectOfSession(sessionId)
|
|
917
|
-
const speakerId = this.memberOfSession(sessionId)
|
|
918
|
-
if (!projectId || !speakerId) throw new HamsError('POLICY_DENIED', '这个会话不在任何项目群里', { nextStep: '项目记忆只能在项目群里记录' })
|
|
919
|
-
const kind = String(input.kind || 'fact')
|
|
920
|
-
const memberId = kind === 'capability' ? (input.botId || speakerId) : null
|
|
921
|
-
const result = createMemory(this.db, this.id, this.now(), speakerId, {
|
|
922
|
-
projectId,
|
|
923
|
-
kind,
|
|
924
|
-
title: input.title,
|
|
925
|
-
body: input.body,
|
|
926
|
-
origin: 'tool',
|
|
927
|
-
memberId,
|
|
928
|
-
sourceRef: `tool:${sessionId.slice(-8)}`,
|
|
929
|
-
})
|
|
930
|
-
return { ok: true, memoryId: result.memory.id, created: result.created, syncedTo: result.syncedTo }
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
private async deliverHumanMessage(
|
|
934
|
-
project: { id: string; name: string; coordinator_id: string | null },
|
|
935
|
-
row: { memberId: string; name: string; kind: TeamKind; status: string; brief: string },
|
|
936
|
-
text: string,
|
|
937
|
-
humanName: string,
|
|
938
|
-
mentioned: boolean,
|
|
939
|
-
): Promise<string> {
|
|
940
|
-
if (row.status !== 'active') {
|
|
941
|
-
throw new HamsError('STATE_INVALID', `${row.name} 当前不在岗,接不了这条指令`, { nextStep: '在智能体详情里恢复它在岗,或换一个 bot' })
|
|
942
|
-
}
|
|
943
|
-
const member = this.bot(row.memberId)
|
|
944
|
-
const isCoordinator = project.coordinator_id === row.memberId
|
|
945
|
-
const roster = this.rosterText(project.id)
|
|
946
|
-
const context = isCoordinator ? this.coordinatorContext(project, roster) : this.memberContext(project, roster)
|
|
947
|
-
const { sessionId } = await this.ensureGroupSession(project, member, isCoordinator, context)
|
|
948
|
-
const transcript = this.recentTranscript(project.id, 10)
|
|
949
|
-
const prefix = mentioned ? `[人类 ${humanName} @你]` : `[人类 ${humanName}]`
|
|
950
|
-
const instruction = isCoordinator
|
|
951
|
-
? '(你是推进 bot。派活必须用 hams_group_send,并在 text 开头写「@成员名」,让群里看得见你点了谁;' +
|
|
952
|
-
'自己的判断可以直接回答,不要@不存在的人;关单与审批只能由人做。)'
|
|
953
|
-
: '(你被人类点名了。直接回答这条指令;需要别人继续用 hams_group_send;关单与审批只能由人做,不要编造没记录过的事。)'
|
|
954
|
-
const memory = projectMemoryBlock(this.db, project.id)
|
|
955
|
-
await this.runtime.send(
|
|
956
|
-
sessionId,
|
|
957
|
-
`${roster}\n\n${memory ? `${memory}\n\n` : ''}【群聊最近记录】\n${transcript}\n\n${prefix} ${text}\n\n${instruction}`,
|
|
958
|
-
)
|
|
959
|
-
return sessionId
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
private coordinatorContext(project: { id: string; name: string }, roster: string): string {
|
|
963
|
-
return (
|
|
964
|
-
`你在 HAMS 项目《${project.name}》(projectId=${project.id})的群聊里,是这个项目的推进 bot。\n` +
|
|
965
|
-
`${roster}\n` +
|
|
966
|
-
'你可以:看项目工作项(hams_list_project_work)、指派任务(hams_assign_task)、@ 群成员(hams_group_send 带 memberId)、' +
|
|
967
|
-
'建临时 bot(hams_group_spawn_bot,只属于这个项目群)、记项目记忆(hams_remember)。关单与审批只能由人做。用中文简短回答。\n' +
|
|
968
|
-
'遇到决定、偏好、教训、口径或 bot 能力经验,用 hams_remember 记下来;能力类会同步给对应 bot。\n' +
|
|
969
|
-
(projectMemoryBlock(this.db, project.id) ? `${projectMemoryBlock(this.db, project.id)}\n` : '') +
|
|
970
|
-
'硬规则:派活给谁,就在群聊正文里写出「@名字」;hams_group_send 会自动补上开头的 @名字,' +
|
|
971
|
-
'所以调用它时 text 直接写任务内容即可,但最终群里必须能看到你点了谁。'
|
|
972
|
-
)
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
private memberContext(project: { id: string; name: string }, roster: string): string {
|
|
976
|
-
return (
|
|
977
|
-
`你在 HAMS 项目《${project.name}》的群聊里,是群里的一名 bot 成员。\n${roster}\n` +
|
|
978
|
-
'被 @ 时你会收到带上下文的指令;回复直接说话即可,会被合并进群聊记录。需要其他成员时用 hams_group_send,' +
|
|
979
|
-
'正文同样要写出「@名字」。不要替推进 bot 派活,也不要批准关单。' +
|
|
980
|
-
(projectMemoryBlock(this.db, project.id) ? `\n${projectMemoryBlock(this.db, project.id)}` : '')
|
|
981
|
-
)
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
/** 工具路径:以某个会话的身份在群里发言(@ 某个成员)。 */
|
|
985
|
-
async groupSpeakBySession(sessionId: string, input: { toMemberId: string; text: string }): Promise<{ ok: true; messageId: string }> {
|
|
986
|
-
const projectId = this.projectOfSession(sessionId)
|
|
987
|
-
const speakerId = this.memberOfSession(sessionId)
|
|
988
|
-
if (!projectId || !speakerId) throw new HamsError('POLICY_DENIED', '这个会话不在任何项目群里', { nextStep: '群聊只能在项目群会话里用' })
|
|
989
|
-
const project = this.projectRow(projectId)
|
|
990
|
-
const speaker = this.memberRow(speakerId)
|
|
991
|
-
if (!speaker) throw new HamsError('STATE_INVALID', '群会话的成员不存在')
|
|
992
|
-
const isCoordinator = project.coordinator_id === speakerId
|
|
993
|
-
const isMember = Boolean(this.participantRow(projectId, speakerId) && !this.participantRow(projectId, speakerId)!.removed_at)
|
|
994
|
-
if (!isCoordinator && !isMember) throw new HamsError('POLICY_DENIED', '你不在这个项目群里', { nextStep: '让管理员把你加进项目团队' })
|
|
995
|
-
if (!input.toMemberId) throw new HamsError('STATE_INVALID', '要 @ 谁?请给出 memberId(先用 hams_group_members 查)')
|
|
996
|
-
const target = this.teamRows(projectId).find((row) => row.memberId === input.toMemberId)
|
|
997
|
-
if (!target) throw new HamsError('STATE_INVALID', `${input.toMemberId} 不在这个项目群里`, { nextStep: '先用 hams_group_members 看有谁' })
|
|
998
|
-
const raw = String(input.text || '').trim()
|
|
999
|
-
if (!raw) throw new HamsError('STATE_INVALID', '要说的话不能为空')
|
|
1000
|
-
// 派活必须在正文里看得见「@名字」,不能只靠一个 toMemberId 字段。
|
|
1001
|
-
const atPrefix = `@${target.name}`
|
|
1002
|
-
const text = raw.startsWith(atPrefix) ? raw : `${atPrefix} ${raw}`
|
|
1003
|
-
const messageId = this.appendGroupMessage({
|
|
1004
|
-
projectId,
|
|
1005
|
-
authorKind: 'bot',
|
|
1006
|
-
authorId: speakerId,
|
|
1007
|
-
authorName: speaker.name,
|
|
1008
|
-
text,
|
|
1009
|
-
kind: 'mention',
|
|
1010
|
-
toMemberId: input.toMemberId,
|
|
1011
|
-
})
|
|
1012
|
-
this.collectMemories(projectId, {
|
|
1013
|
-
id: messageId,
|
|
1014
|
-
authorKind: 'bot',
|
|
1015
|
-
authorId: speakerId,
|
|
1016
|
-
authorName: speaker.name,
|
|
1017
|
-
text,
|
|
1018
|
-
})
|
|
1019
|
-
const targetMember = this.bot(input.toMemberId)
|
|
1020
|
-
const roster = this.rosterText(projectId)
|
|
1021
|
-
const { sessionId: targetSession } = await this.ensureGroupSession(project, targetMember, false, this.memberContext(project, roster))
|
|
1022
|
-
const transcript = this.recentTranscript(projectId, 10)
|
|
1023
|
-
const memory = projectMemoryBlock(this.db, projectId)
|
|
1024
|
-
await this.runtime.send(
|
|
1025
|
-
targetSession,
|
|
1026
|
-
`${roster}\n\n${memory ? `${memory}\n\n` : ''}【群聊最近记录】\n${transcript}\n\n[${speaker.name} @你] ${text}\n\n` +
|
|
1027
|
-
'(在群聊里回复即可;需要别人继续用 hams_group_send。不要编造没记录过的事。)',
|
|
1028
|
-
)
|
|
1029
|
-
return { ok: true, messageId }
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
/**
|
|
1033
|
-
* 工具路径:推进 bot 指派工作项后,在群里留一条可见的「@接活人」记录。
|
|
1034
|
-
* 只写合并记录,不再唤醒被指派人(真正开跑由会话适配器派发)。
|
|
1035
|
-
*/
|
|
1036
|
-
groupNoteAssignmentBySession(sessionId: string, input: { memberId: string; text: string }): { ok: true; messageId: string } | null {
|
|
1037
|
-
const projectId = this.projectOfSession(sessionId)
|
|
1038
|
-
const speakerId = this.memberOfSession(sessionId)
|
|
1039
|
-
if (!projectId || !speakerId) return null
|
|
1040
|
-
const project = this.projectRow(projectId)
|
|
1041
|
-
if (project.coordinator_id !== speakerId) return null
|
|
1042
|
-
const speaker = this.memberRow(speakerId)
|
|
1043
|
-
if (!speaker) return null
|
|
1044
|
-
const body = String(input.text || '').trim()
|
|
1045
|
-
if (!body) return null
|
|
1046
|
-
let target = this.teamRows(projectId).find((row) => row.memberId === input.memberId)
|
|
1047
|
-
if (!target) {
|
|
1048
|
-
const member = this.memberRow(input.memberId)
|
|
1049
|
-
if (!member || member.kind !== 'bot' || member.status !== 'active') return null
|
|
1050
|
-
this.db
|
|
1051
|
-
.prepare(
|
|
1052
|
-
`INSERT INTO project_participant (project_id, member_id, kind, brief, added_by, added_at, removed_by, removed_at)
|
|
1053
|
-
VALUES (?, ?, 'member', '被指派加入', ?, ?, NULL, NULL)
|
|
1054
|
-
ON CONFLICT(project_id, member_id) DO UPDATE SET removed_by = NULL, removed_at = NULL`,
|
|
1055
|
-
)
|
|
1056
|
-
.run(projectId, member.id, speakerId, this.now())
|
|
1057
|
-
target = this.teamRows(projectId).find((row) => row.memberId === input.memberId)
|
|
1058
|
-
if (!target) return null
|
|
1059
|
-
}
|
|
1060
|
-
const text = body.startsWith(`@${target.name}`) ? body : `@${target.name} ${body}`
|
|
1061
|
-
const messageId = this.appendGroupMessage({
|
|
1062
|
-
projectId,
|
|
1063
|
-
authorKind: 'bot',
|
|
1064
|
-
authorId: speakerId,
|
|
1065
|
-
authorName: speaker.name,
|
|
1066
|
-
text,
|
|
1067
|
-
kind: 'mention',
|
|
1068
|
-
toMemberId: target.memberId,
|
|
1069
|
-
})
|
|
1070
|
-
return { ok: true, messageId }
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
/** 工具路径:推进 bot 建临时 bot(只属于本群)。 */
|
|
1074
|
-
groupSpawnBySession(sessionId: string, input: { name: string; brief?: string }): { memberId: string; name: string } {
|
|
1075
|
-
const projectId = this.projectOfSession(sessionId)
|
|
1076
|
-
const speakerId = this.memberOfSession(sessionId)
|
|
1077
|
-
if (!projectId || !speakerId) throw new HamsError('POLICY_DENIED', '这个会话不在任何项目群里')
|
|
1078
|
-
const project = this.projectRow(projectId)
|
|
1079
|
-
if (project.coordinator_id !== speakerId) throw new HamsError('POLICY_DENIED', '只有推进 bot 能在自己群里建临时 bot')
|
|
1080
|
-
return this.spawnTempBot({ principalId: speakerId, channel: 'tool', correlationId: `group-spawn-${this.now()}` }, { projectId, ...input })
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
/** 工具路径:群成员名单。 */
|
|
1084
|
-
groupMembersBySession(sessionId: string): Array<{ memberId: string; name: string; kind: TeamKind; status: string; brief: string }> {
|
|
1085
|
-
const projectId = this.projectOfSession(sessionId)
|
|
1086
|
-
if (!projectId) throw new HamsError('POLICY_DENIED', '这个会话不在任何项目群里')
|
|
1087
|
-
return this.teamRows(projectId)
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
projectOfSession(sessionId: string): string | null {
|
|
1091
|
-
const row = this.db.prepare('SELECT value FROM hams_meta WHERE key = ?').get(`chat:group:session:${sessionId}`) as { value: string } | undefined
|
|
1092
|
-
return row?.value || null
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
memberOfSession(sessionId: string): string | null {
|
|
1096
|
-
const row = this.db.prepare('SELECT value FROM hams_meta WHERE key = ?').get(`chat:session:${sessionId}`) as { value: string } | undefined
|
|
1097
|
-
return row?.value || null
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
}
|