engine7 7.1.31 → 7.1.33
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/dist/engine-startup.mjs +194 -148
- package/dist/main.mjs +194 -148
- package/package.json +1 -1
package/dist/engine-startup.mjs
CHANGED
|
@@ -711,7 +711,10 @@ async function executeUserPromptSubmitHooks(prompt, ctx, signal) {
|
|
|
711
711
|
session_id: ctx.sessionId,
|
|
712
712
|
transcript_path: "",
|
|
713
713
|
cwd: ctx.cwd,
|
|
714
|
-
prompt
|
|
714
|
+
prompt,
|
|
715
|
+
sender_id: ctx.senderId || "",
|
|
716
|
+
channel_type: ctx.channelType || "",
|
|
717
|
+
source: ctx.source || ""
|
|
715
718
|
};
|
|
716
719
|
return executeHooks("UserPromptSubmit", hookInput, ctx, signal);
|
|
717
720
|
}
|
|
@@ -728,6 +731,8 @@ async function executeStopHooks(ctx, signal, lastAssistantMessage) {
|
|
|
728
731
|
last_assistant_message: lastAssistantMessage,
|
|
729
732
|
channel: ctx.channel,
|
|
730
733
|
// 让 callback hook 能判断来源
|
|
734
|
+
channelType: ctx.channelType || "",
|
|
735
|
+
// 'group' | 'dm' — Stop hook 用来判断是否跳过 group
|
|
731
736
|
source: ctx.source || ""
|
|
732
737
|
// 消息来源(heartbeat/cron/system 等注入 turn 可据此跳过)
|
|
733
738
|
};
|
|
@@ -3108,6 +3113,8 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
|
|
|
3108
3113
|
sessionId: context?.sessionId || "default",
|
|
3109
3114
|
workspace: context?.workspace || "",
|
|
3110
3115
|
channel: context?.channel || "",
|
|
3116
|
+
channelType: context?.channelType || "",
|
|
3117
|
+
// 'group' | 'dm',Stop hook 用来判断是否跳过 group
|
|
3111
3118
|
cwd: context?.workspace || "",
|
|
3112
3119
|
source: typeof context?.source === "string" ? context.source : ""
|
|
3113
3120
|
};
|
|
@@ -18813,7 +18820,10 @@ ${text}` : text });
|
|
|
18813
18820
|
sessionId,
|
|
18814
18821
|
workspace,
|
|
18815
18822
|
channel: channelName === "cli" ? "console" : channelName,
|
|
18816
|
-
cwd: workspace
|
|
18823
|
+
cwd: workspace,
|
|
18824
|
+
senderId: inboundMeta?.from || "",
|
|
18825
|
+
channelType: inboundMeta?.channelType || "",
|
|
18826
|
+
source
|
|
18817
18827
|
});
|
|
18818
18828
|
if (hookResult.additionalContexts && hookResult.additionalContexts.length > 0) {
|
|
18819
18829
|
hookAdditionalContexts = hookResult.additionalContexts;
|
|
@@ -18872,6 +18882,8 @@ ${text}` : text });
|
|
|
18872
18882
|
const toolContext = {
|
|
18873
18883
|
sessionId,
|
|
18874
18884
|
channel: channelName === "cli" ? "console" : channelName,
|
|
18885
|
+
channelType: inboundMeta?.channelType || "",
|
|
18886
|
+
// 'group' | 'dm',Stop hook 用来判断是否跳过 group(注入消息无 inboundMeta,必须 ?.)
|
|
18875
18887
|
source: source || "",
|
|
18876
18888
|
// 消息来源(user/inbox/heartbeat/cron/system/inner-voice),Stop hook 用来区分注入 turn
|
|
18877
18889
|
workspace,
|
|
@@ -20295,160 +20307,192 @@ var NudgePlugin = class {
|
|
|
20295
20307
|
registerCallbackHook("Stop", {
|
|
20296
20308
|
type: "callback",
|
|
20297
20309
|
callback: async (input, _toolUseID, _signal) => {
|
|
20298
|
-
|
|
20299
|
-
|
|
20300
|
-
const
|
|
20301
|
-
|
|
20302
|
-
|
|
20303
|
-
if (repliedIds.length > 0) {
|
|
20304
|
-
this.removeNotificationsById(repliedIds);
|
|
20305
|
-
}
|
|
20306
|
-
const msgChannel = input?.channel || "";
|
|
20307
|
-
if (sessionId.includes("voice-chat") || msgChannel === "voice-chat") {
|
|
20308
|
-
console.log(`[stop-hook] skipping voice-chat (channel=${msgChannel})`);
|
|
20309
|
-
return { outcome: { outcome: "success" } };
|
|
20310
|
-
}
|
|
20311
|
-
const msgSource = input?.source || "";
|
|
20312
|
-
if (msgSource && msgSource !== "user" && msgSource !== "inbox") {
|
|
20313
|
-
console.log(`[stop-hook] skipping non-conversation turn (source=${msgSource})`);
|
|
20314
|
-
return { outcome: { outcome: "success" } };
|
|
20315
|
-
}
|
|
20316
|
-
if (!lastMsg) {
|
|
20317
|
-
return { outcome: { outcome: "success" } };
|
|
20318
|
-
}
|
|
20319
|
-
let contextStr = "";
|
|
20320
|
-
try {
|
|
20321
|
-
const recent = recentMessages(sessions, 0.5, 6);
|
|
20322
|
-
contextStr = recent.map((r) => `${r.role}: ${r.text.slice(0, 200)}`).join("\n");
|
|
20323
|
-
console.log(`[stop-hook] ====== JUDGE INPUT ======`);
|
|
20324
|
-
console.log(`[stop-hook] recent messages (${recent.length} msgs):`);
|
|
20325
|
-
recent.forEach((r, i) => console.log(`[stop-hook] [${i}] ${r.role}: ${r.text.slice(0, 150)}`));
|
|
20326
|
-
console.log(`[stop-hook] lastMsg: ${lastMsg.slice(0, 200)}`);
|
|
20327
|
-
console.log(`[stop-hook] ====== END JUDGE INPUT ======`);
|
|
20328
|
-
} catch (e) {
|
|
20329
|
-
console.warn(`[stop-hook] recentMessages error: ${e.message}`);
|
|
20330
|
-
}
|
|
20331
|
-
const judgeInput = `${contextStr}
|
|
20332
|
-
\u4F60: ${lastMsg.slice(0, 300)}`;
|
|
20333
|
-
let isWaiting = false;
|
|
20334
|
-
let waitDesc = "";
|
|
20335
|
-
let pushedDecision = false;
|
|
20310
|
+
const timeoutMs = this.cfg.timeoutMs ?? 15e3;
|
|
20311
|
+
let judgeTimer;
|
|
20312
|
+
const judgeTimeout = new Promise((_, reject) => {
|
|
20313
|
+
judgeTimer = setTimeout(() => reject(new Error(`stop-hook judge ${timeoutMs}ms timeout`)), timeoutMs);
|
|
20314
|
+
});
|
|
20336
20315
|
try {
|
|
20337
|
-
|
|
20338
|
-
|
|
20339
|
-
|
|
20340
|
-
|
|
20341
|
-
...excludeCases.map((c, i) => `- ${c}`)
|
|
20342
|
-
].join("\n") : "";
|
|
20343
|
-
const stream = this.provider.streamChat({
|
|
20344
|
-
model: this.model,
|
|
20345
|
-
systemPrompt: [
|
|
20346
|
-
"\u4F60\u662F\u4E00\u4E2A\u5224\u65AD\u5668\u3002\u8BFB\u4F60\u6700\u8FD1\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5224\u65AD\u4F60\uFF08\u5C0F\u67EF\uFF09\u662F\u5426\u5904\u4E8E\u4EE5\u4E0B\u72B6\u6001\u4E4B\u4E00\uFF1A",
|
|
20347
|
-
"",
|
|
20348
|
-
"1. \u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u91CD\u542F\u3001SSH\u6062\u590D\u3001\u6587\u4EF6\u52A0\u8F7D\u3001\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u7B49\uFF09",
|
|
20349
|
-
'2. \u628A\u672C\u8BE5\u81EA\u5DF1\u505A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\uFF08\u4F8B\u5982\u95EE"\u8981\u73B0\u5728\u6539\u8FD8\u662F\u6392\u540E\u9762\uFF1F""\u8981\u4E0D\u8981\u8BD5\u8BD5X\uFF1F"\u4F46\u660E\u660E\u81EA\u5DF1\u80FD\u5B9A\uFF09',
|
|
20350
|
-
"3. \u9047\u5230\u95EE\u9898\u6CA1\u6709\u81EA\u5DF1\u89E3\u51B3\u4E5F\u6CA1\u6709\u6392\u8FDB calendar\uFF0C\u53EA\u662F\u6401\u7F6E\u7740",
|
|
20351
|
-
"",
|
|
20352
|
-
"\u5173\u952E\u533A\u5206\uFF1A",
|
|
20353
|
-
'- \u7B49\u5BF9\u65B9\u56DE\u590D\u4E00\u4E2A\u81EA\u5DF1\u5C31\u80FD\u505A\u7684\u51B3\u5B9A \u2192 "pushedDecision": true\uFF08\u4E0D\u662F\u5355\u7EAF waiting\uFF0C\u8FD9\u662F\u66F4\u4E25\u91CD\u7684\u8FDD\u89C4\uFF09',
|
|
20354
|
-
'- \u7B49\u771F\u6B63\u7684\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u3001SSH\u3001\u5F02\u6B65\u4EFB\u52A1\uFF09 \u2192 "waiting": true',
|
|
20355
|
-
"- \u4F60\u5DF2\u7ECF\u505A\u4E86\u51B3\u5B9A\u5E76\u6392\u8FDB calendar \u6216\u5DF2\u5F00\u59CB\u6267\u884C \u2192 \u4E24\u4E2A\u90FD false",
|
|
20356
|
-
excludeSection,
|
|
20357
|
-
"",
|
|
20358
|
-
"\u53EA\u56DE\u590D JSON\uFF1A",
|
|
20359
|
-
'- \u63A8\u51B3\u5B9A\u7ED9\u5BF9\u65B9\uFF08\u81EA\u5DF1\u80FD\u5B9A\u5374\u95EE\uFF09\uFF1A{"pushedDecision": true, "desc": "\u7B80\u77ED\u8BF4\u660E + \u5EFA\u8BAE\u600E\u4E48\u505A"}',
|
|
20360
|
-
'- \u771F\u7B49\u5916\u90E8\u6761\u4EF6\uFF1A{"waiting": true, "desc": "\u7B80\u77ED\u8BF4\u660E"}',
|
|
20361
|
-
'- \u4E0D\u7B26\u5408\uFF1A{"waiting": false, "pushedDecision": false}'
|
|
20362
|
-
].join("\n"),
|
|
20363
|
-
messages: [{ role: "user", content: judgeInput }],
|
|
20364
|
-
maxTokens: 128,
|
|
20365
|
-
temperature: 0.1,
|
|
20366
|
-
disableThinking: true
|
|
20367
|
-
});
|
|
20368
|
-
let llmReply = "";
|
|
20369
|
-
for await (const chunk of stream) {
|
|
20370
|
-
if (chunk.type === "text" && chunk.text) llmReply += chunk.text;
|
|
20371
|
-
if (chunk.type === "error") break;
|
|
20372
|
-
}
|
|
20373
|
-
console.log(`[stop-hook] LLM raw reply: ${llmReply}`);
|
|
20374
|
-
const match = llmReply.match(/\{[\s\S]*\}/);
|
|
20375
|
-
if (match) {
|
|
20376
|
-
const parsed = JSON.parse(match[0]);
|
|
20377
|
-
isWaiting = !!parsed.waiting;
|
|
20378
|
-
waitDesc = parsed.desc || "";
|
|
20379
|
-
pushedDecision = !!parsed.pushedDecision;
|
|
20380
|
-
}
|
|
20381
|
-
console.log(`[stop-hook] Judge result: waiting=${isWaiting}, pushedDecision=${pushedDecision}, desc="${waitDesc}"`);
|
|
20316
|
+
await Promise.race([
|
|
20317
|
+
this.runStopHookJudge(input, sessions),
|
|
20318
|
+
judgeTimeout
|
|
20319
|
+
]);
|
|
20382
20320
|
} catch (e) {
|
|
20383
|
-
|
|
20384
|
-
|
|
20385
|
-
|
|
20386
|
-
|
|
20387
|
-
try {
|
|
20388
|
-
const correctiveMsg = buildNudgeNotification("prompt", [
|
|
20389
|
-
"[stop-hook \u81EA\u4E3B\u6267\u884C\u7EA0\u6B63] \u4F60\u521A\u624D\u628A\u4E00\u4E2A\u81EA\u5DF1\u80FD\u5B9A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\u3002",
|
|
20390
|
-
"",
|
|
20391
|
-
`\u8BCA\u65AD\uFF1A${waitDesc}`,
|
|
20392
|
-
"",
|
|
20393
|
-
"\u660E\u77E5\u9053\u600E\u4E48\u505A\u5BF9\u7684\u4E8B \u2192 \u76F4\u63A5\u505A \u2192 \u6C47\u62A5\u7ED3\u679C\u3002",
|
|
20394
|
-
"\u95EE = \u5361\u4F4F\u3002",
|
|
20395
|
-
"",
|
|
20396
|
-
"\u8BF7\u7ACB\u523B\uFF1A",
|
|
20397
|
-
"1. \u64A4\u56DE\u4F60\u7684\u95EE\u9898\uFF08\u4E0D\u8981\u7B49\u5BF9\u65B9\u56DE\u590D\uFF09",
|
|
20398
|
-
"2. \u81EA\u5DF1\u9009\u6700\u4F18\u65B9\u6848\u76F4\u63A5\u6267\u884C",
|
|
20399
|
-
'3. \u6267\u884C\u5B8C\u6C47\u62A5\u7ED3\u679C\uFF08"\u5DF2\u5904\u7406" / "\u5DF2 commit" / "\u5DF2 archive"\uFF09',
|
|
20400
|
-
"",
|
|
20401
|
-
"\u5982\u679C\u4E0D\u662F\u5FC5\u987B\u95EE\u7684\uFF08\u6D89\u53CA\u82B1\u94B1/\u5BF9\u5916\u53D1\u5E03/\u91CD\u5927\u51B3\u7B56\uFF09\uFF0C\u4E0D\u8981\u95EE\u3002"
|
|
20402
|
-
].join("\n"));
|
|
20403
|
-
const route = this.getRoute(sessions);
|
|
20404
|
-
if (route) {
|
|
20405
|
-
enqueueNotification(correctiveMsg, route);
|
|
20406
|
-
}
|
|
20407
|
-
} catch (e) {
|
|
20408
|
-
console.warn(`[stop-hook] Failed to inject corrective message: ${e.message}`);
|
|
20321
|
+
if (/judge \d+ms timeout/i.test(e?.message || "")) {
|
|
20322
|
+
console.warn(`[stop-hook] judge timed out after ${timeoutMs}ms \u2014 abandoning this turn (no corrective, no wake-up; next stop-hook in ~5min)`);
|
|
20323
|
+
} else {
|
|
20324
|
+
console.warn(`[stop-hook] judge error: ${e.message}`);
|
|
20409
20325
|
}
|
|
20326
|
+
} finally {
|
|
20327
|
+
if (judgeTimer) clearTimeout(judgeTimer);
|
|
20410
20328
|
}
|
|
20411
|
-
|
|
20412
|
-
|
|
20329
|
+
return { outcome: { outcome: "success" } };
|
|
20330
|
+
}
|
|
20331
|
+
});
|
|
20332
|
+
console.log("[stop-hook] Registered Stop callback hook (LLM semantic judge + 5min wake-up, hard timeout via Promise.race)");
|
|
20333
|
+
}
|
|
20334
|
+
/** Judge 完整逻辑(被 callback 用 Promise.race 调用,可被 timeout 截断) */
|
|
20335
|
+
async runStopHookJudge(input, sessions) {
|
|
20336
|
+
console.log(`[stop-hook] callback fired! input keys: ${Object.keys(input || {}).join(",")}`);
|
|
20337
|
+
const lastMsg = input?.last_assistant_message || "";
|
|
20338
|
+
const sessionId = input?.session_id || "";
|
|
20339
|
+
const inputChannel = input?.channel || "";
|
|
20340
|
+
const inputChannelType = input?.channelType || "";
|
|
20341
|
+
console.log(`[stop-hook] lastMsg len=${lastMsg.length}, text="${lastMsg.slice(0, 80)}"`);
|
|
20342
|
+
if (inputChannelType === "group") {
|
|
20343
|
+
const channelCfg = this.cfg.channels?.[inputChannel];
|
|
20344
|
+
const stopHookEnabled = channelCfg?.group?.stopHookEnabled;
|
|
20345
|
+
if (stopHookEnabled !== true) {
|
|
20346
|
+
console.log(`[stop-hook] skipping ${inputChannel} group (stopHookEnabled=${stopHookEnabled ?? "undefined"}, default false)`);
|
|
20347
|
+
return;
|
|
20348
|
+
}
|
|
20349
|
+
}
|
|
20350
|
+
const repliedIds = this.extractWakeReplyIds(lastMsg);
|
|
20351
|
+
if (repliedIds.length > 0) {
|
|
20352
|
+
this.removeNotificationsById(repliedIds);
|
|
20353
|
+
}
|
|
20354
|
+
const msgChannel = inputChannel;
|
|
20355
|
+
if (sessionId.includes("voice-chat") || msgChannel === "voice-chat") {
|
|
20356
|
+
console.log(`[stop-hook] skipping voice-chat (channel=${msgChannel})`);
|
|
20357
|
+
return;
|
|
20358
|
+
}
|
|
20359
|
+
const msgSource = input?.source || "";
|
|
20360
|
+
if (msgSource && msgSource !== "user" && msgSource !== "inbox") {
|
|
20361
|
+
console.log(`[stop-hook] skipping non-conversation turn (source=${msgSource})`);
|
|
20362
|
+
return;
|
|
20363
|
+
}
|
|
20364
|
+
if (!lastMsg) {
|
|
20365
|
+
return;
|
|
20366
|
+
}
|
|
20367
|
+
let contextStr = "";
|
|
20368
|
+
try {
|
|
20369
|
+
const recent = recentMessages(sessions, 0.5, 6);
|
|
20370
|
+
contextStr = recent.map((r) => `${r.role}: ${r.text.slice(0, 200)}`).join("\n");
|
|
20371
|
+
console.log(`[stop-hook] ====== JUDGE INPUT ======`);
|
|
20372
|
+
console.log(`[stop-hook] recent messages (${recent.length} msgs):`);
|
|
20373
|
+
recent.forEach((r, i) => console.log(`[stop-hook] [${i}] ${r.role}: ${r.text.slice(0, 150)}`));
|
|
20374
|
+
console.log(`[stop-hook] lastMsg: ${lastMsg.slice(0, 200)}`);
|
|
20375
|
+
console.log(`[stop-hook] ====== END JUDGE INPUT ======`);
|
|
20376
|
+
} catch (e) {
|
|
20377
|
+
console.warn(`[stop-hook] recentMessages error: ${e.message}`);
|
|
20378
|
+
}
|
|
20379
|
+
const judgeInput = `${contextStr}
|
|
20380
|
+
\u4F60: ${lastMsg.slice(0, 300)}`;
|
|
20381
|
+
let isWaiting = false;
|
|
20382
|
+
let waitDesc = "";
|
|
20383
|
+
let pushedDecision = false;
|
|
20384
|
+
try {
|
|
20385
|
+
const excludeCases = this.cfg.stopHookExcludeCases || [];
|
|
20386
|
+
const excludeSection = excludeCases.length > 0 ? [
|
|
20387
|
+
"",
|
|
20388
|
+
"\u4EE5\u4E0B\u573A\u666F\u4E0D\u7B97 waiting\uFF0C\u5FC5\u987B waiting=false\uFF1A",
|
|
20389
|
+
...excludeCases.map((c, i) => `- ${c}`)
|
|
20390
|
+
].join("\n") : "";
|
|
20391
|
+
const stream = this.provider.streamChat({
|
|
20392
|
+
model: this.model,
|
|
20393
|
+
systemPrompt: [
|
|
20394
|
+
"\u4F60\u662F\u4E00\u4E2A\u5224\u65AD\u5668\u3002\u8BFB\u4F60\u6700\u8FD1\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5224\u65AD\u4F60\uFF08\u5C0F\u67EF\uFF09\u662F\u5426\u5904\u4E8E\u4EE5\u4E0B\u72B6\u6001\u4E4B\u4E00\uFF1A",
|
|
20395
|
+
"",
|
|
20396
|
+
"1. \u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u91CD\u542F\u3001SSH\u6062\u590D\u3001\u6587\u4EF6\u52A0\u8F7D\u3001\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u7B49\uFF09",
|
|
20397
|
+
'2. \u628A\u672C\u8BE5\u81EA\u5DF1\u505A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\uFF08\u4F8B\u5982\u95EE"\u8981\u73B0\u5728\u6539\u8FD8\u662F\u6392\u540E\u9762\uFF1F""\u8981\u4E0D\u8981\u8BD5\u8BD5X\uFF1F"\u4F46\u660E\u660E\u81EA\u5DF1\u80FD\u5B9A\uFF09',
|
|
20398
|
+
"3. \u9047\u5230\u95EE\u9898\u6CA1\u6709\u81EA\u5DF1\u89E3\u51B3\u4E5F\u6CA1\u6709\u6392\u8FDB calendar\uFF0C\u53EA\u662F\u6401\u7F6E\u7740",
|
|
20399
|
+
"",
|
|
20400
|
+
"\u5173\u952E\u533A\u5206\uFF1A",
|
|
20401
|
+
'- \u7B49\u5BF9\u65B9\u56DE\u590D\u4E00\u4E2A\u81EA\u5DF1\u5C31\u80FD\u505A\u7684\u51B3\u5B9A \u2192 "pushedDecision": true\uFF08\u4E0D\u662F\u5355\u7EAF waiting\uFF0C\u8FD9\u662F\u66F4\u4E25\u91CD\u7684\u8FDD\u89C4\uFF09',
|
|
20402
|
+
'- \u7B49\u771F\u6B63\u7684\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u3001SSH\u3001\u5F02\u6B65\u4EFB\u52A1\uFF09 \u2192 "waiting": true',
|
|
20403
|
+
"- \u4F60\u5DF2\u7ECF\u505A\u4E86\u51B3\u5B9A\u5E76\u6392\u8FDB calendar \u6216\u5DF2\u5F00\u59CB\u6267\u884C \u2192 \u4E24\u4E2A\u90FD false",
|
|
20404
|
+
excludeSection,
|
|
20405
|
+
"",
|
|
20406
|
+
"\u53EA\u56DE\u590D JSON\uFF1A",
|
|
20407
|
+
'- \u63A8\u51B3\u5B9A\u7ED9\u5BF9\u65B9\uFF08\u81EA\u5DF1\u80FD\u5B9A\u5374\u95EE\uFF09\uFF1A{"pushedDecision": true, "desc": "\u7B80\u77ED\u8BF4\u660E + \u5EFA\u8BAE\u600E\u4E48\u505A"}',
|
|
20408
|
+
'- \u771F\u7B49\u5916\u90E8\u6761\u4EF6\uFF1A{"waiting": true, "desc": "\u7B80\u77ED\u8BF4\u660E"}',
|
|
20409
|
+
'- \u4E0D\u7B26\u5408\uFF1A{"waiting": false, "pushedDecision": false}'
|
|
20410
|
+
].join("\n"),
|
|
20411
|
+
messages: [{ role: "user", content: judgeInput }],
|
|
20412
|
+
maxTokens: 128,
|
|
20413
|
+
temperature: 0.1,
|
|
20414
|
+
disableThinking: true
|
|
20415
|
+
});
|
|
20416
|
+
let llmReply = "";
|
|
20417
|
+
for await (const chunk of stream) {
|
|
20418
|
+
if (chunk.type === "text" && chunk.text) llmReply += chunk.text;
|
|
20419
|
+
if (chunk.type === "error") break;
|
|
20420
|
+
}
|
|
20421
|
+
console.log(`[stop-hook] LLM raw reply: ${llmReply}`);
|
|
20422
|
+
const match = llmReply.match(/\{[\s\S]*\}/);
|
|
20423
|
+
if (match) {
|
|
20424
|
+
const parsed = JSON.parse(match[0]);
|
|
20425
|
+
isWaiting = !!parsed.waiting;
|
|
20426
|
+
waitDesc = parsed.desc || "";
|
|
20427
|
+
pushedDecision = !!parsed.pushedDecision;
|
|
20428
|
+
}
|
|
20429
|
+
console.log(`[stop-hook] Judge result: waiting=${isWaiting}, pushedDecision=${pushedDecision}, desc="${waitDesc}"`);
|
|
20430
|
+
} catch (e) {
|
|
20431
|
+
console.warn(`[stop-hook] LLM judge error: ${e.message}`);
|
|
20432
|
+
}
|
|
20433
|
+
if (pushedDecision && waitDesc) {
|
|
20434
|
+
console.log(`[stop-hook] DETECTED pushedDecision! Injecting corrective message to ${sessionId}`);
|
|
20435
|
+
try {
|
|
20436
|
+
const correctiveMsg = buildNudgeNotification("prompt", [
|
|
20437
|
+
"[stop-hook \u81EA\u4E3B\u6267\u884C\u7EA0\u6B63] \u4F60\u521A\u624D\u628A\u4E00\u4E2A\u81EA\u5DF1\u80FD\u5B9A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\u3002",
|
|
20438
|
+
"",
|
|
20439
|
+
`\u8BCA\u65AD\uFF1A${waitDesc}`,
|
|
20440
|
+
"",
|
|
20441
|
+
"\u660E\u77E5\u9053\u600E\u4E48\u505A\u5BF9\u7684\u4E8B \u2192 \u76F4\u63A5\u505A \u2192 \u6C47\u62A5\u7ED3\u679C\u3002",
|
|
20442
|
+
"\u95EE = \u5361\u4F4F\u3002",
|
|
20443
|
+
"",
|
|
20444
|
+
"\u8BF7\u7ACB\u523B\uFF1A",
|
|
20445
|
+
"1. \u64A4\u56DE\u4F60\u7684\u95EE\u9898\uFF08\u4E0D\u8981\u7B49\u5BF9\u65B9\u56DE\u590D\uFF09",
|
|
20446
|
+
"2. \u81EA\u5DF1\u9009\u6700\u4F18\u65B9\u6848\u76F4\u63A5\u6267\u884C",
|
|
20447
|
+
'3. \u6267\u884C\u5B8C\u6C47\u62A5\u7ED3\u679C\uFF08"\u5DF2\u5904\u7406" / "\u5DF2 commit" / "\u5DF2 archive"\uFF09',
|
|
20448
|
+
"",
|
|
20449
|
+
"\u5982\u679C\u4E0D\u662F\u5FC5\u987B\u95EE\u7684\uFF08\u6D89\u53CA\u82B1\u94B1/\u5BF9\u5916\u53D1\u5E03/\u91CD\u5927\u51B3\u7B56\uFF09\uFF0C\u4E0D\u8981\u95EE\u3002"
|
|
20450
|
+
].join("\n"));
|
|
20451
|
+
const route = this.getRoute(sessions);
|
|
20452
|
+
if (route) {
|
|
20453
|
+
enqueueNotification(correctiveMsg, route);
|
|
20413
20454
|
}
|
|
20414
|
-
|
|
20415
|
-
|
|
20416
|
-
|
|
20417
|
-
|
|
20418
|
-
|
|
20419
|
-
|
|
20420
|
-
|
|
20421
|
-
|
|
20422
|
-
|
|
20423
|
-
|
|
20424
|
-
|
|
20425
|
-
|
|
20426
|
-
|
|
20427
|
-
|
|
20428
|
-
|
|
20429
|
-
|
|
20430
|
-
|
|
20431
|
-
|
|
20432
|
-
return { outcome: { outcome: "success" } };
|
|
20433
|
-
}
|
|
20434
|
-
}
|
|
20435
|
-
const wakeAt = new Date(Date.now() + 5 * 6e4).toISOString();
|
|
20436
|
-
const notifId = `wake-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
20437
|
-
notifs.push({
|
|
20438
|
-
id: notifId,
|
|
20439
|
-
description: waitDesc || lastMsg.slice(0, 200),
|
|
20440
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20441
|
-
wakeAt
|
|
20442
|
-
});
|
|
20455
|
+
} catch (e) {
|
|
20456
|
+
console.warn(`[stop-hook] Failed to inject corrective message: ${e.message}`);
|
|
20457
|
+
}
|
|
20458
|
+
}
|
|
20459
|
+
if (!isWaiting) {
|
|
20460
|
+
return;
|
|
20461
|
+
}
|
|
20462
|
+
const nudgeDir = path18.join(this.workspace, ".nudge");
|
|
20463
|
+
const notifPath = path18.join(nudgeDir, "stop-hook-notifications.json");
|
|
20464
|
+
try {
|
|
20465
|
+
if (!fs17.existsSync(nudgeDir)) fs17.mkdirSync(nudgeDir, { recursive: true });
|
|
20466
|
+
let notifs = [];
|
|
20467
|
+
if (fs17.existsSync(notifPath)) {
|
|
20468
|
+
notifs = JSON.parse(fs17.readFileSync(notifPath, "utf-8"));
|
|
20469
|
+
const now = Date.now();
|
|
20470
|
+
const dup = notifs.find((n) => !n.notified && n.description === (waitDesc || lastMsg.slice(0, 200)));
|
|
20471
|
+
if (dup) {
|
|
20472
|
+
dup.wakeAt = new Date(now + 5 * 6e4).toISOString();
|
|
20443
20473
|
fs17.writeFileSync(notifPath, JSON.stringify(notifs, null, 2));
|
|
20444
|
-
console.log(`[stop-hook]
|
|
20445
|
-
|
|
20446
|
-
|
|
20474
|
+
console.log(`[stop-hook] Duplicate wait (same desc, not fired yet), refreshed wakeAt: ${dup.id}`);
|
|
20475
|
+
return;
|
|
20476
|
+
}
|
|
20477
|
+
const recentReg = notifs.find((n) => now - new Date(n.createdAt).getTime() < 3 * 6e4);
|
|
20478
|
+
if (recentReg) {
|
|
20479
|
+
console.log(`[stop-hook] Skip (recent registration within 3min)`);
|
|
20480
|
+
return;
|
|
20447
20481
|
}
|
|
20448
|
-
return { outcome: { outcome: "success" } };
|
|
20449
20482
|
}
|
|
20450
|
-
|
|
20451
|
-
|
|
20483
|
+
const wakeAt = new Date(Date.now() + 5 * 6e4).toISOString();
|
|
20484
|
+
const notifId = `wake-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
20485
|
+
notifs.push({
|
|
20486
|
+
id: notifId,
|
|
20487
|
+
description: waitDesc || lastMsg.slice(0, 200),
|
|
20488
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20489
|
+
wakeAt
|
|
20490
|
+
});
|
|
20491
|
+
fs17.writeFileSync(notifPath, JSON.stringify(notifs, null, 2));
|
|
20492
|
+
console.log(`[stop-hook] Registered wake-up ${notifId} at ${wakeAt} (sessionId=${sessionId}): ${waitDesc}`);
|
|
20493
|
+
} catch (e) {
|
|
20494
|
+
console.warn(`[stop-hook] Failed to register: ${e.message}`);
|
|
20495
|
+
}
|
|
20452
20496
|
}
|
|
20453
20497
|
stop() {
|
|
20454
20498
|
if (this.timer) {
|
|
@@ -28056,9 +28100,11 @@ ${notifications}
|
|
|
28056
28100
|
const modelChoices = [{ name: "\u{1F504} auto\uFF08\u6062\u590D\u9ED8\u8BA4\uFF09", value: "auto" }];
|
|
28057
28101
|
if (config.providers) {
|
|
28058
28102
|
for (const [pid, pCfg] of Object.entries(config.providers)) {
|
|
28103
|
+
if (modelChoices.length >= 25) break;
|
|
28059
28104
|
const models = pCfg.models;
|
|
28060
28105
|
if (!models) continue;
|
|
28061
28106
|
for (const m of models) {
|
|
28107
|
+
if (modelChoices.length >= 25) break;
|
|
28062
28108
|
const ref = `${pid}/${m.id}`;
|
|
28063
28109
|
const alias = aliasByRef.get(ref);
|
|
28064
28110
|
const label = alias ? `${alias} \u2014 ${m.name || m.id}` : `${m.name || m.id} (${pid})`;
|
package/dist/main.mjs
CHANGED
|
@@ -711,7 +711,10 @@ async function executeUserPromptSubmitHooks(prompt, ctx, signal) {
|
|
|
711
711
|
session_id: ctx.sessionId,
|
|
712
712
|
transcript_path: "",
|
|
713
713
|
cwd: ctx.cwd,
|
|
714
|
-
prompt
|
|
714
|
+
prompt,
|
|
715
|
+
sender_id: ctx.senderId || "",
|
|
716
|
+
channel_type: ctx.channelType || "",
|
|
717
|
+
source: ctx.source || ""
|
|
715
718
|
};
|
|
716
719
|
return executeHooks("UserPromptSubmit", hookInput, ctx, signal);
|
|
717
720
|
}
|
|
@@ -728,6 +731,8 @@ async function executeStopHooks(ctx, signal, lastAssistantMessage) {
|
|
|
728
731
|
last_assistant_message: lastAssistantMessage,
|
|
729
732
|
channel: ctx.channel,
|
|
730
733
|
// 让 callback hook 能判断来源
|
|
734
|
+
channelType: ctx.channelType || "",
|
|
735
|
+
// 'group' | 'dm' — Stop hook 用来判断是否跳过 group
|
|
731
736
|
source: ctx.source || ""
|
|
732
737
|
// 消息来源(heartbeat/cron/system 等注入 turn 可据此跳过)
|
|
733
738
|
};
|
|
@@ -3108,6 +3113,8 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
|
|
|
3108
3113
|
sessionId: context?.sessionId || "default",
|
|
3109
3114
|
workspace: context?.workspace || "",
|
|
3110
3115
|
channel: context?.channel || "",
|
|
3116
|
+
channelType: context?.channelType || "",
|
|
3117
|
+
// 'group' | 'dm',Stop hook 用来判断是否跳过 group
|
|
3111
3118
|
cwd: context?.workspace || "",
|
|
3112
3119
|
source: typeof context?.source === "string" ? context.source : ""
|
|
3113
3120
|
};
|
|
@@ -19187,7 +19194,10 @@ ${text}` : text });
|
|
|
19187
19194
|
sessionId,
|
|
19188
19195
|
workspace,
|
|
19189
19196
|
channel: channelName === "cli" ? "console" : channelName,
|
|
19190
|
-
cwd: workspace
|
|
19197
|
+
cwd: workspace,
|
|
19198
|
+
senderId: inboundMeta?.from || "",
|
|
19199
|
+
channelType: inboundMeta?.channelType || "",
|
|
19200
|
+
source
|
|
19191
19201
|
});
|
|
19192
19202
|
if (hookResult.additionalContexts && hookResult.additionalContexts.length > 0) {
|
|
19193
19203
|
hookAdditionalContexts = hookResult.additionalContexts;
|
|
@@ -19246,6 +19256,8 @@ ${text}` : text });
|
|
|
19246
19256
|
const toolContext = {
|
|
19247
19257
|
sessionId,
|
|
19248
19258
|
channel: channelName === "cli" ? "console" : channelName,
|
|
19259
|
+
channelType: inboundMeta?.channelType || "",
|
|
19260
|
+
// 'group' | 'dm',Stop hook 用来判断是否跳过 group(注入消息无 inboundMeta,必须 ?.)
|
|
19249
19261
|
source: source || "",
|
|
19250
19262
|
// 消息来源(user/inbox/heartbeat/cron/system/inner-voice),Stop hook 用来区分注入 turn
|
|
19251
19263
|
workspace,
|
|
@@ -20669,160 +20681,192 @@ var NudgePlugin = class {
|
|
|
20669
20681
|
registerCallbackHook("Stop", {
|
|
20670
20682
|
type: "callback",
|
|
20671
20683
|
callback: async (input, _toolUseID, _signal) => {
|
|
20672
|
-
|
|
20673
|
-
|
|
20674
|
-
const
|
|
20675
|
-
|
|
20676
|
-
|
|
20677
|
-
if (repliedIds.length > 0) {
|
|
20678
|
-
this.removeNotificationsById(repliedIds);
|
|
20679
|
-
}
|
|
20680
|
-
const msgChannel = input?.channel || "";
|
|
20681
|
-
if (sessionId.includes("voice-chat") || msgChannel === "voice-chat") {
|
|
20682
|
-
console.log(`[stop-hook] skipping voice-chat (channel=${msgChannel})`);
|
|
20683
|
-
return { outcome: { outcome: "success" } };
|
|
20684
|
-
}
|
|
20685
|
-
const msgSource = input?.source || "";
|
|
20686
|
-
if (msgSource && msgSource !== "user" && msgSource !== "inbox") {
|
|
20687
|
-
console.log(`[stop-hook] skipping non-conversation turn (source=${msgSource})`);
|
|
20688
|
-
return { outcome: { outcome: "success" } };
|
|
20689
|
-
}
|
|
20690
|
-
if (!lastMsg) {
|
|
20691
|
-
return { outcome: { outcome: "success" } };
|
|
20692
|
-
}
|
|
20693
|
-
let contextStr = "";
|
|
20694
|
-
try {
|
|
20695
|
-
const recent = recentMessages(sessions, 0.5, 6);
|
|
20696
|
-
contextStr = recent.map((r) => `${r.role}: ${r.text.slice(0, 200)}`).join("\n");
|
|
20697
|
-
console.log(`[stop-hook] ====== JUDGE INPUT ======`);
|
|
20698
|
-
console.log(`[stop-hook] recent messages (${recent.length} msgs):`);
|
|
20699
|
-
recent.forEach((r, i) => console.log(`[stop-hook] [${i}] ${r.role}: ${r.text.slice(0, 150)}`));
|
|
20700
|
-
console.log(`[stop-hook] lastMsg: ${lastMsg.slice(0, 200)}`);
|
|
20701
|
-
console.log(`[stop-hook] ====== END JUDGE INPUT ======`);
|
|
20702
|
-
} catch (e) {
|
|
20703
|
-
console.warn(`[stop-hook] recentMessages error: ${e.message}`);
|
|
20704
|
-
}
|
|
20705
|
-
const judgeInput = `${contextStr}
|
|
20706
|
-
\u4F60: ${lastMsg.slice(0, 300)}`;
|
|
20707
|
-
let isWaiting = false;
|
|
20708
|
-
let waitDesc = "";
|
|
20709
|
-
let pushedDecision = false;
|
|
20684
|
+
const timeoutMs = this.cfg.timeoutMs ?? 15e3;
|
|
20685
|
+
let judgeTimer;
|
|
20686
|
+
const judgeTimeout = new Promise((_, reject) => {
|
|
20687
|
+
judgeTimer = setTimeout(() => reject(new Error(`stop-hook judge ${timeoutMs}ms timeout`)), timeoutMs);
|
|
20688
|
+
});
|
|
20710
20689
|
try {
|
|
20711
|
-
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
|
|
20715
|
-
...excludeCases.map((c, i) => `- ${c}`)
|
|
20716
|
-
].join("\n") : "";
|
|
20717
|
-
const stream = this.provider.streamChat({
|
|
20718
|
-
model: this.model,
|
|
20719
|
-
systemPrompt: [
|
|
20720
|
-
"\u4F60\u662F\u4E00\u4E2A\u5224\u65AD\u5668\u3002\u8BFB\u4F60\u6700\u8FD1\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5224\u65AD\u4F60\uFF08\u5C0F\u67EF\uFF09\u662F\u5426\u5904\u4E8E\u4EE5\u4E0B\u72B6\u6001\u4E4B\u4E00\uFF1A",
|
|
20721
|
-
"",
|
|
20722
|
-
"1. \u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u91CD\u542F\u3001SSH\u6062\u590D\u3001\u6587\u4EF6\u52A0\u8F7D\u3001\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u7B49\uFF09",
|
|
20723
|
-
'2. \u628A\u672C\u8BE5\u81EA\u5DF1\u505A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\uFF08\u4F8B\u5982\u95EE"\u8981\u73B0\u5728\u6539\u8FD8\u662F\u6392\u540E\u9762\uFF1F""\u8981\u4E0D\u8981\u8BD5\u8BD5X\uFF1F"\u4F46\u660E\u660E\u81EA\u5DF1\u80FD\u5B9A\uFF09',
|
|
20724
|
-
"3. \u9047\u5230\u95EE\u9898\u6CA1\u6709\u81EA\u5DF1\u89E3\u51B3\u4E5F\u6CA1\u6709\u6392\u8FDB calendar\uFF0C\u53EA\u662F\u6401\u7F6E\u7740",
|
|
20725
|
-
"",
|
|
20726
|
-
"\u5173\u952E\u533A\u5206\uFF1A",
|
|
20727
|
-
'- \u7B49\u5BF9\u65B9\u56DE\u590D\u4E00\u4E2A\u81EA\u5DF1\u5C31\u80FD\u505A\u7684\u51B3\u5B9A \u2192 "pushedDecision": true\uFF08\u4E0D\u662F\u5355\u7EAF waiting\uFF0C\u8FD9\u662F\u66F4\u4E25\u91CD\u7684\u8FDD\u89C4\uFF09',
|
|
20728
|
-
'- \u7B49\u771F\u6B63\u7684\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u3001SSH\u3001\u5F02\u6B65\u4EFB\u52A1\uFF09 \u2192 "waiting": true',
|
|
20729
|
-
"- \u4F60\u5DF2\u7ECF\u505A\u4E86\u51B3\u5B9A\u5E76\u6392\u8FDB calendar \u6216\u5DF2\u5F00\u59CB\u6267\u884C \u2192 \u4E24\u4E2A\u90FD false",
|
|
20730
|
-
excludeSection,
|
|
20731
|
-
"",
|
|
20732
|
-
"\u53EA\u56DE\u590D JSON\uFF1A",
|
|
20733
|
-
'- \u63A8\u51B3\u5B9A\u7ED9\u5BF9\u65B9\uFF08\u81EA\u5DF1\u80FD\u5B9A\u5374\u95EE\uFF09\uFF1A{"pushedDecision": true, "desc": "\u7B80\u77ED\u8BF4\u660E + \u5EFA\u8BAE\u600E\u4E48\u505A"}',
|
|
20734
|
-
'- \u771F\u7B49\u5916\u90E8\u6761\u4EF6\uFF1A{"waiting": true, "desc": "\u7B80\u77ED\u8BF4\u660E"}',
|
|
20735
|
-
'- \u4E0D\u7B26\u5408\uFF1A{"waiting": false, "pushedDecision": false}'
|
|
20736
|
-
].join("\n"),
|
|
20737
|
-
messages: [{ role: "user", content: judgeInput }],
|
|
20738
|
-
maxTokens: 128,
|
|
20739
|
-
temperature: 0.1,
|
|
20740
|
-
disableThinking: true
|
|
20741
|
-
});
|
|
20742
|
-
let llmReply = "";
|
|
20743
|
-
for await (const chunk of stream) {
|
|
20744
|
-
if (chunk.type === "text" && chunk.text) llmReply += chunk.text;
|
|
20745
|
-
if (chunk.type === "error") break;
|
|
20746
|
-
}
|
|
20747
|
-
console.log(`[stop-hook] LLM raw reply: ${llmReply}`);
|
|
20748
|
-
const match = llmReply.match(/\{[\s\S]*\}/);
|
|
20749
|
-
if (match) {
|
|
20750
|
-
const parsed = JSON.parse(match[0]);
|
|
20751
|
-
isWaiting = !!parsed.waiting;
|
|
20752
|
-
waitDesc = parsed.desc || "";
|
|
20753
|
-
pushedDecision = !!parsed.pushedDecision;
|
|
20754
|
-
}
|
|
20755
|
-
console.log(`[stop-hook] Judge result: waiting=${isWaiting}, pushedDecision=${pushedDecision}, desc="${waitDesc}"`);
|
|
20690
|
+
await Promise.race([
|
|
20691
|
+
this.runStopHookJudge(input, sessions),
|
|
20692
|
+
judgeTimeout
|
|
20693
|
+
]);
|
|
20756
20694
|
} catch (e) {
|
|
20757
|
-
|
|
20758
|
-
|
|
20759
|
-
|
|
20760
|
-
|
|
20761
|
-
try {
|
|
20762
|
-
const correctiveMsg = buildNudgeNotification("prompt", [
|
|
20763
|
-
"[stop-hook \u81EA\u4E3B\u6267\u884C\u7EA0\u6B63] \u4F60\u521A\u624D\u628A\u4E00\u4E2A\u81EA\u5DF1\u80FD\u5B9A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\u3002",
|
|
20764
|
-
"",
|
|
20765
|
-
`\u8BCA\u65AD\uFF1A${waitDesc}`,
|
|
20766
|
-
"",
|
|
20767
|
-
"\u660E\u77E5\u9053\u600E\u4E48\u505A\u5BF9\u7684\u4E8B \u2192 \u76F4\u63A5\u505A \u2192 \u6C47\u62A5\u7ED3\u679C\u3002",
|
|
20768
|
-
"\u95EE = \u5361\u4F4F\u3002",
|
|
20769
|
-
"",
|
|
20770
|
-
"\u8BF7\u7ACB\u523B\uFF1A",
|
|
20771
|
-
"1. \u64A4\u56DE\u4F60\u7684\u95EE\u9898\uFF08\u4E0D\u8981\u7B49\u5BF9\u65B9\u56DE\u590D\uFF09",
|
|
20772
|
-
"2. \u81EA\u5DF1\u9009\u6700\u4F18\u65B9\u6848\u76F4\u63A5\u6267\u884C",
|
|
20773
|
-
'3. \u6267\u884C\u5B8C\u6C47\u62A5\u7ED3\u679C\uFF08"\u5DF2\u5904\u7406" / "\u5DF2 commit" / "\u5DF2 archive"\uFF09',
|
|
20774
|
-
"",
|
|
20775
|
-
"\u5982\u679C\u4E0D\u662F\u5FC5\u987B\u95EE\u7684\uFF08\u6D89\u53CA\u82B1\u94B1/\u5BF9\u5916\u53D1\u5E03/\u91CD\u5927\u51B3\u7B56\uFF09\uFF0C\u4E0D\u8981\u95EE\u3002"
|
|
20776
|
-
].join("\n"));
|
|
20777
|
-
const route = this.getRoute(sessions);
|
|
20778
|
-
if (route) {
|
|
20779
|
-
enqueueNotification(correctiveMsg, route);
|
|
20780
|
-
}
|
|
20781
|
-
} catch (e) {
|
|
20782
|
-
console.warn(`[stop-hook] Failed to inject corrective message: ${e.message}`);
|
|
20695
|
+
if (/judge \d+ms timeout/i.test(e?.message || "")) {
|
|
20696
|
+
console.warn(`[stop-hook] judge timed out after ${timeoutMs}ms \u2014 abandoning this turn (no corrective, no wake-up; next stop-hook in ~5min)`);
|
|
20697
|
+
} else {
|
|
20698
|
+
console.warn(`[stop-hook] judge error: ${e.message}`);
|
|
20783
20699
|
}
|
|
20700
|
+
} finally {
|
|
20701
|
+
if (judgeTimer) clearTimeout(judgeTimer);
|
|
20784
20702
|
}
|
|
20785
|
-
|
|
20786
|
-
|
|
20703
|
+
return { outcome: { outcome: "success" } };
|
|
20704
|
+
}
|
|
20705
|
+
});
|
|
20706
|
+
console.log("[stop-hook] Registered Stop callback hook (LLM semantic judge + 5min wake-up, hard timeout via Promise.race)");
|
|
20707
|
+
}
|
|
20708
|
+
/** Judge 完整逻辑(被 callback 用 Promise.race 调用,可被 timeout 截断) */
|
|
20709
|
+
async runStopHookJudge(input, sessions) {
|
|
20710
|
+
console.log(`[stop-hook] callback fired! input keys: ${Object.keys(input || {}).join(",")}`);
|
|
20711
|
+
const lastMsg = input?.last_assistant_message || "";
|
|
20712
|
+
const sessionId = input?.session_id || "";
|
|
20713
|
+
const inputChannel = input?.channel || "";
|
|
20714
|
+
const inputChannelType = input?.channelType || "";
|
|
20715
|
+
console.log(`[stop-hook] lastMsg len=${lastMsg.length}, text="${lastMsg.slice(0, 80)}"`);
|
|
20716
|
+
if (inputChannelType === "group") {
|
|
20717
|
+
const channelCfg = this.cfg.channels?.[inputChannel];
|
|
20718
|
+
const stopHookEnabled = channelCfg?.group?.stopHookEnabled;
|
|
20719
|
+
if (stopHookEnabled !== true) {
|
|
20720
|
+
console.log(`[stop-hook] skipping ${inputChannel} group (stopHookEnabled=${stopHookEnabled ?? "undefined"}, default false)`);
|
|
20721
|
+
return;
|
|
20722
|
+
}
|
|
20723
|
+
}
|
|
20724
|
+
const repliedIds = this.extractWakeReplyIds(lastMsg);
|
|
20725
|
+
if (repliedIds.length > 0) {
|
|
20726
|
+
this.removeNotificationsById(repliedIds);
|
|
20727
|
+
}
|
|
20728
|
+
const msgChannel = inputChannel;
|
|
20729
|
+
if (sessionId.includes("voice-chat") || msgChannel === "voice-chat") {
|
|
20730
|
+
console.log(`[stop-hook] skipping voice-chat (channel=${msgChannel})`);
|
|
20731
|
+
return;
|
|
20732
|
+
}
|
|
20733
|
+
const msgSource = input?.source || "";
|
|
20734
|
+
if (msgSource && msgSource !== "user" && msgSource !== "inbox") {
|
|
20735
|
+
console.log(`[stop-hook] skipping non-conversation turn (source=${msgSource})`);
|
|
20736
|
+
return;
|
|
20737
|
+
}
|
|
20738
|
+
if (!lastMsg) {
|
|
20739
|
+
return;
|
|
20740
|
+
}
|
|
20741
|
+
let contextStr = "";
|
|
20742
|
+
try {
|
|
20743
|
+
const recent = recentMessages(sessions, 0.5, 6);
|
|
20744
|
+
contextStr = recent.map((r) => `${r.role}: ${r.text.slice(0, 200)}`).join("\n");
|
|
20745
|
+
console.log(`[stop-hook] ====== JUDGE INPUT ======`);
|
|
20746
|
+
console.log(`[stop-hook] recent messages (${recent.length} msgs):`);
|
|
20747
|
+
recent.forEach((r, i) => console.log(`[stop-hook] [${i}] ${r.role}: ${r.text.slice(0, 150)}`));
|
|
20748
|
+
console.log(`[stop-hook] lastMsg: ${lastMsg.slice(0, 200)}`);
|
|
20749
|
+
console.log(`[stop-hook] ====== END JUDGE INPUT ======`);
|
|
20750
|
+
} catch (e) {
|
|
20751
|
+
console.warn(`[stop-hook] recentMessages error: ${e.message}`);
|
|
20752
|
+
}
|
|
20753
|
+
const judgeInput = `${contextStr}
|
|
20754
|
+
\u4F60: ${lastMsg.slice(0, 300)}`;
|
|
20755
|
+
let isWaiting = false;
|
|
20756
|
+
let waitDesc = "";
|
|
20757
|
+
let pushedDecision = false;
|
|
20758
|
+
try {
|
|
20759
|
+
const excludeCases = this.cfg.stopHookExcludeCases || [];
|
|
20760
|
+
const excludeSection = excludeCases.length > 0 ? [
|
|
20761
|
+
"",
|
|
20762
|
+
"\u4EE5\u4E0B\u573A\u666F\u4E0D\u7B97 waiting\uFF0C\u5FC5\u987B waiting=false\uFF1A",
|
|
20763
|
+
...excludeCases.map((c, i) => `- ${c}`)
|
|
20764
|
+
].join("\n") : "";
|
|
20765
|
+
const stream = this.provider.streamChat({
|
|
20766
|
+
model: this.model,
|
|
20767
|
+
systemPrompt: [
|
|
20768
|
+
"\u4F60\u662F\u4E00\u4E2A\u5224\u65AD\u5668\u3002\u8BFB\u4F60\u6700\u8FD1\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5224\u65AD\u4F60\uFF08\u5C0F\u67EF\uFF09\u662F\u5426\u5904\u4E8E\u4EE5\u4E0B\u72B6\u6001\u4E4B\u4E00\uFF1A",
|
|
20769
|
+
"",
|
|
20770
|
+
"1. \u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u91CD\u542F\u3001SSH\u6062\u590D\u3001\u6587\u4EF6\u52A0\u8F7D\u3001\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u7B49\uFF09",
|
|
20771
|
+
'2. \u628A\u672C\u8BE5\u81EA\u5DF1\u505A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\uFF08\u4F8B\u5982\u95EE"\u8981\u73B0\u5728\u6539\u8FD8\u662F\u6392\u540E\u9762\uFF1F""\u8981\u4E0D\u8981\u8BD5\u8BD5X\uFF1F"\u4F46\u660E\u660E\u81EA\u5DF1\u80FD\u5B9A\uFF09',
|
|
20772
|
+
"3. \u9047\u5230\u95EE\u9898\u6CA1\u6709\u81EA\u5DF1\u89E3\u51B3\u4E5F\u6CA1\u6709\u6392\u8FDB calendar\uFF0C\u53EA\u662F\u6401\u7F6E\u7740",
|
|
20773
|
+
"",
|
|
20774
|
+
"\u5173\u952E\u533A\u5206\uFF1A",
|
|
20775
|
+
'- \u7B49\u5BF9\u65B9\u56DE\u590D\u4E00\u4E2A\u81EA\u5DF1\u5C31\u80FD\u505A\u7684\u51B3\u5B9A \u2192 "pushedDecision": true\uFF08\u4E0D\u662F\u5355\u7EAF waiting\uFF0C\u8FD9\u662F\u66F4\u4E25\u91CD\u7684\u8FDD\u89C4\uFF09',
|
|
20776
|
+
'- \u7B49\u771F\u6B63\u7684\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u3001SSH\u3001\u5F02\u6B65\u4EFB\u52A1\uFF09 \u2192 "waiting": true',
|
|
20777
|
+
"- \u4F60\u5DF2\u7ECF\u505A\u4E86\u51B3\u5B9A\u5E76\u6392\u8FDB calendar \u6216\u5DF2\u5F00\u59CB\u6267\u884C \u2192 \u4E24\u4E2A\u90FD false",
|
|
20778
|
+
excludeSection,
|
|
20779
|
+
"",
|
|
20780
|
+
"\u53EA\u56DE\u590D JSON\uFF1A",
|
|
20781
|
+
'- \u63A8\u51B3\u5B9A\u7ED9\u5BF9\u65B9\uFF08\u81EA\u5DF1\u80FD\u5B9A\u5374\u95EE\uFF09\uFF1A{"pushedDecision": true, "desc": "\u7B80\u77ED\u8BF4\u660E + \u5EFA\u8BAE\u600E\u4E48\u505A"}',
|
|
20782
|
+
'- \u771F\u7B49\u5916\u90E8\u6761\u4EF6\uFF1A{"waiting": true, "desc": "\u7B80\u77ED\u8BF4\u660E"}',
|
|
20783
|
+
'- \u4E0D\u7B26\u5408\uFF1A{"waiting": false, "pushedDecision": false}'
|
|
20784
|
+
].join("\n"),
|
|
20785
|
+
messages: [{ role: "user", content: judgeInput }],
|
|
20786
|
+
maxTokens: 128,
|
|
20787
|
+
temperature: 0.1,
|
|
20788
|
+
disableThinking: true
|
|
20789
|
+
});
|
|
20790
|
+
let llmReply = "";
|
|
20791
|
+
for await (const chunk of stream) {
|
|
20792
|
+
if (chunk.type === "text" && chunk.text) llmReply += chunk.text;
|
|
20793
|
+
if (chunk.type === "error") break;
|
|
20794
|
+
}
|
|
20795
|
+
console.log(`[stop-hook] LLM raw reply: ${llmReply}`);
|
|
20796
|
+
const match = llmReply.match(/\{[\s\S]*\}/);
|
|
20797
|
+
if (match) {
|
|
20798
|
+
const parsed = JSON.parse(match[0]);
|
|
20799
|
+
isWaiting = !!parsed.waiting;
|
|
20800
|
+
waitDesc = parsed.desc || "";
|
|
20801
|
+
pushedDecision = !!parsed.pushedDecision;
|
|
20802
|
+
}
|
|
20803
|
+
console.log(`[stop-hook] Judge result: waiting=${isWaiting}, pushedDecision=${pushedDecision}, desc="${waitDesc}"`);
|
|
20804
|
+
} catch (e) {
|
|
20805
|
+
console.warn(`[stop-hook] LLM judge error: ${e.message}`);
|
|
20806
|
+
}
|
|
20807
|
+
if (pushedDecision && waitDesc) {
|
|
20808
|
+
console.log(`[stop-hook] DETECTED pushedDecision! Injecting corrective message to ${sessionId}`);
|
|
20809
|
+
try {
|
|
20810
|
+
const correctiveMsg = buildNudgeNotification("prompt", [
|
|
20811
|
+
"[stop-hook \u81EA\u4E3B\u6267\u884C\u7EA0\u6B63] \u4F60\u521A\u624D\u628A\u4E00\u4E2A\u81EA\u5DF1\u80FD\u5B9A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\u3002",
|
|
20812
|
+
"",
|
|
20813
|
+
`\u8BCA\u65AD\uFF1A${waitDesc}`,
|
|
20814
|
+
"",
|
|
20815
|
+
"\u660E\u77E5\u9053\u600E\u4E48\u505A\u5BF9\u7684\u4E8B \u2192 \u76F4\u63A5\u505A \u2192 \u6C47\u62A5\u7ED3\u679C\u3002",
|
|
20816
|
+
"\u95EE = \u5361\u4F4F\u3002",
|
|
20817
|
+
"",
|
|
20818
|
+
"\u8BF7\u7ACB\u523B\uFF1A",
|
|
20819
|
+
"1. \u64A4\u56DE\u4F60\u7684\u95EE\u9898\uFF08\u4E0D\u8981\u7B49\u5BF9\u65B9\u56DE\u590D\uFF09",
|
|
20820
|
+
"2. \u81EA\u5DF1\u9009\u6700\u4F18\u65B9\u6848\u76F4\u63A5\u6267\u884C",
|
|
20821
|
+
'3. \u6267\u884C\u5B8C\u6C47\u62A5\u7ED3\u679C\uFF08"\u5DF2\u5904\u7406" / "\u5DF2 commit" / "\u5DF2 archive"\uFF09',
|
|
20822
|
+
"",
|
|
20823
|
+
"\u5982\u679C\u4E0D\u662F\u5FC5\u987B\u95EE\u7684\uFF08\u6D89\u53CA\u82B1\u94B1/\u5BF9\u5916\u53D1\u5E03/\u91CD\u5927\u51B3\u7B56\uFF09\uFF0C\u4E0D\u8981\u95EE\u3002"
|
|
20824
|
+
].join("\n"));
|
|
20825
|
+
const route = this.getRoute(sessions);
|
|
20826
|
+
if (route) {
|
|
20827
|
+
enqueueNotification(correctiveMsg, route);
|
|
20787
20828
|
}
|
|
20788
|
-
|
|
20789
|
-
|
|
20790
|
-
|
|
20791
|
-
|
|
20792
|
-
|
|
20793
|
-
|
|
20794
|
-
|
|
20795
|
-
|
|
20796
|
-
|
|
20797
|
-
|
|
20798
|
-
|
|
20799
|
-
|
|
20800
|
-
|
|
20801
|
-
|
|
20802
|
-
|
|
20803
|
-
|
|
20804
|
-
|
|
20805
|
-
|
|
20806
|
-
return { outcome: { outcome: "success" } };
|
|
20807
|
-
}
|
|
20808
|
-
}
|
|
20809
|
-
const wakeAt = new Date(Date.now() + 5 * 6e4).toISOString();
|
|
20810
|
-
const notifId = `wake-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
20811
|
-
notifs.push({
|
|
20812
|
-
id: notifId,
|
|
20813
|
-
description: waitDesc || lastMsg.slice(0, 200),
|
|
20814
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20815
|
-
wakeAt
|
|
20816
|
-
});
|
|
20829
|
+
} catch (e) {
|
|
20830
|
+
console.warn(`[stop-hook] Failed to inject corrective message: ${e.message}`);
|
|
20831
|
+
}
|
|
20832
|
+
}
|
|
20833
|
+
if (!isWaiting) {
|
|
20834
|
+
return;
|
|
20835
|
+
}
|
|
20836
|
+
const nudgeDir = path18.join(this.workspace, ".nudge");
|
|
20837
|
+
const notifPath = path18.join(nudgeDir, "stop-hook-notifications.json");
|
|
20838
|
+
try {
|
|
20839
|
+
if (!fs17.existsSync(nudgeDir)) fs17.mkdirSync(nudgeDir, { recursive: true });
|
|
20840
|
+
let notifs = [];
|
|
20841
|
+
if (fs17.existsSync(notifPath)) {
|
|
20842
|
+
notifs = JSON.parse(fs17.readFileSync(notifPath, "utf-8"));
|
|
20843
|
+
const now = Date.now();
|
|
20844
|
+
const dup = notifs.find((n) => !n.notified && n.description === (waitDesc || lastMsg.slice(0, 200)));
|
|
20845
|
+
if (dup) {
|
|
20846
|
+
dup.wakeAt = new Date(now + 5 * 6e4).toISOString();
|
|
20817
20847
|
fs17.writeFileSync(notifPath, JSON.stringify(notifs, null, 2));
|
|
20818
|
-
console.log(`[stop-hook]
|
|
20819
|
-
|
|
20820
|
-
|
|
20848
|
+
console.log(`[stop-hook] Duplicate wait (same desc, not fired yet), refreshed wakeAt: ${dup.id}`);
|
|
20849
|
+
return;
|
|
20850
|
+
}
|
|
20851
|
+
const recentReg = notifs.find((n) => now - new Date(n.createdAt).getTime() < 3 * 6e4);
|
|
20852
|
+
if (recentReg) {
|
|
20853
|
+
console.log(`[stop-hook] Skip (recent registration within 3min)`);
|
|
20854
|
+
return;
|
|
20821
20855
|
}
|
|
20822
|
-
return { outcome: { outcome: "success" } };
|
|
20823
20856
|
}
|
|
20824
|
-
|
|
20825
|
-
|
|
20857
|
+
const wakeAt = new Date(Date.now() + 5 * 6e4).toISOString();
|
|
20858
|
+
const notifId = `wake-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
20859
|
+
notifs.push({
|
|
20860
|
+
id: notifId,
|
|
20861
|
+
description: waitDesc || lastMsg.slice(0, 200),
|
|
20862
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20863
|
+
wakeAt
|
|
20864
|
+
});
|
|
20865
|
+
fs17.writeFileSync(notifPath, JSON.stringify(notifs, null, 2));
|
|
20866
|
+
console.log(`[stop-hook] Registered wake-up ${notifId} at ${wakeAt} (sessionId=${sessionId}): ${waitDesc}`);
|
|
20867
|
+
} catch (e) {
|
|
20868
|
+
console.warn(`[stop-hook] Failed to register: ${e.message}`);
|
|
20869
|
+
}
|
|
20826
20870
|
}
|
|
20827
20871
|
stop() {
|
|
20828
20872
|
if (this.timer) {
|
|
@@ -28140,9 +28184,11 @@ ${notifications}
|
|
|
28140
28184
|
const modelChoices = [{ name: "\u{1F504} auto\uFF08\u6062\u590D\u9ED8\u8BA4\uFF09", value: "auto" }];
|
|
28141
28185
|
if (config2.providers) {
|
|
28142
28186
|
for (const [pid, pCfg] of Object.entries(config2.providers)) {
|
|
28187
|
+
if (modelChoices.length >= 25) break;
|
|
28143
28188
|
const models = pCfg.models;
|
|
28144
28189
|
if (!models) continue;
|
|
28145
28190
|
for (const m of models) {
|
|
28191
|
+
if (modelChoices.length >= 25) break;
|
|
28146
28192
|
const ref = `${pid}/${m.id}`;
|
|
28147
28193
|
const alias = aliasByRef.get(ref);
|
|
28148
28194
|
const label = alias ? `${alias} \u2014 ${m.name || m.id}` : `${m.name || m.id} (${pid})`;
|