ework-qq-bridge 0.5.1 → 0.5.2
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/chat.ts +11 -2
- package/src/config.ts +4 -0
- package/src/router.ts +1 -1
- package/tests/bridge.test.ts +50 -0
package/package.json
CHANGED
package/src/chat.ts
CHANGED
|
@@ -76,12 +76,17 @@ export function buildChatMessages(
|
|
|
76
76
|
];
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
export function stripThink(text: string): string {
|
|
80
|
+
return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
79
83
|
export async function chatComplete(
|
|
80
84
|
apiBase: string,
|
|
81
85
|
apiKey: string,
|
|
82
86
|
model: string,
|
|
83
87
|
messages: { role: string; name?: string; content: string }[],
|
|
84
88
|
timeoutMs: number,
|
|
89
|
+
noThink = true,
|
|
85
90
|
): Promise<string> {
|
|
86
91
|
const ctrl = new AbortController();
|
|
87
92
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
@@ -89,14 +94,18 @@ export async function chatComplete(
|
|
|
89
94
|
const res = await fetch(`${apiBase.replace(/\/+$/, "")}/chat/completions`, {
|
|
90
95
|
method: "POST",
|
|
91
96
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
92
|
-
body: JSON.stringify(
|
|
97
|
+
body: JSON.stringify(
|
|
98
|
+
noThink
|
|
99
|
+
? { model, messages, max_tokens: 1024, temperature: 0.4, chat_template_kwargs: { enable_thinking: false } }
|
|
100
|
+
: { model, messages, max_tokens: 1024, temperature: 0.4 },
|
|
101
|
+
),
|
|
93
102
|
signal: ctrl.signal,
|
|
94
103
|
});
|
|
95
104
|
if (!res.ok) {
|
|
96
105
|
throw new Error(`LLM ${res.status}: ${(await res.text()).slice(0, 120)}`);
|
|
97
106
|
}
|
|
98
107
|
const data = (await res.json()) as { choices?: { message?: { content?: string } }[] };
|
|
99
|
-
const text = data.choices?.[0]?.message?.content
|
|
108
|
+
const text = stripThink(data.choices?.[0]?.message?.content ?? "");
|
|
100
109
|
if (!text) throw new Error("LLM returned empty content");
|
|
101
110
|
return text;
|
|
102
111
|
} finally {
|
package/src/config.ts
CHANGED
|
@@ -35,6 +35,10 @@ const Schema = z.object({
|
|
|
35
35
|
// Token budget (heuristic estimate) for one chat request; when history
|
|
36
36
|
// exceeds it the oldest turns are dropped ("满了就清理").
|
|
37
37
|
WORK_CHAT_MAX_CONTEXT: z.coerce.number().int().default(50000),
|
|
38
|
+
// Disable the model's thinking pass for QQ chat replies only (default on:
|
|
39
|
+
// "仅仅这个机器人关掉"); "0" restores thinking. Daemon/opencode sessions
|
|
40
|
+
// are untouched — they use their own runtime.
|
|
41
|
+
WORK_CHAT_NO_THINK: z.preprocess((v) => (v === undefined || v === "" ? "1" : v), z.string()).transform((v) => v !== "0"),
|
|
38
42
|
|
|
39
43
|
// QQ user_ids allowed to dispatch AI work (comma-separated). Messages from
|
|
40
44
|
// other members are logged and ignored — same trust model as the GitHub
|
package/src/router.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function createRouter(deps: RouterDeps) {
|
|
|
56
56
|
const stored = trimStored(chatHistory.get(ev.groupId) ?? [], cfg.WORK_CHAT_MAX_HISTORY, cfg.WORK_CHAT_MAX_CONTEXT);
|
|
57
57
|
chatHistory.set(ev.groupId, stored);
|
|
58
58
|
const messages = buildChatMessages(stored, turn);
|
|
59
|
-
const answer = await chatComplete(cfg.WORK_CHAT_API, cfg.WORK_CHAT_API_KEY, cfg.WORK_CHAT_MODEL, messages, cfg.WORK_CHAT_TIMEOUT_MS);
|
|
59
|
+
const answer = await chatComplete(cfg.WORK_CHAT_API, cfg.WORK_CHAT_API_KEY, cfg.WORK_CHAT_MODEL, messages, cfg.WORK_CHAT_TIMEOUT_MS, cfg.WORK_CHAT_NO_THINK);
|
|
60
60
|
chatHistory.set(ev.groupId, [...stored, turn, { role: "assistant", name: "bot", content: capContent(answer) }]);
|
|
61
61
|
for (const part of splitForQQ(answer)) {
|
|
62
62
|
await deps.reply(ev.groupId, part);
|
package/tests/bridge.test.ts
CHANGED
|
@@ -361,3 +361,53 @@ describe("pure-API chat", () => {
|
|
|
361
361
|
}
|
|
362
362
|
});
|
|
363
363
|
});
|
|
364
|
+
|
|
365
|
+
describe("no-think", () => {
|
|
366
|
+
const { stripThink } = require("../src/chat");
|
|
367
|
+
|
|
368
|
+
test("stripThink removes inline think blocks", () => {
|
|
369
|
+
expect(stripThink("<think>internal</think>答案")).toBe("答案");
|
|
370
|
+
expect(stripThink("<think>a</think>前<think>b</think>后")).toBe("前后");
|
|
371
|
+
expect(stripThink("普通回复")).toBe("普通回复");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("chat request carries enable_thinking:false by default", async () => {
|
|
375
|
+
const origFetch = globalThis.fetch;
|
|
376
|
+
let sent: Record<string, unknown> = {};
|
|
377
|
+
globalThis.fetch = (async (_u: unknown, init?: { body?: string }) => {
|
|
378
|
+
sent = JSON.parse(String(init?.body));
|
|
379
|
+
return new Response(JSON.stringify({ choices: [{ message: { content: "ok" } }] }), { status: 200 });
|
|
380
|
+
}) as typeof fetch;
|
|
381
|
+
try {
|
|
382
|
+
const { chatComplete } = require("../src/chat");
|
|
383
|
+
const out = await chatComplete("http://x/v1", "k", "m", [{ role: "user", content: "q" }], 1000);
|
|
384
|
+
expect(out).toBe("ok");
|
|
385
|
+
expect((sent.chat_template_kwargs as { enable_thinking?: boolean })?.enable_thinking).toBe(false);
|
|
386
|
+
await chatComplete("http://x/v1", "k", "m", [{ role: "user", content: "q" }], 1000, false);
|
|
387
|
+
expect(sent.chat_template_kwargs).toBeUndefined();
|
|
388
|
+
} finally {
|
|
389
|
+
globalThis.fetch = origFetch;
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("thinking payload is stripped before replying", async () => {
|
|
394
|
+
const origFetch = globalThis.fetch;
|
|
395
|
+
globalThis.fetch = (async () => new Response(JSON.stringify({ choices: [{ message: { content: "<think>隐藏推理</think>可见答案" } }] }), { status: 200 })) as typeof fetch;
|
|
396
|
+
try {
|
|
397
|
+
const { createRouter } = require("../src/router");
|
|
398
|
+
const replies: string[] = [];
|
|
399
|
+
const router = createRouter({
|
|
400
|
+
cfg: { VERBOSE: false, WORK_CHAT_API: "http://x/v1", WORK_CHAT_API_KEY: "k", WORK_CHAT_MODEL: "m", WORK_CHAT_TIMEOUT_MS: 1000, WORK_CHAT_MAX_HISTORY: 20, WORK_CHAT_MAX_CONTEXT: 50000, WORK_CHAT_NO_THINK: true },
|
|
401
|
+
bindings: new BindingStore([{ groupId: 1, owner: "o", repo: "r" }], pinFile()),
|
|
402
|
+
wakeList: new Set(["1"]),
|
|
403
|
+
ework: { createIssue: async () => 9, addComment: async () => {} },
|
|
404
|
+
store: { seenPost: () => false },
|
|
405
|
+
reply: async (_g: number, x: string) => { replies.push(x); },
|
|
406
|
+
});
|
|
407
|
+
await router.handleGroupMessage({ groupId: 1, userId: 1, nickname: "u", postId: "nt1", rawMessage: "[CQ:at,qq=2661222094] 问" });
|
|
408
|
+
expect(replies).toEqual(["可见答案"]);
|
|
409
|
+
} finally {
|
|
410
|
+
globalThis.fetch = origFetch;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
});
|