jbc-dssi 1.0.2 → 1.1.1
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/package.json +1 -1
- package/vendor/groups/bridge/engine.js +263 -137
- package/vendor/groups/bridge/index.js +16 -8
- package/vendor/groups/bridge/prompt.js +125 -33
package/package.json
CHANGED
|
@@ -1,41 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* groups/engine.js ——
|
|
2
|
+
* groups/bridge/engine.js —— 群聊任务调度引擎(2026-10-31 新群规)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* 新模型(用户定稿,替换旧的「@ 接话 / 主管触发全员」规则):
|
|
5
|
+
* - **群主管是唯一入口**:用户不直接 @ 具体成员时,整条指令只投给主管;
|
|
6
|
+
* 主管负责意图分析 → 拆解 → 精准分发(禁止全员广播)→ 收反馈 → 决策。
|
|
7
|
+
* - **用户可以直接 @ 成员**:被点名的成员直接回答,不经主管。
|
|
8
|
+
* - **成员只在被分派时发言**:反馈给主管,不互相接话、不点名、不替他人发言。
|
|
9
|
+
* - **轮次**:每「分发任务 → 接收反馈」算 1 轮;1~4 轮内可提前结束;
|
|
10
|
+
* 第 5 轮(`MAX_ROUNDS`)反馈收齐后**强制熔断**,只允许汇报,不允许第 6 轮。
|
|
9
11
|
*
|
|
10
|
-
*
|
|
12
|
+
* 谁守什么:轮次、熔断、分发合法性(禁止 @all / 不能分给自己)由本文件**硬控**;
|
|
13
|
+
* 拆解质量、汇报措辞靠 prompt(模型不可靠,红线必须落在代码里)。
|
|
14
|
+
*
|
|
15
|
+
* 实现:一次投递一个成员(串行)。用户消息进群 → 判定直投 or 交主管 →
|
|
11
16
|
* 逐个投递到成员的群会话(懒创建,dssAgent.createSession)→ 收集回复 →
|
|
12
|
-
*
|
|
17
|
+
* 按主管回复里的 @ 点名分发下一批。
|
|
13
18
|
*
|
|
14
|
-
* 不依赖 DSH 内部实现:会话操作全部走 ctx.sessionController
|
|
15
|
-
* 单测用假的 sessionController / dssAgent 驱动,不需要真实模型。
|
|
19
|
+
* 不依赖 DSH 内部实现:会话操作全部走 ctx.sessionController 的公开方法。
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
import { randomUUID } from 'node:crypto';
|
|
19
23
|
import { mentionsIn } from './mentions.js';
|
|
20
|
-
import {
|
|
24
|
+
import { buildManagerPrompt, buildMemberPrompt } from './prompt.js';
|
|
21
25
|
|
|
22
26
|
/** 房间日志最多保留条数(超出丢最旧,防无限膨胀)。 */
|
|
23
27
|
const ROOM_LOG_MAX = 60;
|
|
24
28
|
/** 单个成员一轮回复的等待上限。 */
|
|
25
29
|
const TURN_TIMEOUT_MS = 240_000;
|
|
26
30
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* 成员再拉回来答第二轮 —— 真机实测「一次提问,三个人各答两遍」(room 日志 6 条
|
|
31
|
-
* 成员消息,21:26:44 → 21:26:56)。用户要的是「每人一轮」,所以收紧到 1。
|
|
32
|
-
*
|
|
33
|
-
* 收紧后接力语义不变:点名只对**本轮还没说过话**的成员生效 —— 用户 @A,A 答完
|
|
34
|
-
* @B,B 仍会答(B 还没说);但 A→B→A 这种回环到第二个人就停。
|
|
31
|
+
* 轮次上限(最高红线,用户定稿):
|
|
32
|
+
* 「分发任务 → 接收反馈」算 1 轮;1~4 轮内可提前结束;第 5 轮收齐反馈后必须强制汇报,
|
|
33
|
+
* 不允许第 6 轮。engine 硬控,不依赖模型自觉。
|
|
35
34
|
*/
|
|
36
|
-
const
|
|
37
|
-
/**
|
|
38
|
-
const
|
|
35
|
+
const MAX_ROUNDS = 5;
|
|
36
|
+
/** 主管连续不按规范分发 / 熔断后仍要分发时的容忍次数(超过直接终止)。 */
|
|
37
|
+
const MAX_REFUSALS = 2;
|
|
38
|
+
/** 群上下文随投递带给模型的最大条数(与 prompt.js 的截断口径一致)。 */
|
|
39
|
+
const RECENT_LINES_MAX = 8;
|
|
39
40
|
|
|
40
41
|
/** 从 assistant/message 事件里取正文(与 DSH 会话事件形状对齐)。 */
|
|
41
42
|
function assistantText(event) {
|
|
@@ -54,9 +55,6 @@ function assistantText(event) {
|
|
|
54
55
|
*
|
|
55
56
|
* 老版本 DSH 不发 `usage` → 返回 null,手机侧不显示那一项(不显示比显示 0 诚实)。
|
|
56
57
|
* 全 0 也按"没有"处理:说明这轮根本没统计到,画一个 0 tok 会误导。
|
|
57
|
-
*
|
|
58
|
-
* @param {object|undefined} usage
|
|
59
|
-
* @returns {{inputTokens:number,outputTokens:number,cacheReadTokens:number,cacheWriteTokens:number,totalTokens:number}|null}
|
|
60
58
|
*/
|
|
61
59
|
function normalizeUsage(usage) {
|
|
62
60
|
if (!usage || typeof usage !== 'object') return null;
|
|
@@ -68,7 +66,37 @@ function normalizeUsage(usage) {
|
|
|
68
66
|
cacheWriteTokens: num(usage.cacheWriteTokens),
|
|
69
67
|
totalTokens: num(usage.totalTokens),
|
|
70
68
|
};
|
|
71
|
-
|
|
69
|
+
const any = out.totalTokens > 0 || out.inputTokens > 0 || out.outputTokens > 0
|
|
70
|
+
|| out.cacheReadTokens > 0 || out.cacheWriteTokens > 0;
|
|
71
|
+
return any ? out : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 把一次模型调用的 usage 累加进本轮 —— **2026-10-31 修的就是这里**。
|
|
76
|
+
*
|
|
77
|
+
* 【为什么必须累加】
|
|
78
|
+
* 一个 turn 会被拆成多个 step,**每一步都发一条 assistant/message、各带这一步的
|
|
79
|
+
* usage**。旧实现 `lastUsage = usage` 只留最后一条,于是面板出现自相矛盾的数字:
|
|
80
|
+
* 「共 10 步」却只有 60,686 tok —— 而第 10 步单独就 60,686(59,008+240+1,438),
|
|
81
|
+
* 前 9 步全丢。真机那一轮(2026-10-31 21:26:57~21:27:45,群会话 session-66663e11)
|
|
82
|
+
* 逐步核对:10 步合计 539,775 tok(缓存命中 521,728 / 未命中 9,159 / 输出 8,888),
|
|
83
|
+
* 谷价 ≈¥0.0551;旧口径只显示 60,686 tok / ≈¥0.0072,少算约 9 步。
|
|
84
|
+
*
|
|
85
|
+
* 【口径】与单聊(`android/.../data/Transcript.kt` 的 `addUsage`)和 Web UI 的
|
|
86
|
+
* `deriveTurnTokenUsage` 完全一致:每条 assistant/message 的 usage 各自相加;
|
|
87
|
+
* totalTokens 也逐条相加,**不**用 input+output 重算(会与 Web 差出数字)。
|
|
88
|
+
* 同一 step 的重试会产生多条 assistant/message:用量按 attempt 累加(与单聊一致),
|
|
89
|
+
* 步数则由调用方按 step 号去重。
|
|
90
|
+
*/
|
|
91
|
+
function sumUsage(acc, u) {
|
|
92
|
+
if (!acc) return u;
|
|
93
|
+
return {
|
|
94
|
+
inputTokens: acc.inputTokens + u.inputTokens,
|
|
95
|
+
outputTokens: acc.outputTokens + u.outputTokens,
|
|
96
|
+
cacheReadTokens: acc.cacheReadTokens + u.cacheReadTokens,
|
|
97
|
+
cacheWriteTokens: acc.cacheWriteTokens + u.cacheWriteTokens,
|
|
98
|
+
totalTokens: acc.totalTokens + u.totalTokens,
|
|
99
|
+
};
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
/**
|
|
@@ -81,7 +109,7 @@ function normalizeUsage(usage) {
|
|
|
81
109
|
* @param {Function} opts.broadcast 群事件广播
|
|
82
110
|
*/
|
|
83
111
|
export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }) {
|
|
84
|
-
/**
|
|
112
|
+
/** 当前活跃任务(同一时刻只跑一个群的一轮,防止消息乱序)。 */
|
|
85
113
|
let activeRound = null;
|
|
86
114
|
|
|
87
115
|
const now = () => Date.now();
|
|
@@ -123,11 +151,16 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
|
|
|
123
151
|
return sessionId;
|
|
124
152
|
}
|
|
125
153
|
|
|
154
|
+
/** 最近群消息(含刚落的用户行/成员行),给 prompt 做上下文。 */
|
|
155
|
+
function recentLinesOf(group) {
|
|
156
|
+
return (state.rooms[group.id] ?? []).slice(-RECENT_LINES_MAX);
|
|
157
|
+
}
|
|
158
|
+
|
|
126
159
|
/**
|
|
127
160
|
* 投递一轮给一个成员,等回复完成后 settle。
|
|
128
|
-
* @returns {Promise<{text?:string, failed?:string, missed?:boolean}>}
|
|
161
|
+
* @returns {Promise<{text?:string, failed?:string, missed?:boolean, usage?:object|null, steps?:number}>}
|
|
129
162
|
*/
|
|
130
|
-
function deliver(group,
|
|
163
|
+
function deliver(group, member, prompt, attachments = {}) {
|
|
131
164
|
return new Promise((resolve) => {
|
|
132
165
|
let settled = false;
|
|
133
166
|
let followClosed = false;
|
|
@@ -151,14 +184,19 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
|
|
|
151
184
|
let prompted = false;
|
|
152
185
|
let minTurn = null;
|
|
153
186
|
let lastText = '';
|
|
154
|
-
|
|
187
|
+
/*
|
|
188
|
+
* 本轮用量 = 各 step 的 usage **累加**(见 [sumUsage])。
|
|
189
|
+
*
|
|
190
|
+
* 2026-10-31 之前这里是 `lastUsage`(只留最后一条 assistant/message),
|
|
191
|
+
* 于是「共 N 步」配的是"最后一步"的 token,步数越多的轮次少算越多。
|
|
192
|
+
*/
|
|
193
|
+
let turnUsage = null;
|
|
155
194
|
/*
|
|
156
195
|
* 「共 N 步」的账:turn → 这一轮出现过的 step 号集合。
|
|
157
196
|
*
|
|
158
197
|
* 与单聊**同一个算法**(ChatScreen 的 `turnStepNums`):assistant/message
|
|
159
198
|
* 每个 step 发一条,收集本轮的 step 号去重后求个数;重试时同一个 step 可能
|
|
160
|
-
*
|
|
161
|
-
* 不在手机侧另造一套统计。
|
|
199
|
+
* 重复到达,用集合才不会多算。
|
|
162
200
|
*/
|
|
163
201
|
const turnStepNums = new Map();
|
|
164
202
|
let lastTurn = -1;
|
|
@@ -178,15 +216,6 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
|
|
|
178
216
|
return;
|
|
179
217
|
}
|
|
180
218
|
|
|
181
|
-
const prompt = buildTurnPrompt({
|
|
182
|
-
group,
|
|
183
|
-
members,
|
|
184
|
-
selfId: member.id,
|
|
185
|
-
recentLines,
|
|
186
|
-
message,
|
|
187
|
-
trigger,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
219
|
const ac = new AbortController();
|
|
191
220
|
const stream = ctx.sessionController.follow(
|
|
192
221
|
{ address: { kind: 'session', sessionId }, assistantStream: true, maxMessages: 1 },
|
|
@@ -233,7 +262,7 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
|
|
|
233
262
|
const text = assistantText(event);
|
|
234
263
|
if (text) lastText = text;
|
|
235
264
|
const usage = normalizeUsage(data.usage);
|
|
236
|
-
if (usage)
|
|
265
|
+
if (usage) turnUsage = sumUsage(turnUsage, usage);
|
|
237
266
|
/*
|
|
238
267
|
* 步数:**所有** assistant/message 都要记,包括只调工具、不画块的那几步
|
|
239
268
|
* (与单聊一致 —— token 大头正是这些步骤花掉的)。
|
|
@@ -250,12 +279,11 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
|
|
|
250
279
|
} else {
|
|
251
280
|
/*
|
|
252
281
|
* 本轮步数:取 turn/end 指向的那一轮(拿不到就退回最后见过的一轮)。
|
|
253
|
-
* 0 = 没统计到(老版本插件 / 事件里没带 step)→
|
|
254
|
-
* 与单聊「steps == 0 不写共 0 步」同一语义。
|
|
282
|
+
* 0 = 没统计到(老版本插件 / 事件里没带 step)→ 手机侧整段不显示。
|
|
255
283
|
*/
|
|
256
284
|
const endTurn = typeof data.turn === 'number' ? data.turn : lastTurn;
|
|
257
285
|
const steps = turnStepNums.get(endTurn)?.size ?? 0;
|
|
258
|
-
settle({ text: lastText, usage:
|
|
286
|
+
settle({ text: lastText, usage: turnUsage, steps });
|
|
259
287
|
}
|
|
260
288
|
ac.abort();
|
|
261
289
|
} else if (event?.type === 'turn/error' || event?.type === 'turn/failed') {
|
|
@@ -272,14 +300,10 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
|
|
|
272
300
|
try {
|
|
273
301
|
await new Promise((resolveNext) => setTimeout(resolveNext, 10));
|
|
274
302
|
/*
|
|
275
|
-
* 投递内容
|
|
303
|
+
* 投递内容 = prompt 文字,后面按顺序挂上附件 part。
|
|
276
304
|
*
|
|
277
305
|
* 形状与单聊 PROMPT 完全一致:{type:'image', mediaType, data} /
|
|
278
|
-
* {type:'file', receiptId}
|
|
279
|
-
* 图能直接看,文件由会话自己解析,不需要我们另造通路。
|
|
280
|
-
*
|
|
281
|
-
* 图片 base64 是手机**只上传一次**、这里在每个成员的 prompt 里各带一份:
|
|
282
|
-
* 走的是本机内存,不额外过网络;群成员上限很小,代价可接受。
|
|
306
|
+
* {type:'file', receiptId}。这样成员用的就是同一套模型能力。
|
|
283
307
|
*/
|
|
284
308
|
const content = [{ type: 'text', text: prompt }];
|
|
285
309
|
for (const image of (attachments?.images ?? [])) {
|
|
@@ -304,15 +328,7 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
|
|
|
304
328
|
// 请求已进队列:此后到达的事件才属于本轮(之前的都是历史回放)。
|
|
305
329
|
prompted = true;
|
|
306
330
|
timeoutTimer = setTimeout(() => {
|
|
307
|
-
if (!settled) {
|
|
308
|
-
emitLine(group.id, {
|
|
309
|
-
at: now(),
|
|
310
|
-
role: 'system',
|
|
311
|
-
name: member.name,
|
|
312
|
-
text: `${member.name} 这一轮迟迟没有回应。`,
|
|
313
|
-
});
|
|
314
|
-
settle({ missed: true });
|
|
315
|
-
}
|
|
331
|
+
if (!settled) settle({ missed: true });
|
|
316
332
|
}, TURN_TIMEOUT_MS);
|
|
317
333
|
} catch (err) {
|
|
318
334
|
if (!settled) {
|
|
@@ -326,88 +342,167 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
|
|
|
326
342
|
});
|
|
327
343
|
}
|
|
328
344
|
|
|
329
|
-
/**
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
};
|
|
346
|
-
for (const m of targets) enqueue(m);
|
|
345
|
+
/** 把一次投递的结果落成房间行(成员/主管同一套:手机侧按 memberId 显示名字与主管标)。 */
|
|
346
|
+
function emitReplyLine(group, round, member, result) {
|
|
347
|
+
const text = String(result.text ?? '').trim() || '[未回应]';
|
|
348
|
+
emitLine(group.id, {
|
|
349
|
+
at: now(),
|
|
350
|
+
role: 'member',
|
|
351
|
+
memberId: member.id,
|
|
352
|
+
name: member.name,
|
|
353
|
+
text,
|
|
354
|
+
usage: result.usage ?? null,
|
|
355
|
+
steps: result.steps ?? 0,
|
|
356
|
+
round: round?.rounds ?? 0,
|
|
357
|
+
});
|
|
358
|
+
round.totalTurns += 1;
|
|
359
|
+
return text;
|
|
360
|
+
}
|
|
347
361
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
362
|
+
/**
|
|
363
|
+
* 用户直接 @ 成员:被点名的成员直接回答,不经主管(用户定稿:直接 @ 生效)。
|
|
364
|
+
* 不算「分发→反馈」轮次,也不触发任何接力。
|
|
365
|
+
*/
|
|
366
|
+
async function runDirect(group, round, targets, members, text, attachments) {
|
|
367
|
+
for (const member of targets) {
|
|
368
|
+
const prompt = buildMemberPrompt({
|
|
369
|
+
group,
|
|
370
|
+
members,
|
|
371
|
+
selfId: member.id,
|
|
372
|
+
recentLines: recentLinesOf(group),
|
|
373
|
+
task: text,
|
|
374
|
+
mode: 'user-direct',
|
|
375
|
+
});
|
|
376
|
+
const result = await deliver(group, member, prompt, attachments);
|
|
377
|
+
if (result.failed) {
|
|
378
|
+
emitLine(group.id, {
|
|
379
|
+
at: now(), role: 'system', name: member.name,
|
|
380
|
+
text: `${member.name} 回应失败:${result.failed}`,
|
|
381
|
+
});
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
emitReplyLine(group, round, member, result);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
355
387
|
|
|
356
|
-
|
|
357
|
-
|
|
388
|
+
/**
|
|
389
|
+
* 主管统筹:分发 → 反馈 → 决策,直到主管输出汇报或第 MAX_ROUNDS 轮熔断。
|
|
390
|
+
*/
|
|
391
|
+
async function runCoordinated(group, round, manager, members, userMessage, attachments) {
|
|
392
|
+
/** 已完成的「分发→反馈」轮次。 */
|
|
393
|
+
let roundIndex = 0;
|
|
394
|
+
/** 上一轮主管分发后收集到的成员反馈。 */
|
|
395
|
+
let feedbacks = [];
|
|
396
|
+
/** 是否进入强制汇报阶段(第 MAX_ROUNDS 轮反馈收齐后)。 */
|
|
397
|
+
let forcedReport = false;
|
|
398
|
+
/** 主管连续不按规范动作的次数(防死循环)。 */
|
|
399
|
+
let refusals = 0;
|
|
358
400
|
|
|
359
|
-
|
|
360
|
-
|
|
401
|
+
while (true) {
|
|
402
|
+
const prompt = buildManagerPrompt({
|
|
403
|
+
group,
|
|
404
|
+
members,
|
|
405
|
+
selfId: manager.id,
|
|
406
|
+
recentLines: recentLinesOf(group),
|
|
407
|
+
userMessage,
|
|
408
|
+
roundIndex,
|
|
409
|
+
feedbacks,
|
|
410
|
+
maxRounds: MAX_ROUNDS,
|
|
411
|
+
forcedReport,
|
|
412
|
+
});
|
|
413
|
+
// 用户附件只在第一轮(计划阶段)带给主管;后续复查阶段不再重复投递。
|
|
414
|
+
const managerAttachments = roundIndex === 0 && feedbacks.length === 0 ? attachments : {};
|
|
415
|
+
const result = await deliver(group, manager, prompt, managerAttachments);
|
|
416
|
+
if (result.failed) {
|
|
417
|
+
emitLine(group.id, {
|
|
418
|
+
at: now(), role: 'system', name: manager.name,
|
|
419
|
+
text: `群主管回应失败:${result.failed}`,
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
emitReplyLine(group, round, manager, result);
|
|
424
|
+
const managerText = String(result.text ?? '').trim();
|
|
361
425
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
426
|
+
/* ── 解析主管这一句:是「分发」还是「汇报」 ── */
|
|
427
|
+
const { all, found } = mentionsIn(managerText, members);
|
|
428
|
+
const targets = found.filter((m) => m.id !== manager.id);
|
|
429
|
+
const badDispatch = all || (found.length > 0 && targets.length === 0);
|
|
430
|
+
|
|
431
|
+
if (targets.length === 0) {
|
|
432
|
+
if (!badDispatch) {
|
|
433
|
+
// 没有点名任何人 = 主管在汇报 → 结束本轮任务
|
|
434
|
+
return;
|
|
368
435
|
}
|
|
369
436
|
/*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
437
|
+
* 非法分发:@全体(禁止全员广播)或只 @ 自己(主管不是执行者)。
|
|
438
|
+
* 不降级成"悄悄忽略"——明确回一条系统提示并让主管重来,否则用户会以为
|
|
439
|
+
* 任务已经分发下去了。
|
|
373
440
|
*/
|
|
374
|
-
|
|
441
|
+
refusals += 1;
|
|
375
442
|
emitLine(group.id, {
|
|
376
|
-
at: now(), role: '
|
|
377
|
-
|
|
378
|
-
usage: result.usage ?? null,
|
|
379
|
-
// 本轮步数:用量浮层里「共 N 步」用;0 = 没统计到,手机侧不显示
|
|
380
|
-
steps: result.steps ?? 0,
|
|
443
|
+
at: now(), role: 'system', name: '',
|
|
444
|
+
text: '群主管的分发不符合规范(禁止全员广播、不能把任务分给自己)。请求重新分发。',
|
|
381
445
|
});
|
|
446
|
+
if (refusals >= MAX_REFUSALS) {
|
|
447
|
+
emitLine(group.id, {
|
|
448
|
+
at: now(), role: 'system', name: '',
|
|
449
|
+
text: '群主管连续未按规范分发,本轮任务已终止。',
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
forcedReport = true;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
382
456
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
trigger = 'manager';
|
|
398
|
-
}
|
|
399
|
-
} else if (found.length > 0) {
|
|
400
|
-
const next = found.filter((m) => (round.turns.get(m.id) ?? 0) < MAX_TURNS_EACH);
|
|
401
|
-
if (next.length > 0) {
|
|
402
|
-
for (const m of next) enqueue(m);
|
|
403
|
-
trigger = 'member';
|
|
404
|
-
}
|
|
457
|
+
/* ── 到了第 MAX_ROUNDS 轮:只允许汇报,不允许再分发 ── */
|
|
458
|
+
if (roundIndex >= MAX_ROUNDS) {
|
|
459
|
+
round.fused = true;
|
|
460
|
+
refusals += 1;
|
|
461
|
+
emitLine(group.id, {
|
|
462
|
+
at: now(), role: 'system', name: '',
|
|
463
|
+
text: `已到第 ${MAX_ROUNDS} 轮,禁止继续分发,必须立即总结汇报。`,
|
|
464
|
+
});
|
|
465
|
+
if (refusals >= MAX_REFUSALS) {
|
|
466
|
+
emitLine(group.id, {
|
|
467
|
+
at: now(), role: 'system', name: '',
|
|
468
|
+
text: `已强制熔断(第 ${MAX_ROUNDS} 轮上限),任务终止。`,
|
|
469
|
+
});
|
|
470
|
+
return;
|
|
405
471
|
}
|
|
472
|
+
forcedReport = true;
|
|
473
|
+
continue;
|
|
406
474
|
}
|
|
407
|
-
|
|
408
|
-
|
|
475
|
+
|
|
476
|
+
/* ── 合法分发:进入下一轮,逐个投递给被点名的成员 ── */
|
|
477
|
+
roundIndex += 1;
|
|
478
|
+
round.rounds = roundIndex;
|
|
479
|
+
feedbacks = [];
|
|
480
|
+
for (const target of targets) {
|
|
481
|
+
const memberPrompt = buildMemberPrompt({
|
|
482
|
+
group,
|
|
483
|
+
members,
|
|
484
|
+
selfId: target.id,
|
|
485
|
+
recentLines: recentLinesOf(group),
|
|
486
|
+
task: managerText,
|
|
487
|
+
mode: 'manager-task',
|
|
488
|
+
});
|
|
489
|
+
const memberResult = await deliver(group, target, memberPrompt, attachments);
|
|
490
|
+
if (memberResult.failed) {
|
|
491
|
+
emitLine(group.id, {
|
|
492
|
+
at: now(), role: 'system', name: target.name,
|
|
493
|
+
text: `${target.name} 回应失败:${memberResult.failed}`,
|
|
494
|
+
});
|
|
495
|
+
// 失败也要计为本轮反馈,否则轮次会卡死、主管永远等不到人
|
|
496
|
+
round.totalTurns += 1;
|
|
497
|
+
feedbacks.push({ name: target.name, text: String(memberResult.failed), failed: true });
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const text = emitReplyLine(group, round, target, memberResult);
|
|
501
|
+
feedbacks.push({ name: target.name, text, missed: memberResult.missed === true });
|
|
502
|
+
}
|
|
503
|
+
// 第 MAX_ROUNDS 轮反馈收齐 → 下一句主管只能汇报
|
|
504
|
+
forcedReport = roundIndex >= MAX_ROUNDS;
|
|
409
505
|
}
|
|
410
|
-
broadcast?.({ type: 'round_end', groupId: group.id, totalTurns: round.totalTurns });
|
|
411
506
|
}
|
|
412
507
|
|
|
413
508
|
/**
|
|
@@ -415,6 +510,7 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
|
|
|
415
510
|
* @param {object} group 群实体
|
|
416
511
|
* @param {{id:string,name:string}[]} members 群成员(含主管)
|
|
417
512
|
* @param {string} text 用户消息
|
|
513
|
+
* @param {{images?:object[],files?:object[]}} attachments 附件(与单聊同形)
|
|
418
514
|
*/
|
|
419
515
|
async function handleUserMessage(group, members, text, attachments = {}) {
|
|
420
516
|
const images = Array.isArray(attachments?.images) ? attachments.images : [];
|
|
@@ -428,13 +524,15 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
|
|
|
428
524
|
].filter(Boolean).join('、');
|
|
429
525
|
const openerText = attachNote ? `${body}\n(本条消息附带:${attachNote})` : body;
|
|
430
526
|
|
|
527
|
+
const manager = members.find((m) => m.id === group.managerId) ?? null;
|
|
528
|
+
|
|
431
529
|
if (activeRound !== null) {
|
|
432
530
|
/*
|
|
433
531
|
* 上一轮还没跑完:提示一句就返回。
|
|
434
532
|
*
|
|
435
533
|
* ★ 这条过去是「只广播、不落盘」—— 于是它带的 seq 是临时算的,很容易和
|
|
436
534
|
* 房间里最后一条撞上;手机侧 LazyColumn 拿 seq 当 key,重复 key 直接崩。
|
|
437
|
-
* 现在统一走 emitLine:seq
|
|
535
|
+
* 现在统一走 emitLine:seq 由它分配、同时落盘。
|
|
438
536
|
*/
|
|
439
537
|
emitLine(group.id, {
|
|
440
538
|
at: now(), role: 'system',
|
|
@@ -442,10 +540,18 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
|
|
|
442
540
|
});
|
|
443
541
|
return;
|
|
444
542
|
}
|
|
543
|
+
if (!manager) {
|
|
544
|
+
// 新规:群必须有主管。历史群(或数据被手工改坏)兜底:不落用户行、明确提示。
|
|
545
|
+
emitLine(group.id, {
|
|
546
|
+
at: now(), role: 'system',
|
|
547
|
+
name: '', text: '本群还没有主管,无法开始任务。请先在群设置里指定主管。',
|
|
548
|
+
});
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
445
552
|
/*
|
|
446
553
|
* 用户行落盘:带附件元信息(手机侧据此把图/文件名画回自己的气泡)。
|
|
447
|
-
* 只存 `{isImage, ref, name}` ——
|
|
448
|
-
* (日志要落盘、要随快照全量下发,塞 base64 会把群历史撑爆)。
|
|
554
|
+
* 只存 `{isImage, ref, name}` —— 字节在手机本地缓存里,不在房间日志里存一份。
|
|
449
555
|
*/
|
|
450
556
|
const attachmentMeta = [
|
|
451
557
|
...images.map((i) => ({ isImage: true, ref: typeof i?.ref === 'string' ? i.ref : '' })),
|
|
@@ -461,11 +567,31 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
|
|
|
461
567
|
attachments: attachmentMeta,
|
|
462
568
|
});
|
|
463
569
|
|
|
464
|
-
const {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
570
|
+
const round = { groupId: group.id, rounds: 0, totalTurns: 0, fused: false };
|
|
571
|
+
activeRound = round;
|
|
572
|
+
try {
|
|
573
|
+
/*
|
|
574
|
+
* 路由(用户定稿):
|
|
575
|
+
* - 用户直接 @ 了具体成员 → 只投给被 @ 的成员(绕过主管);
|
|
576
|
+
* - 其余情况(没 @ / 只 @ 主管 / @all)→ 整条指令交给主管统筹。
|
|
577
|
+
*/
|
|
578
|
+
const { found } = mentionsIn(text ?? '', members);
|
|
579
|
+
const direct = found.filter((m) => m.id !== manager.id);
|
|
580
|
+
if (direct.length > 0) {
|
|
581
|
+
await runDirect(group, round, direct, members, openerText, { images, files });
|
|
582
|
+
} else {
|
|
583
|
+
await runCoordinated(group, round, manager, members, openerText, { images, files });
|
|
584
|
+
}
|
|
585
|
+
} finally {
|
|
586
|
+
if (activeRound === round) activeRound = null;
|
|
587
|
+
broadcast?.({
|
|
588
|
+
type: 'round_end',
|
|
589
|
+
groupId: group.id,
|
|
590
|
+
totalTurns: round.totalTurns,
|
|
591
|
+
rounds: round.rounds,
|
|
592
|
+
fused: round.fused === true,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
469
595
|
}
|
|
470
596
|
|
|
471
597
|
/** 打开群聊页时拉房间历史。 */
|
|
@@ -139,7 +139,7 @@ export function createGroupsEngine({
|
|
|
139
139
|
|
|
140
140
|
/* ── 命令(GROUPS_CMD op) ───────────────────────────────────── */
|
|
141
141
|
|
|
142
|
-
/** 创建群:{ name, memberIds, managerId
|
|
142
|
+
/** 创建群:{ name, memberIds, managerId }(2026-10-31 新规:**必须指定主管**)。 */
|
|
143
143
|
async function createGroup(payload) {
|
|
144
144
|
await ensure();
|
|
145
145
|
const name = String(payload?.name ?? '').trim();
|
|
@@ -147,12 +147,14 @@ export function createGroupsEngine({
|
|
|
147
147
|
if (!name) throw new Error('群名称不能为空');
|
|
148
148
|
if (memberIds.length < 2) throw new Error('至少拉 2 个智能体进群');
|
|
149
149
|
const managerId = String(payload?.managerId ?? '');
|
|
150
|
-
|
|
150
|
+
// 新群规:主管是群的唯一入口与调度中枢,没有主管群无法运转 —— 建群时必须指定。
|
|
151
|
+
if (!managerId) throw new Error('建群必须指定群主管');
|
|
152
|
+
if (!memberIds.includes(managerId)) throw new Error('群主管必须是群成员');
|
|
151
153
|
const at = Date.now();
|
|
152
154
|
const group = {
|
|
153
155
|
id: randomUUID(),
|
|
154
156
|
name,
|
|
155
|
-
managerId
|
|
157
|
+
managerId,
|
|
156
158
|
memberIds,
|
|
157
159
|
memberNames: {},
|
|
158
160
|
createdAt: at,
|
|
@@ -195,14 +197,15 @@ export function createGroupsEngine({
|
|
|
195
197
|
return { id, deleted: true };
|
|
196
198
|
}
|
|
197
199
|
|
|
198
|
-
/** 设置/更换群主管:{ id, managerId }
|
|
200
|
+
/** 设置/更换群主管:{ id, managerId }(新规:**不允许取消**,只能换人)。 */
|
|
199
201
|
async function setManager(payload) {
|
|
200
202
|
await ensure();
|
|
201
203
|
const group = findGroup(String(payload?.id ?? ''));
|
|
202
204
|
if (!group) throw new Error('群不存在');
|
|
203
205
|
const managerId = String(payload?.managerId ?? '');
|
|
204
|
-
if (
|
|
205
|
-
group.managerId
|
|
206
|
+
if (!managerId) throw new Error('群必须有主管,不能取消;只能更换主管');
|
|
207
|
+
if (!group.memberIds.includes(managerId)) throw new Error('群主管必须是群成员');
|
|
208
|
+
group.managerId = managerId;
|
|
206
209
|
group.updatedAt = Date.now();
|
|
207
210
|
await save();
|
|
208
211
|
notifyListChanged();
|
|
@@ -226,16 +229,19 @@ export function createGroupsEngine({
|
|
|
226
229
|
return publicGroup(group);
|
|
227
230
|
}
|
|
228
231
|
|
|
229
|
-
/** 移出群:{ id, memberIds }
|
|
232
|
+
/** 移出群:{ id, memberIds }(新规:**主管不能被移出**,必须先换主管)。 */
|
|
230
233
|
async function removeMembers(payload) {
|
|
231
234
|
await ensure();
|
|
232
235
|
const group = findGroup(String(payload?.id ?? ''));
|
|
233
236
|
if (!group) throw new Error('群不存在');
|
|
234
237
|
const removeIds = Array.isArray(payload?.memberIds) ? new Set(payload.memberIds) : new Set();
|
|
238
|
+
if (group.managerId && removeIds.has(group.managerId)) {
|
|
239
|
+
throw new Error('主管不能被移出群,请先指定新的主管');
|
|
240
|
+
}
|
|
235
241
|
group.memberIds = group.memberIds.filter((id) => !removeIds.has(id));
|
|
236
|
-
if (group.managerId && removeIds.has(group.managerId)) group.managerId = null;
|
|
237
242
|
group.updatedAt = Date.now();
|
|
238
243
|
if (group.memberIds.length < 2) throw new Error('群成员不足 2 人,请解散群');
|
|
244
|
+
await refreshMemberNames(group);
|
|
239
245
|
await save();
|
|
240
246
|
notifyListChanged();
|
|
241
247
|
return publicGroup(group);
|
|
@@ -246,6 +252,8 @@ export function createGroupsEngine({
|
|
|
246
252
|
await ensure();
|
|
247
253
|
const group = findGroup(String(payload?.id ?? ''));
|
|
248
254
|
if (!group) throw new Error('群不存在');
|
|
255
|
+
// 新群规:主管是唯一入口,没有主管的群不能开始任务。
|
|
256
|
+
if (!group.managerId) throw new Error('本群还没有主管,请先在群设置里指定主管');
|
|
249
257
|
const text = String(payload?.text ?? '').trim();
|
|
250
258
|
// 附件与单聊同形:图片带 base64(mediaType/data/ref),文件带上传回执(receiptId/ref/name)
|
|
251
259
|
const images = (Array.isArray(payload?.images) ? payload.images : [])
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* groups/prompt.js —— 构建投递给群成员的"群会话"消息。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 两种角色、两套提示词(2026-10-31 用户定稿的新群规):
|
|
5
|
+
* - **群主管**:唯一入口与调度中枢。接收用户指令 → 拆解 → 精准分发(禁止全员广播)
|
|
6
|
+
* → 收反馈 → 决策 → 1~4 轮内可停、第 5 轮强制熔断 → 三段式标准汇报。
|
|
7
|
+
* - **成员**:只在被分派(或用户直接 @)时发言,只做与自己相关的部分,把结果反馈给
|
|
8
|
+
* 主管;不互相接话、不点名、不替别人发言。
|
|
9
|
+
*
|
|
10
|
+
* 这里只管"把上下文+职责拼进 prompt",不碰 DSH 内部实现;轮次/熔断/分发合法性
|
|
11
|
+
* 由 engine.js 硬控(模型不可靠,红线必须由代码守)。
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
|
-
import { mentionsIn } from './mentions.js';
|
|
10
|
-
|
|
11
14
|
/** 群上下文最多带多少条最近消息(控制 token,防刷屏)。 */
|
|
12
15
|
const CONTEXT_LINES_MAX = 8;
|
|
13
16
|
/** 单条消息最多截断多长(展示用)。 */
|
|
@@ -19,49 +22,138 @@ function lineOf(line) {
|
|
|
19
22
|
return `${who}:${text}`;
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
function memberListText(members, managerId) {
|
|
26
|
+
return members
|
|
27
|
+
.map((m) => (m.id === managerId ? `${m.name}(群主管)` : m.name))
|
|
28
|
+
.join('、');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function recentText(recentLines) {
|
|
32
|
+
const lines = (recentLines ?? []).slice(-CONTEXT_LINES_MAX).map(lineOf).join('\n');
|
|
33
|
+
return lines ? `最近群消息:\n${lines}` : '(群里还没有其他消息)';
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
/**
|
|
37
|
+
* 群主管提示词。
|
|
38
|
+
*
|
|
23
39
|
* @param {object} opts
|
|
24
40
|
* @param {object} opts.group 群实体({id,name,managerId})
|
|
25
|
-
* @param {{id:string,name:string}[]} opts.members
|
|
26
|
-
* @param {string} opts.selfId
|
|
41
|
+
* @param {{id:string,name:string}[]} opts.members 群成员(含主管自己)
|
|
42
|
+
* @param {string} opts.selfId 主管成员 id
|
|
27
43
|
* @param {{role:string,name?:string,text:string}[]} opts.recentLines 最近群消息
|
|
28
|
-
* @param {string} opts.
|
|
29
|
-
* @param {
|
|
44
|
+
* @param {string} opts.userMessage 用户原话
|
|
45
|
+
* @param {number} opts.roundIndex 已经完成的"分发→反馈"轮次(0 = 尚未分发)
|
|
46
|
+
* @param {{name:string,text:string,failed?:boolean,missed?:boolean}[]} opts.feedbacks 本轮成员反馈
|
|
47
|
+
* @param {number} opts.maxRounds 轮次上限(第 N 轮后必须汇报)
|
|
48
|
+
* @param {boolean} opts.forcedReport 是否已到强制汇报(第 N 轮反馈收齐后)
|
|
30
49
|
* @returns {string}
|
|
31
50
|
*/
|
|
32
|
-
export function
|
|
51
|
+
export function buildManagerPrompt({
|
|
52
|
+
group,
|
|
53
|
+
members,
|
|
54
|
+
selfId,
|
|
55
|
+
recentLines,
|
|
56
|
+
userMessage,
|
|
57
|
+
roundIndex,
|
|
58
|
+
feedbacks,
|
|
59
|
+
maxRounds,
|
|
60
|
+
forcedReport,
|
|
61
|
+
}) {
|
|
33
62
|
const self = members.find((m) => m.id === selfId);
|
|
34
63
|
const selfName = self?.name ?? '你';
|
|
35
|
-
const
|
|
64
|
+
const feedbackText = (feedbacks ?? []).length
|
|
65
|
+
? (feedbacks ?? [])
|
|
66
|
+
.map((f) => {
|
|
67
|
+
const tag = f.failed ? '(回应失败)' : f.missed ? '(未回应)' : '';
|
|
68
|
+
const text = String(f.text ?? '').replace(/\s+/g, ' ').slice(0, LINE_TEXT_MAX);
|
|
69
|
+
return `- ${f.name}${tag}:${text}`;
|
|
70
|
+
})
|
|
71
|
+
.join('\n')
|
|
72
|
+
: '(还没有成员反馈)';
|
|
36
73
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
74
|
+
const stateText = roundIndex === 0
|
|
75
|
+
? '用户刚在群里发言,你**还没有开始分发**。'
|
|
76
|
+
: `已完成第 ${roundIndex} 轮(分发→反馈)。`;
|
|
40
77
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
: '你(用户)在群里发言';
|
|
78
|
+
const decideText = forcedReport
|
|
79
|
+
? `【轮次红线】已经到第 ${maxRounds} 轮,**不允许再分发任何任务**。现在只能输出标准化汇报结束任务。`
|
|
80
|
+
: roundIndex === 0
|
|
81
|
+
? '请先分析用户真实意图,决定是否需要成员参与:需要就按分发格式点名分派;不需要就**直接输出标准化汇报**。'
|
|
82
|
+
: '请决定:继续分发(用 @ 点名具体成员),或者**直接输出标准化汇报**结束任务;如果判断任务已满足用户意图,随时可以提前结束。';
|
|
47
83
|
|
|
48
84
|
return [
|
|
49
|
-
`你在工作群「${group.name}
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
85
|
+
`你在工作群「${group.name}」里担任**群主管**。群成员:${memberListText(members, group.managerId)}。你自己是「${selfName}」。`,
|
|
86
|
+
'',
|
|
87
|
+
'【你的核心任务】',
|
|
88
|
+
'接收用户指令,以用户意图为唯一基准进行任务拆解与智能路由分发,监控执行进度,并严格控制交互轮次。',
|
|
89
|
+
'',
|
|
90
|
+
'【执行规则】',
|
|
91
|
+
'1. 意图分析与拆解:深入分析用户的真实需求,把大任务拆解为具体、可执行的子任务。如果意图模糊,必须先向用户澄清,严禁主观臆断。',
|
|
92
|
+
'2. 精准按需分发:根据群内各成员(智能体)的职能定位,仅把子任务分发给相关的成员。**绝对不允许全员广播**,无关成员无需回答或执行。',
|
|
93
|
+
'3. 进度追踪与决策:负责收集相关成员的任务反馈,评估完成情况,决定是继续安排后续任务、还是提前结束当前任务闭环。',
|
|
94
|
+
`4. 严格轮次控制(最高红线):每发生一次「分发任务 → 接收反馈」的交互计为 1 轮;1~${maxRounds - 1} 轮内判断已满足用户意图可随时停止并汇报;若流转到第 ${maxRounds} 轮,无论任务是否完成,必须立刻停止后续分发并向用户做阶段性汇报。`,
|
|
95
|
+
'5. 标准化汇报:停止或结束时的汇报必须包含三段:',
|
|
96
|
+
' 【当前进度总结】',
|
|
97
|
+
' 【各成员执行摘要】',
|
|
98
|
+
' 【下一步建议或需用户决策的事项】',
|
|
99
|
+
' 然后等待用户的新指令。',
|
|
100
|
+
'',
|
|
101
|
+
'【分发格式(硬要求)】',
|
|
102
|
+
'需要成员做事时,在回复里用 `@成员名` 点名,并紧跟写清**这个成员具体要做什么**;可以一次点名多个不同成员。',
|
|
103
|
+
'**不得点名全体(@all / @所有人),也不得点名你自己**——你不是执行者。',
|
|
104
|
+
'例:`@张三 请检查登录接口的边界条件,只反馈结论。`',
|
|
105
|
+
'',
|
|
106
|
+
'【行为边界】',
|
|
107
|
+
'· 严禁代替其他智能体回答具体业务问题(你只负责拆解、分发、汇总);',
|
|
108
|
+
'· 每次分发任务后,必须等待相关成员反馈,再进行下一轮动作;',
|
|
109
|
+
'· 除非收到明确指令,否则不得擅自终止或重置任务。',
|
|
110
|
+
'',
|
|
111
|
+
recentText(recentLines),
|
|
54
112
|
'',
|
|
55
|
-
|
|
113
|
+
`【当前状态】${stateText}`,
|
|
114
|
+
`用户原话:「${String(userMessage ?? '')}」`,
|
|
115
|
+
'【本轮成员反馈】',
|
|
116
|
+
feedbackText,
|
|
56
117
|
'',
|
|
57
|
-
|
|
58
|
-
`请回复「${selfName}」的回答:`,
|
|
118
|
+
decideText,
|
|
59
119
|
].join('\n');
|
|
60
120
|
}
|
|
61
121
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
122
|
+
/**
|
|
123
|
+
* 成员提示词(两种来源:用户直接 @ / 主管分派)。
|
|
124
|
+
*
|
|
125
|
+
* @param {object} opts
|
|
126
|
+
* @param {object} opts.group 群实体
|
|
127
|
+
* @param {{id:string,name:string}[]} opts.members 群成员
|
|
128
|
+
* @param {string} opts.selfId 被投递的成员 id
|
|
129
|
+
* @param {{role:string,name?:string,text:string}[]} opts.recentLines 最近群消息
|
|
130
|
+
* @param {string} opts.task 需要该成员做的事(用户原话 / 主管分派内容)
|
|
131
|
+
* @param {'user-direct'|'manager-task'} opts.mode 触发来源
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
export function buildMemberPrompt({ group, members, selfId, recentLines, task, mode }) {
|
|
135
|
+
const self = members.find((m) => m.id === selfId);
|
|
136
|
+
const selfName = self?.name ?? '你';
|
|
137
|
+
const manager = members.find((m) => m.id === group.managerId);
|
|
138
|
+
const source = mode === 'user-direct'
|
|
139
|
+
? '用户在群里**直接点名**让你回答,不需要等群主管分派。'
|
|
140
|
+
: `群主管「${manager?.name ?? '主管'}」把任务分派给了你(这是你的子任务)。`;
|
|
141
|
+
|
|
142
|
+
return [
|
|
143
|
+
`你在工作群「${group.name}」里,你是「${selfName}」。群成员:${memberListText(members, group.managerId)}。`,
|
|
144
|
+
source,
|
|
145
|
+
'',
|
|
146
|
+
'【本次任务】',
|
|
147
|
+
String(task ?? '').trim() || '(没有具体任务,请简短回应)',
|
|
148
|
+
'',
|
|
149
|
+
'【你的职责】',
|
|
150
|
+
'1. 只完成与你相关的部分,不要代替其他成员发言,也不要互相 @ 点名;',
|
|
151
|
+
'2. 完成后给出**简短结果反馈**(结论 / 产出 / 需要决策的点),不要长篇大论;',
|
|
152
|
+
'3. 不要向用户提问(有问题就把问题写进反馈,由群主管汇总);',
|
|
153
|
+
'4. 只回应这一次,不主动接话。',
|
|
154
|
+
'',
|
|
155
|
+
recentText(recentLines),
|
|
156
|
+
'',
|
|
157
|
+
'请给出你的反馈:',
|
|
158
|
+
].join('\n');
|
|
67
159
|
}
|