koishi-plugin-image-prompt 2.0.1 → 2.0.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/lib/index.d.ts +6 -0
- package/lib/index.js +58 -33
- package/package.json +28 -28
- package/readme.md +25 -0
- package/src/index.ts +80 -37
package/lib/index.d.ts
CHANGED
|
@@ -18,6 +18,12 @@ export declare const usage = "\n---\n\n\u6B64\u63D2\u4EF6\u76F4\u63A5\u8C03\u752
|
|
|
18
18
|
* @param attempt 第几次重试(从 0 开始)
|
|
19
19
|
*/
|
|
20
20
|
export declare function computeRetryDelay(status: any, retryAfter: number | undefined, baseInterval: number, attempt: number): number;
|
|
21
|
+
/**
|
|
22
|
+
* 从接口抛出的错误里挖出「到底为什么失败」。
|
|
23
|
+
* koishi 的 http 错误对 5xx 往往只留一句状态文本("Internal Server Error"),
|
|
24
|
+
* 真正的病根(比如上游 ECONNREFUSED、模型负载过高)藏在响应体里,得自己捞出来。
|
|
25
|
+
*/
|
|
26
|
+
export declare function httpErrorText(error: any): string;
|
|
21
27
|
/** 从任意文本里挖出第一个 JSON 对象(容忍代码块围栏和前后废话) */
|
|
22
28
|
export declare function extractJsonObject(raw: string | null | undefined): any | null;
|
|
23
29
|
/**
|
package/lib/index.js
CHANGED
|
@@ -46,6 +46,7 @@ __export(src_exports, {
|
|
|
46
46
|
extractServerMessage: () => extractServerMessage,
|
|
47
47
|
extractTextToRender: () => extractTextToRender,
|
|
48
48
|
fitImageSize: () => fitImageSize,
|
|
49
|
+
httpErrorText: () => httpErrorText,
|
|
49
50
|
inject: () => inject,
|
|
50
51
|
isPassiveReplyError: () => isPassiveReplyError,
|
|
51
52
|
isRetryableStatus: () => isRetryableStatus,
|
|
@@ -136,12 +137,23 @@ Agent \u6307\u4EE4\u6A21\u677F\u53EF\u7528\u5360\u4F4D\u7B26\uFF1A{command} \u63
|
|
|
136
137
|
var logger = new import_koishi.Logger(name);
|
|
137
138
|
function computeRetryDelay(status, retryAfter, baseInterval, attempt) {
|
|
138
139
|
const base = baseInterval > 0 ? baseInterval : 1e3;
|
|
139
|
-
|
|
140
|
+
const worthBackoff = status === 429 || status === 503 || typeof status === "number" && status >= 500 || status === 0;
|
|
141
|
+
if (worthBackoff) {
|
|
140
142
|
if (retryAfter && retryAfter > 0) return retryAfter;
|
|
141
143
|
return Math.min(base * Math.pow(2, attempt), 3e4);
|
|
142
144
|
}
|
|
143
145
|
return base;
|
|
144
146
|
}
|
|
147
|
+
function httpErrorText(error) {
|
|
148
|
+
if (!error) return "\u672A\u77E5\u539F\u56E0";
|
|
149
|
+
const data = error?.response?.data ?? error?.data ?? error?.response;
|
|
150
|
+
const upstream = typeof data === "string" ? data : data?.error?.message ?? data?.message ?? data?.msg;
|
|
151
|
+
let text = String(error?.message || error?.toString?.() || "\u672A\u77E5\u539F\u56E0").trim();
|
|
152
|
+
if (typeof upstream === "string" && upstream.trim() && !text.includes(upstream.trim())) {
|
|
153
|
+
text = `${text}\uFF5C\u63A5\u53E3\u8FD4\u56DE\uFF1A${upstream.trim().slice(0, 200)}`;
|
|
154
|
+
}
|
|
155
|
+
return text;
|
|
156
|
+
}
|
|
145
157
|
function extractJsonObject(raw) {
|
|
146
158
|
if (!raw) return null;
|
|
147
159
|
let text = String(raw).trim();
|
|
@@ -1771,7 +1783,10 @@ Negative prompt: ${String(args.negative_prompt).trim()}` : prompt;
|
|
|
1771
1783
|
ok: res.ok,
|
|
1772
1784
|
references: capped,
|
|
1773
1785
|
...unknown.length ? { unknown } : {},
|
|
1774
|
-
...res.ok ? { note: "\u56FE\u5DF2\
|
|
1786
|
+
...res.ok ? res.queued ? { queued: true, note: "\u7ED8\u56FE\u4EFB\u52A1\u5DF2\u6392\u8FDB\u540E\u53F0\u961F\u5217\uFF0C\u8FD8\u6CA1\u6709\u753B\u5B8C\u3002\u8BF7\u544A\u8BC9\u7528\u6237\u300C\u5DF2\u7ECF\u6392\u4E0A\u961F\uFF0C\u753B\u597D\u540E\u4F1A\u5728\u8FD9\u91CC\u901A\u77E5\u300D\uFF0C\u4E0D\u8981\u8BF4\u5DF2\u7ECF\u753B\u597D\u6216\u5DF2\u7ECF\u53D1\u51FA\u56FE\u7247" } : { sent: true, note: "\u56FE\u5DF2\u7ECF\u53D1\u5230\u7FA4\u91CC\u4E86\uFF0C\u56DE\u4E00\u53E5\u7B80\u77ED\u8BF4\u660E\u5373\u53EF\uFF0C\u4E0D\u8981\u518D\u95EE\u300C\u8FD8\u8981\u6211\u505A\u4EC0\u4E48\u300D" } : {
|
|
1787
|
+
error: res.error === "invalidimage" ? "\u8FD9\u4E9B\u53C2\u8003\u56FE\u4E00\u5F20\u90FD\u4E0B\u8F7D\u4E0D\u4E0B\u6765\uFF0C\u68C0\u67E5\u94FE\u63A5\u8FD8\u80FD\u4E0D\u80FD\u8BBF\u95EE" : `\u7ED8\u56FE\u63A5\u53E3\u8FD9\u6B21\u6CA1\u51FA\u56FE\uFF1A${(res.reason || "\u672A\u77E5\u539F\u56E0").slice(0, 160)}`,
|
|
1788
|
+
note: "\u5982\u5B9E\u544A\u8BC9\u7528\u6237\u8FD9\u6B21\u6CA1\u753B\u51FA\u6765\u3001\u53EF\u80FD\u662F\u4EC0\u4E48\u539F\u56E0\uFF0C\u5E76\u5EFA\u8BAE\u7A0D\u540E\u518D\u8BD5\u6216\u6362\u4E2A\u6A21\u578B\uFF0C\u4E0D\u8981\u8BF4\u5DF2\u7ECF\u753B\u597D\u4E86"
|
|
1789
|
+
}
|
|
1775
1790
|
});
|
|
1776
1791
|
}
|
|
1777
1792
|
}
|
|
@@ -1895,17 +1910,24 @@ ${echo}` : echo);
|
|
|
1895
1910
|
done: session.text("image-prompt.messages.bgdone"),
|
|
1896
1911
|
failed: session.text("image-prompt.messages.failed"),
|
|
1897
1912
|
error: session.text("image-prompt.messages.error")
|
|
1913
|
+
},
|
|
1914
|
+
/** 带上接口给的真实原因,用户一看就知道是模型那边的问题还是自己配置的问题 */
|
|
1915
|
+
formatFailed: (reason) => {
|
|
1916
|
+
const head = String(session.text("image-prompt.messages.failed") || "").replace(/[,,。]\s*$/, "");
|
|
1917
|
+
if (!reason) return `${head}\u3002`;
|
|
1918
|
+
const clean = String(reason).replace(/\s+/g, " ").trim();
|
|
1919
|
+
return `${head}\uFF08${clean.slice(0, 120)}\uFF09`;
|
|
1898
1920
|
}
|
|
1899
1921
|
};
|
|
1900
1922
|
const queued = enqueueDrawJob(snapshot, files);
|
|
1901
1923
|
await sendNotice(session, `${session.text("image-prompt.messages.bgstart", [files.length])}${queued > 0 ? session.text("image-prompt.messages.bgqueue", [queued]) : ""}`);
|
|
1902
|
-
return { ok: true };
|
|
1924
|
+
return { ok: true, queued: true };
|
|
1903
1925
|
}
|
|
1904
|
-
const
|
|
1905
|
-
if (!
|
|
1906
|
-
await saveToGallery(
|
|
1907
|
-
await sendResultImage(session,
|
|
1908
|
-
return { ok: true, url:
|
|
1926
|
+
const gen = await generateFigureImage(files, text);
|
|
1927
|
+
if (!gen.url) return { ok: false, error: "failed", reason: gen.reason };
|
|
1928
|
+
await saveToGallery(gen.url, buildGalleryDescription(text, userInputText), commandName);
|
|
1929
|
+
await sendResultImage(session, gen.url, commandName);
|
|
1930
|
+
return { ok: true, url: gen.url };
|
|
1909
1931
|
}
|
|
1910
1932
|
async function sendNotice(session, text) {
|
|
1911
1933
|
const value = String(text ?? "").trim();
|
|
@@ -1966,7 +1988,7 @@ ${echo}` : echo);
|
|
|
1966
1988
|
if (images.length === 0) return session.text("image-prompt.messages.needimages");
|
|
1967
1989
|
const res = await drawWithFiles(session, cmdConfig, promptText, images, userInputText, { optimizeFailed });
|
|
1968
1990
|
if (!res.ok) {
|
|
1969
|
-
return res.error === "invalidimage" ? session.text("image-prompt.messages.invalidimage") : session.text("image-prompt.messages.failed");
|
|
1991
|
+
return res.error === "invalidimage" ? session.text("image-prompt.messages.invalidimage") : res.reason ? `${String(session.text("image-prompt.messages.failed") || "").replace(/[,,。]\s*$/, "")}\uFF08${res.reason.replace(/\s+/g, " ").trim().slice(0, 120)}\uFF09` : session.text("image-prompt.messages.failed");
|
|
1970
1992
|
}
|
|
1971
1993
|
}
|
|
1972
1994
|
let runningJobs = 0;
|
|
@@ -1994,14 +2016,14 @@ ${echo}` : echo);
|
|
|
1994
2016
|
const send = (parts) => sendBotMessage(snapshot.bot, snapshot.channelId, parts, snapshot.guildId, snapshot.messageId);
|
|
1995
2017
|
try {
|
|
1996
2018
|
if (!isActive || !ctx.scope.isActive) return;
|
|
1997
|
-
const
|
|
1998
|
-
if (!
|
|
1999
|
-
await send([snapshot.texts.failed]);
|
|
2019
|
+
const gen = await generateFigureImage(files, snapshot.promptText);
|
|
2020
|
+
if (!gen.url) {
|
|
2021
|
+
await send([snapshot.formatFailed ? snapshot.formatFailed(gen.reason) : snapshot.texts.failed]);
|
|
2000
2022
|
return;
|
|
2001
2023
|
}
|
|
2002
|
-
await saveToGallery(
|
|
2024
|
+
await saveToGallery(gen.url, buildGalleryDescription(snapshot.promptText, ""), snapshot.commandName);
|
|
2003
2025
|
await send([`${snapshot.texts.done}
|
|
2004
|
-
`, await buildResultImagePart(
|
|
2026
|
+
`, await buildResultImagePart(gen.url)]);
|
|
2005
2027
|
} catch (error) {
|
|
2006
2028
|
ctx.logger.error(`[${snapshot.commandName}] \u540E\u53F0\u7ED8\u56FE\u5931\u8D25:`, error);
|
|
2007
2029
|
try {
|
|
@@ -2556,18 +2578,20 @@ ${lines.join("\n")}`]);
|
|
|
2556
2578
|
return await sendChatRequest(requestBody);
|
|
2557
2579
|
} catch (error) {
|
|
2558
2580
|
ctx.logger.error(`\u751F\u6210\u56FE\u7247\u65F6\u53D1\u751F\u9519\u8BEF: ${error}`);
|
|
2559
|
-
return null;
|
|
2581
|
+
return { url: null, reason: String(error?.message || error) };
|
|
2560
2582
|
}
|
|
2561
2583
|
}
|
|
2562
2584
|
async function sendChatRequest(requestBody) {
|
|
2563
2585
|
let retryCount = 0;
|
|
2564
|
-
|
|
2586
|
+
const maxAttempts = (config.maxRetries ?? 0) + 1;
|
|
2587
|
+
let lastReason = "";
|
|
2588
|
+
while (retryCount < maxAttempts) {
|
|
2565
2589
|
if (!isActive || !ctx.scope.isActive) {
|
|
2566
2590
|
ctx.logger.info("\u63D2\u4EF6\u5DF2\u5378\u8F7D\uFF0C\u505C\u6B62\u91CD\u8BD5");
|
|
2567
|
-
return null;
|
|
2591
|
+
return { url: null, reason: lastReason };
|
|
2568
2592
|
}
|
|
2569
2593
|
try {
|
|
2570
|
-
logInfo2(`\u53D1\u9001\u8BF7\u6C42\u5230 ${config.baseUrl}\uFF0C\u7B2C ${retryCount + 1} \u6B21\u5C1D\u8BD5`);
|
|
2594
|
+
logInfo2(`\u53D1\u9001\u8BF7\u6C42\u5230 ${config.baseUrl}\uFF0C\u7B2C ${retryCount + 1}/${maxAttempts} \u6B21\u5C1D\u8BD5`);
|
|
2571
2595
|
const headers = {
|
|
2572
2596
|
"Content-Type": "application/json"
|
|
2573
2597
|
};
|
|
@@ -2583,35 +2607,35 @@ ${lines.join("\n")}`]);
|
|
|
2583
2607
|
if (markdownMatch && markdownMatch[1]) {
|
|
2584
2608
|
const imageUrl = markdownMatch[1];
|
|
2585
2609
|
logInfo2(`\u6210\u529F\u83B7\u53D6\u56FE\u7247URL: ${imageUrl}`);
|
|
2586
|
-
return imageUrl;
|
|
2610
|
+
return { url: imageUrl };
|
|
2587
2611
|
}
|
|
2588
2612
|
}
|
|
2589
2613
|
}
|
|
2590
|
-
|
|
2591
|
-
throw new Error(errorMsg);
|
|
2614
|
+
throw new Error("\u54CD\u5E94\u4E2D\u672A\u627E\u5230\u56FE\u7247URL");
|
|
2592
2615
|
} catch (error) {
|
|
2593
2616
|
retryCount++;
|
|
2594
|
-
const
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2617
|
+
const reason = httpErrorText(error);
|
|
2618
|
+
lastReason = reason;
|
|
2619
|
+
const statusCode = error.response?.status || error.status || error.code || 0;
|
|
2620
|
+
logInfo2(`\u8BF7\u6C42\u5931\u8D25\uFF08\u7B2C ${retryCount}/${maxAttempts} \u6B21\uFF09\uFF1A${reason}`);
|
|
2621
|
+
if (reason.includes("insufficient_quota") || statusCode === 429) {
|
|
2598
2622
|
ctx.logger.error("API \u914D\u989D\u4E0D\u8DB3\uFF0C\u505C\u6B62\u91CD\u8BD5");
|
|
2599
|
-
return null;
|
|
2623
|
+
return { url: null, reason };
|
|
2600
2624
|
}
|
|
2601
|
-
if (retryCount
|
|
2602
|
-
|
|
2603
|
-
|
|
2625
|
+
if (retryCount < maxAttempts) {
|
|
2626
|
+
const delay = computeRetryDelay(statusCode, void 0, config.retryInterval, retryCount - 1);
|
|
2627
|
+
logInfo2(`${delay}ms \u540E\u91CD\u8BD5`);
|
|
2628
|
+
await (0, import_koishi.sleep)(delay);
|
|
2604
2629
|
if (!isActive || !ctx.scope.isActive) {
|
|
2605
2630
|
ctx.logger.info("\u63D2\u4EF6\u5DF2\u5378\u8F7D\uFF0C\u505C\u6B62\u91CD\u8BD5");
|
|
2606
|
-
return null;
|
|
2631
|
+
return { url: null, reason };
|
|
2607
2632
|
}
|
|
2608
2633
|
} else {
|
|
2609
|
-
ctx.logger.error(`\
|
|
2610
|
-
return null;
|
|
2634
|
+
ctx.logger.error(`\u5DF2\u91CD\u8BD5 ${maxAttempts} \u6B21\u4ECD\u5931\u8D25\uFF0C\u6700\u540E\u9519\u8BEF\uFF1A${reason}`);
|
|
2611
2635
|
}
|
|
2612
2636
|
}
|
|
2613
2637
|
}
|
|
2614
|
-
return null;
|
|
2638
|
+
return { url: null, reason: lastReason };
|
|
2615
2639
|
}
|
|
2616
2640
|
function logInfo2(...args) {
|
|
2617
2641
|
if (config.loggerinfo) {
|
|
@@ -2639,6 +2663,7 @@ ${lines.join("\n")}`]);
|
|
|
2639
2663
|
extractServerMessage,
|
|
2640
2664
|
extractTextToRender,
|
|
2641
2665
|
fitImageSize,
|
|
2666
|
+
httpErrorText,
|
|
2642
2667
|
inject,
|
|
2643
2668
|
isPassiveReplyError,
|
|
2644
2669
|
isRetryableStatus,
|
package/package.json
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "koishi-plugin-image-prompt",
|
|
3
|
-
"description": "多功能 AI 图像处理插件:Agent 模式把「查不查图库 / 要不要追问 / 什么时候开画」全交给模型自己决定,支持手办化、Coser 化、Minecraft 化等 47 种风格与自定义提示词",
|
|
4
|
-
"version": "2.0.
|
|
5
|
-
"main": "lib/index.js",
|
|
6
|
-
"typings": "lib/index.d.ts",
|
|
7
|
-
"files": [
|
|
8
|
-
"lib",
|
|
9
|
-
"src"
|
|
10
|
-
],
|
|
11
|
-
"license": "GPL-3.0",
|
|
12
|
-
"keywords": [
|
|
13
|
-
"chatbot",
|
|
14
|
-
"koishi",
|
|
15
|
-
"plugin",
|
|
16
|
-
"手办化"
|
|
17
|
-
],
|
|
18
|
-
"contributors": [
|
|
19
|
-
"maimai993 <3544815113@qq.com>"
|
|
20
|
-
],
|
|
21
|
-
"homepage": "https://github.com/maimai993/koishi-plugin-image-prompt",
|
|
22
|
-
"bugs": {
|
|
23
|
-
"url": "https://github.com/maimai993/koishi-plugin-image-prompt/issues"
|
|
24
|
-
},
|
|
25
|
-
"peerDependencies": {
|
|
26
|
-
"koishi": "^4.18.7"
|
|
27
|
-
}
|
|
28
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-image-prompt",
|
|
3
|
+
"description": "多功能 AI 图像处理插件:Agent 模式把「查不查图库 / 要不要追问 / 什么时候开画」全交给模型自己决定,支持手办化、Coser 化、Minecraft 化等 47 种风格与自定义提示词",
|
|
4
|
+
"version": "2.0.2",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"license": "GPL-3.0",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"chatbot",
|
|
14
|
+
"koishi",
|
|
15
|
+
"plugin",
|
|
16
|
+
"手办化"
|
|
17
|
+
],
|
|
18
|
+
"contributors": [
|
|
19
|
+
"maimai993 <3544815113@qq.com>"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://github.com/maimai993/koishi-plugin-image-prompt",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/maimai993/koishi-plugin-image-prompt/issues"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"koishi": "^4.18.7"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/readme.md
CHANGED
|
@@ -460,6 +460,31 @@ max_tokens=8000 → finish_reason: stop, content: "{...}", reasoning_tokens:
|
|
|
460
460
|
每一轮的模型输出与工具调用都会写进日志,一眼就能看出是模型没调工具、
|
|
461
461
|
还是调了但参数不对。
|
|
462
462
|
|
|
463
|
+
### 🩺 排查「画不出来 / 接口报错」
|
|
464
|
+
|
|
465
|
+
失败时插件会把**服务端返回的真实原因**原样带出来,先看这一句再决定怎么办:
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
[I] 请求失败(第 3/3 次):Internal Server Error|接口返回:connect ECONNREFUSED 192.168.0.85:20000
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
群里收到的失败消息里同样会带上这句。**这串东西不是乱码**,它通常直接说明问题在哪:
|
|
472
|
+
|
|
473
|
+
| 看到的说法 | 含义 | 怎么办 |
|
|
474
|
+
| --- | --- | --- |
|
|
475
|
+
| `connect ECONNREFUSED <ip>:<端口>` | 中转站的后端节点没起来 / 挂了 | **跟接口提供方无关也不在你这边**,换模型或等它恢复 |
|
|
476
|
+
| `当前模型负载较高` | 该模型现在挤,不是配置问题 | 换个负载均衡点的模型,或稍后再试 |
|
|
477
|
+
| `Model Not Exist` | 模型名写错了 | 核对模型名(大小写也算) |
|
|
478
|
+
| `Invalid API key` | 密钥不对或已过期 | 换密钥 |
|
|
479
|
+
| `insufficient_quota` | 额度没了 | 充值后会立刻恢复,插件不会傻重试 |
|
|
480
|
+
|
|
481
|
+
> 同一个中转站上**所有**绘图模型都报同一个 `ECONNREFUSED` 时,基本可以确定是对方整条链路挂了,
|
|
482
|
+
> 换个模型名也没用,只能等它恢复。判断是否全挂:换 2~3 个不同的绘图模型各试一次,
|
|
483
|
+
> 如果都指向同一个 IP 端口,就是服务端的问题。
|
|
484
|
+
|
|
485
|
+
另外,`429 限流` 和 `5xx 服务端错误` 现在是**指数退避**重试(1s → 2s → 4s…),
|
|
486
|
+
不再用固定间隔连着猛打 —— 对端真的在重启的时候,等久一点反而更容易成功。
|
|
487
|
+
|
|
463
488
|
### ❌ 不会重试的错误
|
|
464
489
|
|
|
465
490
|
`400 / 401 / 403 / 404 / 422` 这类属于永久性错误(参数错、密钥错、地址错、模型不存在),
|
package/src/index.ts
CHANGED
|
@@ -81,13 +81,34 @@ const logger = new Logger(name)
|
|
|
81
81
|
*/
|
|
82
82
|
export function computeRetryDelay(status: any, retryAfter: number | undefined, baseInterval: number, attempt: number): number {
|
|
83
83
|
const base = baseInterval > 0 ? baseInterval : 1000
|
|
84
|
-
|
|
84
|
+
// 429 限流、5xx 服务端错误、status=0(网络层就断了)都值得退避:
|
|
85
|
+
// 上游节点挂了的时候,固定间隔连着猛打四次只是徒增负担
|
|
86
|
+
const worthBackoff = status === 429 || status === 503 || (typeof status === 'number' && status >= 500) || status === 0
|
|
87
|
+
if (worthBackoff) {
|
|
85
88
|
if (retryAfter && retryAfter > 0) return retryAfter
|
|
86
89
|
return Math.min(base * Math.pow(2, attempt), 30000)
|
|
87
90
|
}
|
|
88
91
|
return base
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
/**
|
|
95
|
+
* 从接口抛出的错误里挖出「到底为什么失败」。
|
|
96
|
+
* koishi 的 http 错误对 5xx 往往只留一句状态文本("Internal Server Error"),
|
|
97
|
+
* 真正的病根(比如上游 ECONNREFUSED、模型负载过高)藏在响应体里,得自己捞出来。
|
|
98
|
+
*/
|
|
99
|
+
export function httpErrorText(error: any): string {
|
|
100
|
+
if (!error) return '未知原因'
|
|
101
|
+
const data = error?.response?.data ?? error?.data ?? error?.response
|
|
102
|
+
const upstream = typeof data === 'string'
|
|
103
|
+
? data
|
|
104
|
+
: data?.error?.message ?? data?.message ?? data?.msg
|
|
105
|
+
let text = String(error?.message || error?.toString?.() || '未知原因').trim()
|
|
106
|
+
if (typeof upstream === 'string' && upstream.trim() && !text.includes(upstream.trim())) {
|
|
107
|
+
text = `${text}|接口返回:${upstream.trim().slice(0, 200)}`
|
|
108
|
+
}
|
|
109
|
+
return text
|
|
110
|
+
}
|
|
111
|
+
|
|
91
112
|
/** 从任意文本里挖出第一个 JSON 对象(容忍代码块围栏和前后废话) */
|
|
92
113
|
export function extractJsonObject(raw: string | null | undefined): any | null {
|
|
93
114
|
if (!raw) return null
|
|
@@ -2303,8 +2324,16 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
2303
2324
|
references: capped,
|
|
2304
2325
|
...(unknown.length ? { unknown } : {}),
|
|
2305
2326
|
...(res.ok
|
|
2306
|
-
?
|
|
2307
|
-
|
|
2327
|
+
? (res.queued
|
|
2328
|
+
// 后台绘图:这里只是排上队了,图还没开始画,绝不能让模型告诉用户「画好了」
|
|
2329
|
+
? { queued: true, note: '绘图任务已排进后台队列,还没有画完。请告诉用户「已经排上队,画好后会在这里通知」,不要说已经画好或已经发出图片' }
|
|
2330
|
+
: { sent: true, note: '图已经发到群里了,回一句简短说明即可,不要再问「还要我做什么」' })
|
|
2331
|
+
: {
|
|
2332
|
+
error: res.error === 'invalidimage'
|
|
2333
|
+
? '这些参考图一张都下载不下来,检查链接还能不能访问'
|
|
2334
|
+
: `绘图接口这次没出图:${(res.reason || '未知原因').slice(0, 160)}`,
|
|
2335
|
+
note: '如实告诉用户这次没画出来、可能是什么原因,并建议稍后再试或换个模型,不要说已经画好了',
|
|
2336
|
+
}),
|
|
2308
2337
|
})
|
|
2309
2338
|
}
|
|
2310
2339
|
}
|
|
@@ -2428,7 +2457,7 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
2428
2457
|
imageUrls: string[],
|
|
2429
2458
|
userInputText: string,
|
|
2430
2459
|
options: { optimizeFailed?: boolean, requireInput?: boolean } = {}
|
|
2431
|
-
): Promise<{ ok: boolean, error?: string, url?: string }> {
|
|
2460
|
+
): Promise<{ ok: boolean, error?: string, url?: string, reason?: string, queued?: boolean }> {
|
|
2432
2461
|
const commandName = cmdConfig.name
|
|
2433
2462
|
const requireInput = options.requireInput !== false
|
|
2434
2463
|
const files = await downloadFiles(imageUrls)
|
|
@@ -2474,18 +2503,26 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
2474
2503
|
failed: session.text('image-prompt.messages.failed'),
|
|
2475
2504
|
error: session.text('image-prompt.messages.error'),
|
|
2476
2505
|
},
|
|
2506
|
+
/** 带上接口给的真实原因,用户一看就知道是模型那边的问题还是自己配置的问题 */
|
|
2507
|
+
formatFailed: (reason?: string) => {
|
|
2508
|
+
const head = String(session.text('image-prompt.messages.failed') || '').replace(/[,,。]\s*$/, '')
|
|
2509
|
+
if (!reason) return `${head}。`
|
|
2510
|
+
const clean = String(reason).replace(/\s+/g, ' ').trim()
|
|
2511
|
+
return `${head}(${clean.slice(0, 120)})`
|
|
2512
|
+
},
|
|
2477
2513
|
}
|
|
2478
2514
|
const queued = enqueueDrawJob(snapshot, files)
|
|
2479
2515
|
await sendNotice(session, `${session.text('image-prompt.messages.bgstart', [files.length])}${queued > 0 ? session.text('image-prompt.messages.bgqueue', [queued]) : ''}`)
|
|
2480
|
-
|
|
2516
|
+
// 注意:这里是「排上队了」,不是「画好了」。别让上层当成已完成通知用户
|
|
2517
|
+
return { ok: true, queued: true }
|
|
2481
2518
|
}
|
|
2482
2519
|
|
|
2483
|
-
const
|
|
2484
|
-
if (!
|
|
2520
|
+
const gen = await generateFigureImage(files, text)
|
|
2521
|
+
if (!gen.url) return { ok: false, error: 'failed', reason: gen.reason }
|
|
2485
2522
|
|
|
2486
|
-
await saveToGallery(
|
|
2487
|
-
await sendResultImage(session,
|
|
2488
|
-
return { ok: true, url:
|
|
2523
|
+
await saveToGallery(gen.url, buildGalleryDescription(text, userInputText), commandName)
|
|
2524
|
+
await sendResultImage(session, gen.url, commandName)
|
|
2525
|
+
return { ok: true, url: gen.url }
|
|
2489
2526
|
}
|
|
2490
2527
|
|
|
2491
2528
|
/** 发一条状态说明(支持的平台用 markdown,省得长文被拆) */
|
|
@@ -2565,7 +2602,9 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
2565
2602
|
if (!res.ok) {
|
|
2566
2603
|
return res.error === 'invalidimage'
|
|
2567
2604
|
? session.text('image-prompt.messages.invalidimage')
|
|
2568
|
-
:
|
|
2605
|
+
: res.reason
|
|
2606
|
+
? `${String(session.text('image-prompt.messages.failed') || '').replace(/[,,。]\s*$/, '')}(${res.reason.replace(/\s+/g, ' ').trim().slice(0, 120)})`
|
|
2607
|
+
: session.text('image-prompt.messages.failed')
|
|
2569
2608
|
}
|
|
2570
2609
|
}
|
|
2571
2610
|
// ============ 后台绘图 ============
|
|
@@ -2598,13 +2637,13 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
2598
2637
|
|
|
2599
2638
|
try {
|
|
2600
2639
|
if (!isActive || !ctx.scope.isActive) return
|
|
2601
|
-
const
|
|
2602
|
-
if (!
|
|
2603
|
-
await send([snapshot.texts.failed])
|
|
2640
|
+
const gen = await generateFigureImage(files, snapshot.promptText)
|
|
2641
|
+
if (!gen.url) {
|
|
2642
|
+
await send([snapshot.formatFailed ? snapshot.formatFailed(gen.reason) : snapshot.texts.failed])
|
|
2604
2643
|
return
|
|
2605
2644
|
}
|
|
2606
|
-
await saveToGallery(
|
|
2607
|
-
await send([`${snapshot.texts.done}\n`, await buildResultImagePart(
|
|
2645
|
+
await saveToGallery(gen.url, buildGalleryDescription(snapshot.promptText, ''), snapshot.commandName)
|
|
2646
|
+
await send([`${snapshot.texts.done}\n`, await buildResultImagePart(gen.url)])
|
|
2608
2647
|
} catch (error) {
|
|
2609
2648
|
ctx.logger.error(`[${snapshot.commandName}] 后台绘图失败:`, error)
|
|
2610
2649
|
try {
|
|
@@ -3273,7 +3312,8 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
3273
3312
|
return images
|
|
3274
3313
|
}
|
|
3275
3314
|
|
|
3276
|
-
|
|
3315
|
+
/** @returns 图片地址;失败时带回接口给的真实原因,方便直接告诉用户「是模型那边的毛病」 */
|
|
3316
|
+
async function generateFigureImage(files: any[], prompt: string): Promise<{ url: string | null, reason?: string }> {
|
|
3277
3317
|
try {
|
|
3278
3318
|
const dataUrls: string[] = []
|
|
3279
3319
|
|
|
@@ -3352,22 +3392,24 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
3352
3392
|
return await sendChatRequest(requestBody)
|
|
3353
3393
|
} catch (error) {
|
|
3354
3394
|
ctx.logger.error(`生成图片时发生错误: ${error}`)
|
|
3355
|
-
return null
|
|
3395
|
+
return { url: null, reason: String(error?.message || error) }
|
|
3356
3396
|
}
|
|
3357
3397
|
}
|
|
3358
3398
|
|
|
3359
|
-
async function sendChatRequest(requestBody: any): Promise<string | null> {
|
|
3399
|
+
async function sendChatRequest(requestBody: any): Promise<{ url: string | null, reason?: string }> {
|
|
3360
3400
|
let retryCount = 0
|
|
3401
|
+
const maxAttempts = (config.maxRetries ?? 0) + 1
|
|
3402
|
+
let lastReason = ''
|
|
3361
3403
|
|
|
3362
|
-
while (retryCount
|
|
3404
|
+
while (retryCount < maxAttempts) {
|
|
3363
3405
|
// 在每次重试前检查上下文状态
|
|
3364
3406
|
if (!isActive || !ctx.scope.isActive) {
|
|
3365
3407
|
ctx.logger.info('插件已卸载,停止重试')
|
|
3366
|
-
return null
|
|
3408
|
+
return { url: null, reason: lastReason }
|
|
3367
3409
|
}
|
|
3368
3410
|
|
|
3369
3411
|
try {
|
|
3370
|
-
logInfo(`发送请求到 ${config.baseUrl},第 ${retryCount + 1} 次尝试`)
|
|
3412
|
+
logInfo(`发送请求到 ${config.baseUrl},第 ${retryCount + 1}/${maxAttempts} 次尝试`)
|
|
3371
3413
|
|
|
3372
3414
|
const headers: Record<string, string> = {
|
|
3373
3415
|
'Content-Type': 'application/json'
|
|
@@ -3389,40 +3431,41 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
3389
3431
|
if (markdownMatch && markdownMatch[1]) {
|
|
3390
3432
|
const imageUrl = markdownMatch[1]
|
|
3391
3433
|
logInfo(`成功获取图片URL: ${imageUrl}`)
|
|
3392
|
-
return imageUrl
|
|
3434
|
+
return { url: imageUrl }
|
|
3393
3435
|
}
|
|
3394
3436
|
}
|
|
3395
3437
|
}
|
|
3396
3438
|
|
|
3397
|
-
|
|
3398
|
-
throw new Error(errorMsg)
|
|
3439
|
+
throw new Error('响应中未找到图片URL')
|
|
3399
3440
|
} catch (error) {
|
|
3400
3441
|
retryCount++
|
|
3401
|
-
const
|
|
3402
|
-
|
|
3442
|
+
const reason = httpErrorText(error)
|
|
3443
|
+
lastReason = reason
|
|
3444
|
+
const statusCode = error.response?.status || error.status || error.code || 0
|
|
3403
3445
|
|
|
3404
|
-
logInfo(
|
|
3446
|
+
logInfo(`请求失败(第 ${retryCount}/${maxAttempts} 次):${reason}`)
|
|
3405
3447
|
|
|
3406
3448
|
// 检查是否为配额不足错误
|
|
3407
|
-
if (
|
|
3449
|
+
if (reason.includes('insufficient_quota') || statusCode === 429) {
|
|
3408
3450
|
ctx.logger.error('API 配额不足,停止重试')
|
|
3409
|
-
return null
|
|
3451
|
+
return { url: null, reason }
|
|
3410
3452
|
}
|
|
3411
3453
|
|
|
3412
|
-
if (retryCount
|
|
3413
|
-
|
|
3414
|
-
|
|
3454
|
+
if (retryCount < maxAttempts) {
|
|
3455
|
+
// 服务端错误/网络错误走指数退避,别用固定间隔猛打
|
|
3456
|
+
const delay = computeRetryDelay(statusCode, undefined, config.retryInterval, retryCount - 1)
|
|
3457
|
+
logInfo(`${delay}ms 后重试`)
|
|
3458
|
+
await sleep(delay)
|
|
3415
3459
|
if (!isActive || !ctx.scope.isActive) {
|
|
3416
3460
|
ctx.logger.info('插件已卸载,停止重试')
|
|
3417
|
-
return null
|
|
3461
|
+
return { url: null, reason }
|
|
3418
3462
|
}
|
|
3419
3463
|
} else {
|
|
3420
|
-
ctx.logger.error(
|
|
3421
|
-
return null
|
|
3464
|
+
ctx.logger.error(`已重试 ${maxAttempts} 次仍失败,最后错误:${reason}`)
|
|
3422
3465
|
}
|
|
3423
3466
|
}
|
|
3424
3467
|
}
|
|
3425
|
-
return null
|
|
3468
|
+
return { url: null, reason: lastReason }
|
|
3426
3469
|
}
|
|
3427
3470
|
|
|
3428
3471
|
function logInfo(...args: any[]) {
|