cue-console 0.1.17 → 0.1.19
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/src/components/chat-composer.tsx +103 -1
- package/src/components/chat-view.tsx +116 -0
- package/src/components/conversation-list.tsx +47 -2
- package/src/contexts/config-context.tsx +22 -9
- package/src/lib/actions.ts +79 -28
- package/src/lib/db.ts +19 -0
- package/src/lib/user-config.ts +32 -0
package/package.json
CHANGED
|
@@ -12,9 +12,15 @@ import {
|
|
|
12
12
|
type SetStateAction,
|
|
13
13
|
} from "react";
|
|
14
14
|
import { Button } from "@/components/ui/button";
|
|
15
|
+
import {
|
|
16
|
+
Dialog,
|
|
17
|
+
DialogContent,
|
|
18
|
+
DialogHeader,
|
|
19
|
+
DialogTitle,
|
|
20
|
+
} from "@/components/ui/dialog";
|
|
15
21
|
import { cn, getAgentEmoji } from "@/lib/utils";
|
|
16
22
|
import { setAgentDisplayName } from "@/lib/actions";
|
|
17
|
-
import { CornerUpLeft, GripVertical, Plus, Send, Trash2, X } from "lucide-react";
|
|
23
|
+
import { Bot, CornerUpLeft, GripVertical, Plus, Send, Trash2, X } from "lucide-react";
|
|
18
24
|
|
|
19
25
|
type MentionDraft = {
|
|
20
26
|
userId: string;
|
|
@@ -71,6 +77,8 @@ export function ChatComposer({
|
|
|
71
77
|
setImages,
|
|
72
78
|
setNotice,
|
|
73
79
|
setPreviewImage,
|
|
80
|
+
botEnabled,
|
|
81
|
+
onToggleBot,
|
|
74
82
|
handleSend,
|
|
75
83
|
enqueueCurrent,
|
|
76
84
|
queue,
|
|
@@ -113,6 +121,8 @@ export function ChatComposer({
|
|
|
113
121
|
setImages: Dispatch<SetStateAction<{ mime_type: string; base64_data: string; file_name?: string }[]>>;
|
|
114
122
|
setNotice: Dispatch<SetStateAction<string | null>>;
|
|
115
123
|
setPreviewImage: Dispatch<SetStateAction<{ mime_type: string; base64_data: string } | null>>;
|
|
124
|
+
botEnabled: boolean;
|
|
125
|
+
onToggleBot: () => Promise<boolean>;
|
|
116
126
|
handleSend: () => void | Promise<void>;
|
|
117
127
|
enqueueCurrent: () => void;
|
|
118
128
|
queue: QueuedMessage[];
|
|
@@ -152,6 +162,8 @@ export function ChatComposer({
|
|
|
152
162
|
}, [onBack]);
|
|
153
163
|
|
|
154
164
|
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
|
165
|
+
const [botToggling, setBotToggling] = useState(false);
|
|
166
|
+
const [botConfirmOpen, setBotConfirmOpen] = useState(false);
|
|
155
167
|
const isComposingRef = useRef(false);
|
|
156
168
|
|
|
157
169
|
const submitOrQueue = () => {
|
|
@@ -251,6 +263,54 @@ export function ChatComposer({
|
|
|
251
263
|
</div>
|
|
252
264
|
)}
|
|
253
265
|
|
|
266
|
+
<Dialog open={botConfirmOpen} onOpenChange={setBotConfirmOpen}>
|
|
267
|
+
<DialogContent className="sm:max-w-110">
|
|
268
|
+
<DialogHeader>
|
|
269
|
+
<DialogTitle>Enable bot mode?</DialogTitle>
|
|
270
|
+
</DialogHeader>
|
|
271
|
+
|
|
272
|
+
<div className="text-sm text-muted-foreground space-y-3">
|
|
273
|
+
<p>
|
|
274
|
+
Bot mode will automatically reply to <span className="text-foreground font-medium">cue</span> requests
|
|
275
|
+
in this conversation.
|
|
276
|
+
</p>
|
|
277
|
+
<ul className="list-disc pl-5 space-y-1">
|
|
278
|
+
<li>Only affects the current {type === "group" ? "group" : "agent"} conversation.</li>
|
|
279
|
+
<li>May immediately reply to currently pending cue requests.</li>
|
|
280
|
+
<li>Does not reply to pause confirmations.</li>
|
|
281
|
+
<li>You can turn it off anytime.</li>
|
|
282
|
+
</ul>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<div className="mt-4 flex items-center justify-end gap-2">
|
|
286
|
+
<Button
|
|
287
|
+
type="button"
|
|
288
|
+
variant="outline"
|
|
289
|
+
disabled={botToggling}
|
|
290
|
+
onClick={() => setBotConfirmOpen(false)}
|
|
291
|
+
>
|
|
292
|
+
Cancel
|
|
293
|
+
</Button>
|
|
294
|
+
<Button
|
|
295
|
+
type="button"
|
|
296
|
+
disabled={botToggling}
|
|
297
|
+
onClick={async () => {
|
|
298
|
+
if (botToggling) return;
|
|
299
|
+
setBotToggling(true);
|
|
300
|
+
try {
|
|
301
|
+
await onToggleBot();
|
|
302
|
+
setBotConfirmOpen(false);
|
|
303
|
+
} finally {
|
|
304
|
+
setBotToggling(false);
|
|
305
|
+
}
|
|
306
|
+
}}
|
|
307
|
+
>
|
|
308
|
+
{botToggling ? "Enabling…" : "Enable"}
|
|
309
|
+
</Button>
|
|
310
|
+
</div>
|
|
311
|
+
</DialogContent>
|
|
312
|
+
</Dialog>
|
|
313
|
+
|
|
254
314
|
{/* Image Preview */}
|
|
255
315
|
{images.length > 0 && (
|
|
256
316
|
<div className="flex max-w-full gap-2 overflow-x-auto px-0.5 pt-0.5">
|
|
@@ -614,6 +674,48 @@ export function ChatComposer({
|
|
|
614
674
|
>
|
|
615
675
|
Queue
|
|
616
676
|
</Button>
|
|
677
|
+
|
|
678
|
+
<div className="relative group">
|
|
679
|
+
<Button
|
|
680
|
+
type="button"
|
|
681
|
+
variant="ghost"
|
|
682
|
+
size="icon"
|
|
683
|
+
disabled={busy || botToggling}
|
|
684
|
+
className={cn(
|
|
685
|
+
"relative h-9 w-9 rounded-2xl",
|
|
686
|
+
"hover:bg-white/40",
|
|
687
|
+
botEnabled ? "text-primary" : "text-muted-foreground",
|
|
688
|
+
(busy || botToggling) && "opacity-60 cursor-not-allowed"
|
|
689
|
+
)}
|
|
690
|
+
onClick={async () => {
|
|
691
|
+
if (busy || botToggling) return;
|
|
692
|
+
if (!botEnabled) {
|
|
693
|
+
setBotConfirmOpen(true);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
setBotToggling(true);
|
|
697
|
+
try {
|
|
698
|
+
await onToggleBot();
|
|
699
|
+
} finally {
|
|
700
|
+
setBotToggling(false);
|
|
701
|
+
}
|
|
702
|
+
}}
|
|
703
|
+
aria-label={botEnabled ? "Stop bot" : "Start bot"}
|
|
704
|
+
title={botToggling ? "Turning…" : botEnabled ? "Stop bot" : "Start bot"}
|
|
705
|
+
>
|
|
706
|
+
{botEnabled && (
|
|
707
|
+
<span className="pointer-events-none absolute inset-0 rounded-xl">
|
|
708
|
+
<span className="absolute inset-0 rounded-2xl bg-primary/15 blur-md animate-pulse" />
|
|
709
|
+
</span>
|
|
710
|
+
)}
|
|
711
|
+
<Bot
|
|
712
|
+
className={cn(
|
|
713
|
+
"relative z-10 h-5 w-5",
|
|
714
|
+
botEnabled && "drop-shadow-[0_0_12px_rgba(99,102,241,0.45)]"
|
|
715
|
+
)}
|
|
716
|
+
/>
|
|
717
|
+
</Button>
|
|
718
|
+
</div>
|
|
617
719
|
</div>
|
|
618
720
|
|
|
619
721
|
<Button
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
submitResponse,
|
|
26
26
|
cancelRequest,
|
|
27
27
|
batchRespond,
|
|
28
|
+
processBotTick,
|
|
28
29
|
type CueRequest,
|
|
29
30
|
} from "@/lib/actions";
|
|
30
31
|
import { ChatComposer } from "@/components/chat-composer";
|
|
@@ -77,6 +78,12 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
77
78
|
const deferredInput = useDeferredValue(input);
|
|
78
79
|
const imagesRef = useRef(images);
|
|
79
80
|
|
|
81
|
+
const botStorageKey = useMemo(() => {
|
|
82
|
+
return `cue-console:botEnabled:${type}:${id}`;
|
|
83
|
+
}, [type, id]);
|
|
84
|
+
|
|
85
|
+
const [botEnabled, setBotEnabled] = useState(false);
|
|
86
|
+
|
|
80
87
|
const { soundEnabled, setSoundEnabled, playDing } = useAudioNotification();
|
|
81
88
|
|
|
82
89
|
const {
|
|
@@ -103,6 +110,17 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
103
110
|
|
|
104
111
|
const [composerPadPx, setComposerPadPx] = useState(36 * 4);
|
|
105
112
|
|
|
113
|
+
const botHolderIdRef = useRef<string>(
|
|
114
|
+
(() => {
|
|
115
|
+
try {
|
|
116
|
+
return globalThis.crypto?.randomUUID?.() || `bot-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
117
|
+
} catch {
|
|
118
|
+
return `bot-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
119
|
+
}
|
|
120
|
+
})()
|
|
121
|
+
);
|
|
122
|
+
const botTickBusyRef = useRef(false);
|
|
123
|
+
|
|
106
124
|
const nextCursorRef = useRef<string | null>(null);
|
|
107
125
|
const loadingMoreRef = useRef(false);
|
|
108
126
|
|
|
@@ -207,6 +225,102 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
207
225
|
setError,
|
|
208
226
|
});
|
|
209
227
|
|
|
228
|
+
const triggerBotTickOnce = useCallback(async () => {
|
|
229
|
+
if (document.visibilityState !== "visible") return;
|
|
230
|
+
if (botTickBusyRef.current) return;
|
|
231
|
+
botTickBusyRef.current = true;
|
|
232
|
+
try {
|
|
233
|
+
const res = await processBotTick({
|
|
234
|
+
holderId: botHolderIdRef.current,
|
|
235
|
+
convType: type,
|
|
236
|
+
convId: id,
|
|
237
|
+
limit: 80,
|
|
238
|
+
});
|
|
239
|
+
if (!res.success) {
|
|
240
|
+
setNotice(`Bot tick failed: ${res.error}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (!res.acquired) {
|
|
244
|
+
setNotice("Bot is busy in another window");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (res.replied === 0) {
|
|
248
|
+
setNotice("Bot is enabled (no pending to reply)");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (res.replied > 0) {
|
|
252
|
+
await refreshLatest();
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
} finally {
|
|
256
|
+
botTickBusyRef.current = false;
|
|
257
|
+
}
|
|
258
|
+
}, [id, refreshLatest, setNotice, type]);
|
|
259
|
+
|
|
260
|
+
const toggleBot = useCallback(async (): Promise<boolean> => {
|
|
261
|
+
const prev = botEnabled;
|
|
262
|
+
const next = !prev;
|
|
263
|
+
setBotEnabled(next);
|
|
264
|
+
try {
|
|
265
|
+
window.localStorage.setItem(botStorageKey, next ? "1" : "0");
|
|
266
|
+
if (next) void triggerBotTickOnce();
|
|
267
|
+
return next;
|
|
268
|
+
} catch {
|
|
269
|
+
setBotEnabled(prev);
|
|
270
|
+
setNotice("Failed to toggle bot");
|
|
271
|
+
return prev;
|
|
272
|
+
}
|
|
273
|
+
}, [botEnabled, botStorageKey, setNotice, triggerBotTickOnce]);
|
|
274
|
+
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
try {
|
|
277
|
+
const raw = window.localStorage.getItem(botStorageKey);
|
|
278
|
+
setBotEnabled(raw === "1");
|
|
279
|
+
} catch {
|
|
280
|
+
setBotEnabled(false);
|
|
281
|
+
}
|
|
282
|
+
}, [botStorageKey]);
|
|
283
|
+
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (!botEnabled) return;
|
|
286
|
+
|
|
287
|
+
let cancelled = false;
|
|
288
|
+
|
|
289
|
+
const tick = async () => {
|
|
290
|
+
if (cancelled) return;
|
|
291
|
+
if (document.visibilityState !== "visible") return;
|
|
292
|
+
if (botTickBusyRef.current) return;
|
|
293
|
+
botTickBusyRef.current = true;
|
|
294
|
+
try {
|
|
295
|
+
const res = await processBotTick({
|
|
296
|
+
holderId: botHolderIdRef.current,
|
|
297
|
+
convType: type,
|
|
298
|
+
convId: id,
|
|
299
|
+
limit: 80,
|
|
300
|
+
});
|
|
301
|
+
if (cancelled) return;
|
|
302
|
+
if (!res.success) {
|
|
303
|
+
setNotice(`Bot tick failed: ${res.error}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!res.acquired) return;
|
|
307
|
+
if (res.replied > 0) {
|
|
308
|
+
await refreshLatest();
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
} finally {
|
|
312
|
+
botTickBusyRef.current = false;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
void tick();
|
|
317
|
+
const interval = setInterval(() => void tick(), 2500);
|
|
318
|
+
return () => {
|
|
319
|
+
cancelled = true;
|
|
320
|
+
clearInterval(interval);
|
|
321
|
+
};
|
|
322
|
+
}, [botEnabled, id, refreshLatest, setNotice, type]);
|
|
323
|
+
|
|
210
324
|
|
|
211
325
|
const handleTitleChange = async (newTitle: string) => {
|
|
212
326
|
if (type === "agent") {
|
|
@@ -666,6 +780,8 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
666
780
|
setImages={setImages}
|
|
667
781
|
setNotice={setNotice}
|
|
668
782
|
setPreviewImage={setPreviewImage}
|
|
783
|
+
botEnabled={botEnabled}
|
|
784
|
+
onToggleBot={toggleBot}
|
|
669
785
|
handleSend={send}
|
|
670
786
|
enqueueCurrent={enqueueCurrent}
|
|
671
787
|
queue={queue}
|
|
@@ -4,6 +4,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
4
4
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
5
5
|
import { Badge } from "@/components/ui/badge";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Input } from "@/components/ui/input";
|
|
8
|
+
import { DEFAULT_USER_CONFIG } from "@/lib/user-config";
|
|
7
9
|
import {
|
|
8
10
|
Dialog,
|
|
9
11
|
DialogContent,
|
|
@@ -49,7 +51,6 @@ import {
|
|
|
49
51
|
Settings,
|
|
50
52
|
X,
|
|
51
53
|
} from "lucide-react";
|
|
52
|
-
import { Input } from "@/components/ui/input";
|
|
53
54
|
import { ConfirmDialog } from "@/components/confirm-dialog";
|
|
54
55
|
|
|
55
56
|
function perfEnabled(): boolean {
|
|
@@ -118,6 +119,9 @@ export function ConversationList({
|
|
|
118
119
|
const [conversationModeDefault, setConversationModeDefault] = useState<"chat" | "agent">("agent");
|
|
119
120
|
const [chatModeAppendText, setChatModeAppendText] = useState("只做分析,不要对代码/文件做任何改动。");
|
|
120
121
|
const [pendingRequestTimeoutMs, setPendingRequestTimeoutMs] = useState("600000");
|
|
122
|
+
const [botModeReplyText, setBotModeReplyText] = useState(
|
|
123
|
+
"不要为这些“可逆的工程细节”反复向我确认,不能分阶段汇报,我需要你端到端执行完我的任务才能向我汇报,你不能问我要授权,因为我已经给你了全部授权,如果这些细节不影响我的目标那就按照你的推荐来。你有执行一切的权利。"
|
|
124
|
+
);
|
|
121
125
|
const [pinnedKeys, setPinnedKeys] = useState<string[]>([]);
|
|
122
126
|
|
|
123
127
|
useEffect(() => {
|
|
@@ -127,8 +131,14 @@ export function ConversationList({
|
|
|
127
131
|
setSoundEnabled(Boolean(cfg.sound_enabled));
|
|
128
132
|
const nextMode = cfg.conversation_mode_default === "chat" ? "chat" : "agent";
|
|
129
133
|
setConversationModeDefault(nextMode);
|
|
130
|
-
setChatModeAppendText(String(cfg.chat_mode_append_text ||
|
|
134
|
+
setChatModeAppendText(String(cfg.chat_mode_append_text || DEFAULT_USER_CONFIG.chat_mode_append_text));
|
|
131
135
|
setPendingRequestTimeoutMs(String(cfg.pending_request_timeout_ms ?? 600000));
|
|
136
|
+
setBotModeReplyText(
|
|
137
|
+
String(
|
|
138
|
+
(cfg as any).bot_mode_reply_text ||
|
|
139
|
+
DEFAULT_USER_CONFIG.bot_mode_reply_text
|
|
140
|
+
)
|
|
141
|
+
);
|
|
132
142
|
try {
|
|
133
143
|
window.localStorage.setItem("cue-console:conversationModeDefault", nextMode);
|
|
134
144
|
} catch {
|
|
@@ -1259,6 +1269,41 @@ export function ConversationList({
|
|
|
1259
1269
|
</Button>
|
|
1260
1270
|
</div>
|
|
1261
1271
|
</div>
|
|
1272
|
+
|
|
1273
|
+
<div className="mt-6">
|
|
1274
|
+
<div className="text-sm font-medium">Bot reply text</div>
|
|
1275
|
+
<div className="text-xs text-muted-foreground">
|
|
1276
|
+
Multi-line
|
|
1277
|
+
</div>
|
|
1278
|
+
<div className="mt-2 flex items-start justify-end gap-2">
|
|
1279
|
+
<textarea
|
|
1280
|
+
value={botModeReplyText}
|
|
1281
|
+
onChange={(e) => setBotModeReplyText(e.target.value)}
|
|
1282
|
+
placeholder="Bot reply"
|
|
1283
|
+
className="min-h-24 flex-1 min-w-0 resize-y rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
1284
|
+
/>
|
|
1285
|
+
<Button
|
|
1286
|
+
type="button"
|
|
1287
|
+
variant="secondary"
|
|
1288
|
+
size="sm"
|
|
1289
|
+
className="h-9 rounded-md px-3 text-xs"
|
|
1290
|
+
onClick={async () => {
|
|
1291
|
+
const next = botModeReplyText;
|
|
1292
|
+
try {
|
|
1293
|
+
await setUserConfig({ bot_mode_reply_text: next });
|
|
1294
|
+
} catch {
|
|
1295
|
+
}
|
|
1296
|
+
window.dispatchEvent(
|
|
1297
|
+
new CustomEvent("cue-console:configUpdated", {
|
|
1298
|
+
detail: { bot_mode_reply_text: next },
|
|
1299
|
+
})
|
|
1300
|
+
);
|
|
1301
|
+
}}
|
|
1302
|
+
>
|
|
1303
|
+
Save
|
|
1304
|
+
</Button>
|
|
1305
|
+
</div>
|
|
1306
|
+
</div>
|
|
1262
1307
|
</DialogContent>
|
|
1263
1308
|
</Dialog>
|
|
1264
1309
|
|
|
@@ -2,13 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
|
4
4
|
import { getUserConfig, type UserConfig } from "@/lib/actions";
|
|
5
|
-
|
|
6
|
-
const defaultConfig: UserConfig = {
|
|
7
|
-
sound_enabled: true,
|
|
8
|
-
conversation_mode_default: "agent",
|
|
9
|
-
chat_mode_append_text: "只做分析,不要对代码/文件做任何改动。",
|
|
10
|
-
pending_request_timeout_ms: 10 * 60 * 1000,
|
|
11
|
-
};
|
|
5
|
+
import { DEFAULT_USER_CONFIG } from "@/lib/user-config";
|
|
12
6
|
|
|
13
7
|
type ConfigContextValue = {
|
|
14
8
|
config: UserConfig;
|
|
@@ -25,7 +19,7 @@ export function useConfig() {
|
|
|
25
19
|
}
|
|
26
20
|
|
|
27
21
|
export function ConfigProvider({ children }: { children: ReactNode }) {
|
|
28
|
-
const [config, setConfig] = useState<UserConfig>(
|
|
22
|
+
const [config, setConfig] = useState<UserConfig>(DEFAULT_USER_CONFIG);
|
|
29
23
|
|
|
30
24
|
useEffect(() => {
|
|
31
25
|
let cancelled = false;
|
|
@@ -69,7 +63,26 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
|
|
69
63
|
);
|
|
70
64
|
} catch {
|
|
71
65
|
}
|
|
72
|
-
|
|
66
|
+
try {
|
|
67
|
+
window.localStorage.setItem(
|
|
68
|
+
"cue-console:bot_mode_enabled",
|
|
69
|
+
config.bot_mode_enabled ? "1" : "0"
|
|
70
|
+
);
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
window.localStorage.setItem(
|
|
75
|
+
"cue-console:bot_mode_reply_text",
|
|
76
|
+
String(config.bot_mode_reply_text || "")
|
|
77
|
+
);
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}, [
|
|
81
|
+
config.pending_request_timeout_ms,
|
|
82
|
+
config.chat_mode_append_text,
|
|
83
|
+
config.bot_mode_enabled,
|
|
84
|
+
config.bot_mode_reply_text,
|
|
85
|
+
]);
|
|
73
86
|
|
|
74
87
|
const value = useMemo<ConfigContextValue>(() => ({ config }), [config]);
|
|
75
88
|
|
package/src/lib/actions.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
moveMessageQueueItem,
|
|
41
41
|
acquireWorkerLease,
|
|
42
42
|
processMessageQueueTick,
|
|
43
|
+
getAgentPendingRequests,
|
|
43
44
|
getLastRequestsByAgents,
|
|
44
45
|
getLastResponsesByAgents,
|
|
45
46
|
getPendingCountsByAgents,
|
|
@@ -56,12 +57,15 @@ export type { Group, UserResponse, ImageContent, ConversationItem } from "./type
|
|
|
56
57
|
export type { CueRequest, CueResponse, AgentTimelineItem } from "./db";
|
|
57
58
|
import { v4 as uuidv4 } from "uuid";
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
import {
|
|
61
|
+
DEFAULT_USER_CONFIG,
|
|
62
|
+
clampNumber,
|
|
63
|
+
normalizeMultiline,
|
|
64
|
+
normalizeSingleLine,
|
|
65
|
+
type UserConfig,
|
|
66
|
+
} from "./user-config";
|
|
67
|
+
|
|
68
|
+
export type { UserConfig } from "./user-config";
|
|
65
69
|
|
|
66
70
|
export type QueuedMessage = {
|
|
67
71
|
id: string;
|
|
@@ -70,23 +74,6 @@ export type QueuedMessage = {
|
|
|
70
74
|
createdAt: number;
|
|
71
75
|
};
|
|
72
76
|
|
|
73
|
-
const defaultUserConfig: UserConfig = {
|
|
74
|
-
sound_enabled: true,
|
|
75
|
-
conversation_mode_default: "agent",
|
|
76
|
-
chat_mode_append_text: "只做分析,不要对代码/文件做任何改动。",
|
|
77
|
-
pending_request_timeout_ms: 10 * 60 * 1000,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
function clampNumber(n: number, min: number, max: number): number {
|
|
81
|
-
if (!Number.isFinite(n)) return min;
|
|
82
|
-
return Math.max(min, Math.min(max, n));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function normalizeSingleLine(s: string): string {
|
|
86
|
-
const t = String(s ?? "").replace(/\r?\n/g, " ").trim();
|
|
87
|
-
return t;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
77
|
function getUserConfigPath(): string {
|
|
91
78
|
return path.join(os.homedir(), ".cue", "config.json");
|
|
92
79
|
}
|
|
@@ -100,24 +87,34 @@ export async function getUserConfig(): Promise<UserConfig> {
|
|
|
100
87
|
sound_enabled:
|
|
101
88
|
typeof parsed.sound_enabled === "boolean"
|
|
102
89
|
? parsed.sound_enabled
|
|
103
|
-
:
|
|
90
|
+
: DEFAULT_USER_CONFIG.sound_enabled,
|
|
104
91
|
conversation_mode_default:
|
|
105
92
|
parsed.conversation_mode_default === "chat" || parsed.conversation_mode_default === "agent"
|
|
106
93
|
? parsed.conversation_mode_default
|
|
107
|
-
:
|
|
94
|
+
: DEFAULT_USER_CONFIG.conversation_mode_default,
|
|
108
95
|
|
|
109
96
|
chat_mode_append_text:
|
|
110
97
|
typeof parsed.chat_mode_append_text === "string" && normalizeSingleLine(parsed.chat_mode_append_text).length > 0
|
|
111
98
|
? normalizeSingleLine(parsed.chat_mode_append_text)
|
|
112
|
-
:
|
|
99
|
+
: DEFAULT_USER_CONFIG.chat_mode_append_text,
|
|
113
100
|
|
|
114
101
|
pending_request_timeout_ms:
|
|
115
102
|
typeof parsed.pending_request_timeout_ms === "number"
|
|
116
103
|
? clampNumber(parsed.pending_request_timeout_ms, 60_000, 86_400_000)
|
|
117
|
-
:
|
|
104
|
+
: DEFAULT_USER_CONFIG.pending_request_timeout_ms,
|
|
105
|
+
|
|
106
|
+
bot_mode_enabled:
|
|
107
|
+
typeof parsed.bot_mode_enabled === "boolean"
|
|
108
|
+
? parsed.bot_mode_enabled
|
|
109
|
+
: DEFAULT_USER_CONFIG.bot_mode_enabled,
|
|
110
|
+
|
|
111
|
+
bot_mode_reply_text:
|
|
112
|
+
typeof parsed.bot_mode_reply_text === "string" && normalizeMultiline(parsed.bot_mode_reply_text).length > 0
|
|
113
|
+
? normalizeMultiline(parsed.bot_mode_reply_text)
|
|
114
|
+
: DEFAULT_USER_CONFIG.bot_mode_reply_text,
|
|
118
115
|
};
|
|
119
116
|
} catch {
|
|
120
|
-
return
|
|
117
|
+
return DEFAULT_USER_CONFIG;
|
|
121
118
|
}
|
|
122
119
|
}
|
|
123
120
|
|
|
@@ -140,6 +137,14 @@ export async function setUserConfig(next: Partial<UserConfig>): Promise<UserConf
|
|
|
140
137
|
typeof next.pending_request_timeout_ms === "number"
|
|
141
138
|
? clampNumber(next.pending_request_timeout_ms, 60_000, 86_400_000)
|
|
142
139
|
: prev.pending_request_timeout_ms,
|
|
140
|
+
|
|
141
|
+
bot_mode_enabled:
|
|
142
|
+
typeof next.bot_mode_enabled === "boolean" ? next.bot_mode_enabled : prev.bot_mode_enabled,
|
|
143
|
+
|
|
144
|
+
bot_mode_reply_text:
|
|
145
|
+
typeof next.bot_mode_reply_text === "string" && normalizeMultiline(next.bot_mode_reply_text).length > 0
|
|
146
|
+
? normalizeMultiline(next.bot_mode_reply_text)
|
|
147
|
+
: prev.bot_mode_reply_text,
|
|
143
148
|
};
|
|
144
149
|
const p = getUserConfigPath();
|
|
145
150
|
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
@@ -147,6 +152,52 @@ export async function setUserConfig(next: Partial<UserConfig>): Promise<UserConf
|
|
|
147
152
|
return merged;
|
|
148
153
|
}
|
|
149
154
|
|
|
155
|
+
export async function processBotTick(args: {
|
|
156
|
+
holderId: string;
|
|
157
|
+
convType: ConversationType;
|
|
158
|
+
convId: string;
|
|
159
|
+
limit?: number;
|
|
160
|
+
}): Promise<{ success: true; acquired: boolean; replied: number } | { success: false; error: string }> {
|
|
161
|
+
try {
|
|
162
|
+
const holderId = String(args.holderId || "").trim();
|
|
163
|
+
if (!holderId) return { success: false, error: "holderId required" } as const;
|
|
164
|
+
|
|
165
|
+
const convType = args.convType === "group" ? "group" : "agent";
|
|
166
|
+
const convId = String(args.convId || "").trim();
|
|
167
|
+
if (!convId) return { success: false, error: "convId required" } as const;
|
|
168
|
+
|
|
169
|
+
const cfg = await getUserConfig();
|
|
170
|
+
|
|
171
|
+
const lease = acquireWorkerLease({
|
|
172
|
+
leaseKey: `cue-console:bot-mode:${convType}:${convId}`,
|
|
173
|
+
holderId,
|
|
174
|
+
ttlMs: 5_000,
|
|
175
|
+
});
|
|
176
|
+
if (!lease.acquired) return { success: true, acquired: false, replied: 0 } as const;
|
|
177
|
+
|
|
178
|
+
const limit = Math.max(1, Math.min(200, args.limit ?? 50));
|
|
179
|
+
const pending =
|
|
180
|
+
convType === "agent"
|
|
181
|
+
? getAgentPendingRequests(convId, limit)
|
|
182
|
+
: getGroupPendingRequests(convId).slice(0, limit);
|
|
183
|
+
|
|
184
|
+
let replied = 0;
|
|
185
|
+
const text = cfg.bot_mode_reply_text;
|
|
186
|
+
for (const r of pending) {
|
|
187
|
+
try {
|
|
188
|
+
sendResponse(String(r.request_id), { text }, false);
|
|
189
|
+
replied += 1;
|
|
190
|
+
} catch {
|
|
191
|
+
// ignore per-request failures
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { success: true, acquired: true, replied } as const;
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return { success: false, error: e instanceof Error ? e.message : String(e) } as const;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
150
201
|
export async function fetchAgentDisplayNames(agentIds: string[]) {
|
|
151
202
|
return getAgentDisplayNames(agentIds);
|
|
152
203
|
}
|
package/src/lib/db.ts
CHANGED
|
@@ -922,6 +922,25 @@ export function getPendingRequests(): CueRequest[] {
|
|
|
922
922
|
.all() as CueRequest[];
|
|
923
923
|
}
|
|
924
924
|
|
|
925
|
+
export function getAgentPendingRequests(agentId: string, limit: number = 200): CueRequest[] {
|
|
926
|
+
const cleanAgentId = String(agentId || "").trim();
|
|
927
|
+
if (!cleanAgentId) return [];
|
|
928
|
+
const lim = Math.max(1, Math.min(500, Math.floor(Number(limit) || 0)));
|
|
929
|
+
return getDb()
|
|
930
|
+
.prepare(
|
|
931
|
+
`SELECT * FROM cue_requests
|
|
932
|
+
WHERE agent_id = ?
|
|
933
|
+
AND status = 'PENDING'
|
|
934
|
+
AND NOT (
|
|
935
|
+
COALESCE(payload, '') LIKE '%"type"%confirm%'
|
|
936
|
+
AND COALESCE(payload, '') LIKE '%"variant"%pause%'
|
|
937
|
+
)
|
|
938
|
+
ORDER BY created_at ASC
|
|
939
|
+
LIMIT ?`
|
|
940
|
+
)
|
|
941
|
+
.all(cleanAgentId, lim) as CueRequest[];
|
|
942
|
+
}
|
|
943
|
+
|
|
925
944
|
export function getRequestsByAgent(agentId: string): CueRequest[] {
|
|
926
945
|
return getDb()
|
|
927
946
|
.prepare(
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type UserConfig = {
|
|
2
|
+
sound_enabled: boolean;
|
|
3
|
+
conversation_mode_default: "chat" | "agent";
|
|
4
|
+
chat_mode_append_text: string;
|
|
5
|
+
pending_request_timeout_ms: number;
|
|
6
|
+
bot_mode_enabled: boolean;
|
|
7
|
+
bot_mode_reply_text: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_USER_CONFIG: UserConfig = {
|
|
11
|
+
sound_enabled: true,
|
|
12
|
+
conversation_mode_default: "agent",
|
|
13
|
+
chat_mode_append_text: "只做分析,不要对代码/文件做任何改动。",
|
|
14
|
+
pending_request_timeout_ms: 10 * 60 * 1000,
|
|
15
|
+
bot_mode_enabled: false,
|
|
16
|
+
bot_mode_reply_text:
|
|
17
|
+
"不要为“可逆的工程细节”反复向我确认,不能分阶段汇报,我需要你端到端执行完我的任务才能向我汇报,你不能问我要授权,因为我已经给你了除了 push以外的全部授权,,如果这些细节不影响我的目标那就按照你的推荐来。你有执行一切的能力。最后,如果你执行完我的任务请使用 cue pause 来回复我.",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function clampNumber(n: number, min: number, max: number): number {
|
|
21
|
+
if (!Number.isFinite(n)) return min;
|
|
22
|
+
return Math.max(min, Math.min(max, n));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeSingleLine(s: string): string {
|
|
26
|
+
const t = String(s ?? "").replace(/\r?\n/g, " ").trim();
|
|
27
|
+
return t;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeMultiline(s: string): string {
|
|
31
|
+
return String(s ?? "").replace(/\r\n/g, "\n").trim();
|
|
32
|
+
}
|